Backtest System Audit — 방법론 검증
2026-04-14 | by Lucky 목적: 기존 백테스팅 코드의 방법론적 문제점 식별 및 개선 방향 제시
결론 (TL;DR)
현재 백테스트 결과는 2~5배 과대 추정되었을 가능성이 높다.
핵심 원인 3가지:
- Split 미보정 가격 사용 → 모든 시그널/필터가 오염된 데이터 기반
- 거래 비용 0 → 바이오텍 소형주 스프레드(0.5~2%) 미반영
- Sharpe ratio 잘못 계산 → 트레이드별 계산 (일별 수익률 기반이 아님)
아키텍처와 IS/OOS 분리는 잘 되어 있으나, 데이터와 메트릭이 근본적으로 문제.
1. 데이터 품질 — Split Adjustment 미적용 🔴 CRITICAL
현황
get_split_adjusted_prices()함수는 만들어졌지만 어디에서도 호출되지 않음- 모든 시그널 감지(
entry.py), 유니버스 필터(universe.py)가 raw close 사용 - DuckDB의
adj_close도 50.3% 심볼에서close와 동일 (보정 안 됨)
영향
- ATH drawdown 필터: reverse split된 종목의 과거 가격이 비현실적으로 높음
- 예: PPCB max $32.2B → ATH 대비 -99.99%로 필터 통과 (잘못됨)
- Bottom bounce 시그널: split 경계에서 가짜 바운스 감지
- reverse split 1:20 → 가격 1000으로 보임 → -95% 하락으로 오인
- 볼륨 시그널: split 전후 볼륨 혼합 → 가짜 volume explosion
- MACD/Bollinger: 이동평균이 split 경계에서 무의미
해결
# entry.py에서 모든 get_daily_prices() → get_split_adjusted_prices()로 교체
# universe.py의 ATH 계산도 split-adjusted prices 사용2. 거래 비용 미반영 🔴 CRITICAL
현황
config.py에commission=0.0,slippage=0.001정의되어 있으나 실제로 적용하는 코드 없음portfolio.py의simulate_portfolio()에서 거래 비용 차감 로직 없음
영향
- 바이오텍 소형주 bid-ask 스프레드: 0.5~2%+
- 매수 + 매도 = 왕복 1~4% 비용
- 37개 트레이드 × 2% 평균 비용 = 총 수익률에서 ~74%p 감소
- v5.1 리포트에서 1% 슬리피지 적용한 버전이 있지만, 코드에는 미반영
해결
# portfolio.py의 simulate_portfolio()에서:
trade_return_after_cost = t["return"] - 2 * slippage - commission
# 또는 exit.py에서 exit_price에 슬리피지 적용3. Sharpe Ratio 계산 오류 🟠 HIGH
현황
portfolio.py:103-104: 트레이드별 수익률의 평균/표준편차로 계산sharpe = np.mean(rets) / np.std(rets) # 연간화 안 됨, 일별 아님metrics.py:27-32: 별도 함수 있지만 input이 트레이드별 수익률이면 의미 없음
문제
- 표준 Sharpe = (일별 초과수익 평균 / 일별 표준편차) × √252
- 현재 계산: 트레이드별 수익률 기반 → 비교 불가능한 수치
- 트레이드가 적을수록 Sharpe 과대 추정
해결
- 일별 equity curve 기록 → 일별 수익률 계산 → 연간화 Sharpe
4. 볼륨 제약 미확인 🟠 HIGH
현황
- 포지션 사이즈: 포트폴리오의 10% (= 100K)
- 일일 거래량 확인 없음
영향
- 바이오텍 소형주: 일일 거래량 50K인 종목 다수
- $10K 포지션 = 일일 거래량의 20~200% → 실행 불가능
- 시장 충격(market impact) 미반영
해결
- 20일 평균 거래량의 10% 이하로 포지션 사이즈 제한
- 또는 시장 충격 모델 적용
5. MaxDD 계산 — 진입일만 기록 🟠 HIGH
현황
portfolio.py:82: equity를 새 포지션 진입 시에만 기록- 진입 없는 날의 자산 변동은 포착 안 됨
영향
- 실제 drawdown이 진입일 사이에 발생하면 누락
- 보고된 MaxDD가 실제보다 작을 수 있음
해결
- 일별 equity curve 기록 (포지션별 일별 시가총액 추적)
6. 통계적 유의성 부족 🟠 HIGH
현황
- 최소 트레이드 수: 5개 (시그널 스킵 기준)
- “pass” 기준: IS 20개 트레이드
- 최적 전략: 37개 트레이드 (v5.1)
문제
- 통계적 유의성을 위한 최소: 30~50개 트레이드
- 37개로 Sharpe 1.1 → 95% CI는 대략 [0.3, 1.9] (매우 넓음)
- 현재 신뢰구간(CI) 계산 없음
해결
- 최소 트레이드 수 30개로 상향
- Bootstrap CI 추가 (Sharpe, WR, PF에 대해)
7. Survivorship Bias — 상장폐지 처리 🟠 HIGH
현황
- 상장폐지된 종목:
exit_reason = "data_end"로 기록 - 일반 종료와 구분 없음
- 22개 심볼에 30일+ 거래 갭 존재 (halt/delist 가능성)
해결
- 상장폐지 감지: 마지막 거래일 이후 데이터 없으면 flag
- 상장폐지 시 -100% 손실 또는 별도 분석
잘 되어 있는 것 ✅
| 항목 | 평가 |
|---|---|
| IS/OOS 분리 | OOS_DATE 일관 적용, 데이터 누출 없음 |
| 진입 타이밍 | signal_date 이후 첫 거래일 close — 현실적 |
| 동일 종목 중복 보유 방지 | portfolio.py에서 체크 |
| 포지션 사이징 | cash 기반 올바른 계산 |
| 필터 프리컴퓨트 | 배치 최적화로 효율적 |
| 파라미터 저장 | config.json, state.json으로 기록 |
개선 우선순위
P0 — 즉시 수정 (결과 신뢰도 직결)
- Split-adjusted prices 적용 — entry.py, universe.py 전체
- 거래 비용 모델링 — slippage + commission 적용
- Sharpe ratio 수정 — 일별 equity curve 기반 연간화
P1 — 재백테스트 전 수정
- 일별 equity curve 기록 — MaxDD, Sharpe, CAGR 정확도
- 볼륨 제약 추가 — 20일 평균 거래량 기반
- 최소 트레이드 수 상향 — 5 → 30, pass 기준 20 → 50
- 상장폐지 처리 — data_end 트레이드 분리
P2 — 신뢰도 강화
- Bootstrap CI (Sharpe, WR)
- 데이터 버전 관리 (DuckDB checksum)
- Random seed 전역 설정
예상 영향
P0 수정 후 결과 예상:
- Sharpe: 1.1 → 0.4~0.7 (거래 비용 + 올바른 계산)
- 총 수익률: +2,164% → 수백% 수준 (거래 비용 차감)
- 트레이드 수: 변동 가능 (split 보정 후 유니버스 변경)
- 전략 유효성: 여전히 양의 알파 가능성 있으나, 규모 축소 예상