7 / 12 · AUTO TRADING WIKI
CCI 신호를 과거 데이터로 시뮬레이션하기
신호 다음 거래일 진입과 고정된 청산 규칙으로 계산 흐름을 검증합니다.
CCI 신호를 계산했다면 이제 그 신호를 과거 가격에 적용해 본다. 이번 장의 결과는 수익 인증서가 아니라, 같은 입력으로 같은 진입·청산 기록을 다시 만들 수 있는 시뮬레이션이다.
이 장의 경로·종목·숫자·로그는 교육용 가상 예시다. 작업 폴더는 C:\Users\minjun\auto-trading-lab이며, 가격 원본은 앞 장의 daily_prices(symbol, trade_date, open, high, low, close, volume)을 사용한다. 주문 API를 호출하지 않는다.
1. 시뮬레이션의 질문부터 정하기
“얼마나 벌었는가”를 보기 전에 “그 시점에 알 수 있던 정보만 사용했는가”를 확인한다. T일 종가로 계산한 CCI는 T일 장이 끝난 뒤 확정된다. 그 값을 보고 T일 시가에 매수한 것으로 계산하면 미래 정보를 사용하게 된다.
여기서는 CCI 기간을 20으로 고정하고, 전 거래일 CCI가 -100 이하이며 당일 CCI가 -100을 초과하는 상향 돌파만 예제 신호로 삼는다. CCI가 낮다는 사실 자체를 매수 신호로 취급하지 않는다. 이 기준은 계산을 설명하기 위한 학습용 규칙이며 종목 추천이나 검증된 투자 전략이 아니다.
시뮬레이션에서 확인할 질문은 네 가지다. 신호 이후의 가격을 정확히 연결했는가, 보유 기간을 일관되게 셌는가, 비용과 제외 사유를 남겼는가, 재실행해도 결과가 중복되지 않는가. 모든 질문에 기록으로 답할 수 있어야 다음 장으로 넘어간다.
2. 진입·청산 규칙을 한 문서로 고정하기
config/simulation.json에 다음과 같이 저장하도록 설계한다. 아래 비용은 실제 증권사 요율이나 현행 세율이 아니라 계산을 연습하기 위한 가정값이다.
{
"strategy_version": "cci20_cross_up_m100_v1",
"cci_period": 20,
"signal": "prev_cci <= -100 and cci > -100",
"entry": "next_session_open",
"holding_sessions": 5,
"exit": "fifth_session_close",
"same_symbol_overlap": "skip",
"fee_bps_each_side": "5",
"sell_tax_bps": "20",
"slippage_bps_each_side": "0",
"cost_model": "educational-assumption-v1"
}| 항목 | 이번 장의 처리 |
|---|---|
| 신호 확정 | T일 종가까지의 데이터로만 계산 |
| 진입 | T 다음의 실제 거래 세션 시가 |
| 보유 일수 | 진입 세션을 1일째로 계산 |
| 청산 | 5번째 보유 세션 종가 |
| 중복 신호 | 같은 종목 보유 중이면 OVERLAPPING_POSITION |
| 기간 끝의 신호 | 필요한 가격이 없으면 미완료로 분리 |
“5거래일 보유”를 entry_index + 5로 구현하면 이 문서의 규칙보다 하루 늦어진다. 진입일을 포함하므로 청산 위치는 entry_index + 4다. 주말이나 공휴일을 단순히 날짜에 더하지 않는다.
또한 다음 행이 있다는 사실만으로 다음 거래일이 확인되지는 않는다. 원본에서 하루가 빠졌다면 그다음 행은 T+2일 수 있다. 거래 캘린더와 종목의 거래 가능 상태를 함께 확인하고, 원인 불명 누락은 MISSING_EXPECTED_SESSION으로 제외한다.
3. 데이터 묶음과 실행 식별자 보관하기
같은 기간이라도 원본 가격이 수정되거나 대상 종목이 바뀌면 결과가 달라진다. 실행마다 run_id, 코드 버전, 설정 해시, 데이터 스냅샷 식별자, 대상 종목 목록의 기준일을 보관한다. 오늘의 구성 종목을 과거 전체 기간에 적용했다면 구성 종목의 생존 편향 가능성도 보고서에 쓴다.
outputs/simulation/demo-20261020-01/
manifest.json
trades.csv
skipped_signals.csv
summary.json
manifest.json에는 비밀번호가 들어간 연결 문자열 대신 dataset_id, 조회 기간, 행 수, 정렬 기준을 남긴다. 날짜·종목순으로 정렬한 입력의 해시를 보관하면 재현에 도움이 된다. Python의 SHA-256 사용 방법은 공식 hashlib 문서에 나와 있다.
데이터 조회는 ORDER BY symbol, trade_date로 고정한다. 수정주가와 원주가를 섞거나, 분할 전후 가격을 조정 없이 이어 붙이지 않는다. 가격 조정 정책을 확인할 수 없다면 그 구간을 별도로 표시하고 성과 해석을 보류한다.
앞 장의 CSV 입력은 outputs/indicators/cci_20.csv이며 지표 열은 cci_20, 신호 열은 entry_signal이다. DB에 저장했다면 cci_daily_signals.cci_value를 cci_20으로 읽고 trade_date를 신호 날짜로 연결한다. DB의 period = 20, strategy_version = 'cci20_cross_up_m100_v1' 조건으로 같은 계산 버전만 선택한다.
SELECT symbol, trade_date, cci_value AS cci_20, entry_signal
FROM cci_daily_signals
WHERE period = 20 AND strategy_version = 'cci20_cross_up_m100_v1'
ORDER BY symbol, trade_date;신호 후보는 entry_signal = true인 행이다. 가격과 연결할 때는 (symbol, trade_date)를 사용한다. CSV를 읽는 경우 불리언 열의 True·False 문자열을 엄격하게 해석하고, 문자열 "False"를 단순한 참값으로 변환하지 않는다. 이전 원고의 watch 신호와 이 장의 상향 돌파 신호를 섞지 않는다.
4. 손으로 맞출 수 있는 작은 사례 만들기
먼저 가상 종목 DEMO_A 한 개로 날짜 연결을 확인한다. 아래 날짜는 교육용 세션 목록이며 실제 거래일 캘린더를 대신하지 않는다.
| 이벤트 | 세션 | 값 |
|---|---|---|
| 직전 CCI | 2026-06-01 | -120 |
| 신호 CCI | 2026-06-02 | -85 |
| 진입·보유 1일째 | 2026-06-03 | 시가 10,000 |
| 보유 2·3·4일째 | 06-04, 06-05, 06-08 | 중간 가격 사용 |
| 보유 5일째·청산 | 2026-06-09 | 종가 10,200 |
10주를 샀다고 가정하면 매수 금액은 100,000, 매도 금액은 102,000이다. 비용 전 손익은 2,000이다. 예제 수수료는 매수 50, 매도 51이고, 가정 매도세금은 204이므로 비용 후 손익은 1,695다. 슬리피지는 이 사례에서 0으로 놓았다.
from decimal import Decimal
qty = Decimal("10")
entry = Decimal("10000") * qty
exit_value = Decimal("10200") * qty
buy_fee = entry * Decimal("0.0005")
sell_fee = exit_value * Decimal("0.0005")
sell_tax = exit_value * Decimal("0.0020")
net_pnl = exit_value - entry - buy_fee - sell_fee - sell_tax
print(net_pnl) # 1695.0000금액에는 문자열로 만든 Decimal을 사용하고 최종 반올림 위치를 별도로 정한다. 이는 소수 계산 오차를 관리하기 위한 선택이다. Python decimal 공식 문서는 정밀도와 반올림 방법을 설명한다. 실제 비용 처리 규칙은 이 예제와 다를 수 있다.
5. 일봉으로 알 수 없는 체결은 표시하기
일봉의 시가에 원하는 수량을 모두 체결했다고 보는 것은 단순한 가정이다. 거래정지, 유동성 부족, 가격 제한, 주문 지연을 재현하지 못한다. fill_model = daily_open_close_assumption을 결과에 명시하고, 거래량이 0이거나 가격이 비정상인 날은 체결로 만들지 않는다.
이번 규칙은 정해진 날의 종가 청산이므로 장중 손절·익절을 사용하지 않는다. 나중에 고가가 익절가를 넘고 저가가 손절가 아래인 일봉을 만나도 어느 가격에 먼저 도달했는지는 알 수 없다. 그때는 분봉 등 더 세밀한 데이터가 필요하며, 임의로 유리한 청산 순서를 택하지 않는다. 보수적인 우선순위를 가정하더라도 intraday_order_unknown을 기록해야 한다.
비용 전 결과와 비용 후 결과를 함께 저장한다. 슬리피지 0, 5, 10bp처럼 다른 가정을 비교할 수 있지만 가장 좋아 보이는 가정만 본문에 남기지 않는다. 이는 비용 민감도 확인이지 실제 체결 검증이 아니다.
6. 완료와 제외를 같은 무게로 저장하기
simulation_runs는 실행 단위, simulation_trades는 신호별 결과 단위로 나눈다. 결과의 고유 기준은 (run_id, symbol, signal_date)이며, 새 실험은 새 run_id를 사용한다. 동일 실행의 재시도는 기존 결과와 비교한 뒤 같은 결과이면 건너뛴다. 다른 결과이면 덮어쓰지 않고 불일치로 중단한다.
| 결과 필드 | 기록할 내용 |
|---|---|
status |
COMPLETED, SKIPPED, INCOMPLETE |
signal_date, entry_date, exit_date |
신호·진입·청산 세션 |
quantity, entry_price, exit_price |
가상 수량과 가정 가격 |
gross_pnl, fees, tax, net_pnl |
비용을 구분한 손익 |
skip_reason |
누락, 중복 보유, 가격 이상 등의 사유 |
strategy_version, dataset_id |
재현에 필요한 기준 |
SQL 조회 예시는 다음과 같다. simulation_trades는 이번 장에서 새로 만드는 교육용 결과 테이블 이름이다.
SELECT status, COALESCE(skip_reason, 'NONE') AS reason, COUNT(*) AS cnt
FROM simulation_trades
WHERE run_id = 'demo-20261020-01'
GROUP BY status, COALESCE(skip_reason, 'NONE');예를 들어 신호 21건 중 완료 15건, 기간 끝 미완료 4건, 보유 중 제외 2건이면 합계가 21건이어야 한다. 완료 15건만 보고 신호 처리율이 100%라고 표시하지 않는다. 제외가 없는 결과보다, 모든 제외를 설명할 수 있는 결과가 검토에 유용하다.
7. Codex에 구현과 확인을 나누어 요청하기
다음 요청문으로 파일 구조와 최소 재현 사례를 먼저 만든다. 실행 명령은 구현이 끝난 뒤 실제 파일 이름과 옵션을 확인하고 사용한다.
auto-trading-lab에서 과거 데이터 시뮬레이션만 구현해줘.
daily_prices와 앞 장의 CCI 결과를 읽고 CCI20 -100 상향 돌파를 사용해.
T+1 거래 세션 시가 진입, 진입일 포함 5번째 세션 종가 청산으로 고정해.
비용은 config/simulation.json의 학습용 가정을 사용하고 비용 전후를 구분해.
중복 보유, 누락 세션, 기간 끝 미완료를 사유별로 보관해.
src/simulate.py, scripts/run_simulation.ps1와 가상 데이터 예제를 만들어줘.
설정·데이터·코드 버전과 run_id를 남기고 재실행 시 중복을 방지해.
실제 주문, 증권사 모의주문, 외부 주문 전송 코드는 만들지 마.
한 종목 수동 계산과 날짜 경계·미완료 사례를 확인한 뒤 결과를 보고해.
정상 로그의 모양은 아래와 같다. 실제 실행 결과가 아니라 필요한 항목을 보여주는 예시다.
run_id=demo-20261020-01 mode=simulation dataset=fixture-v1
signals=21 completed=15 skipped=2 incomplete=4
check=entry_after_signal result=PASS
check=duplicate_result result=PASS duplicates=0
state=COMPLETED external_order_calls=0
8. 완료 기준과 문제 해결
완료 기준은 가상 사례의 날짜·비용이 손계산과 일치하고, 모든 신호가 완료·제외·미완료 중 하나에 속하며, 같은 실행을 다시 처리해도 행과 손익이 늘지 않는 것이다. 개별 거래 수익률의 평균은 자금 제약을 반영한 포트폴리오 수익률과 다르므로 별도로 표시한다.
| 증상 | 먼저 확인할 것 |
|---|---|
| 진입일이 신호일과 같음 | 날짜 결합과 next_session 선택 조건 |
| 청산이 하루 늦음 | 진입일을 1일째로 셌는지 |
| 결과가 비정상적으로 좋음 | 미래 정보, 누락 종목, 비용·가격 조정 정책 |
| 결과 건수가 재실행마다 증가 | 실행 식별자와 고유 제약 |
| 완료율이 낮음 | 기간 끝 미완료와 실제 데이터 누락의 구분 |
문제가 생기면 전략 기준을 바꾸기 전에 해당 신호 한 건의 원본 가격·CCI·날짜 연결·계산식을 함께 출력한다. 다음 장에서는 이 기록을 바탕으로 페이퍼 실행을 시작할 기술적 준비가 되었는지 점검한다.
공유 링크를 복사해 이어서 읽을 수 있습니다.