백테스트 Sharpe 3.2가 실전 0.8로 떨어진 이유 — 4가지 함정 점검법
2023년 11월, 3년치 백테스트에서 Sharpe 3.2를 뽑아냈다. 연 수익률 27%, 최대낙폭 8%. 실계좌를 열었다. 3개월 뒤 누적 수익률은 -12%였고, Sharpe는 0.8이었다. 뭐가 문제였을까?
백테스트 결과를 믿고 실전에 들어갔다가 망하는 건 퀀트 입문자의 통과의례 같은 거다. 문제는 코드 버그가 아니라 구조적 함정 네 가지였다. 이 글을 읽고 나면 백테스트 결과가 비현실적으로 좋을 때 무엇부터 의심하고 어떻게 점검하는지 알게 된다.
준비물
백테스트 함정을 점검하려면 다음이 필요하다.
- Python 3.8 이상 환경
- 백테스팅 프레임워크(Backtrader, zipline-reloaded, VectorBT 중 하나)
- 과거 가격 데이터(최소 5년치, OHLCV 형식)
- 상장폐지 종목 이력 데이터(선택이지만 생존편향 점검엔 필수)
프레임워크는 무료 버전으로 충분하다. 저는 VectorBT를 쓰는데, 벡터화 연산이 빨라서 파라미터 스캔할 때 편하더라고요.
생존편향 — 사라진 종목이 빠져 있나요?
백테스트 유니버스에 지금 살아있는 종목만 들어가 있으면 생존편향에 걸린다. 파산하거나 상장폐지된 종목은 애초에 데이터셋에서 빠져 있으니, 전략이 실패 케이스를 한 번도 안 본 거다.
점검 방법은 간단하다. 백테스트 시작일 기준으로 상장돼 있던 종목 수와 현재 데이터셋 종목 수를 비교한다.
# 2020년 1월 기준 코스피 상장 종목: 약 800개
# 현재 데이터셋 종목 수
universe_count = len(price_data.columns)
print(f"데이터셋 종목 수: {universe_count}")
# 800개보다 훨씬 적다면? 생존편향 의심
if universe_count < 750:
print("⚠️ 상장폐지 종목 누락 가능성 높음")
제 경험상 생존편향만 제거해도 백테스트 연 수익률이 22%에서 14%로 떨어졌다. 상장폐지 직전까지 급락한 종목들을 전략이 몰랐으니 당연한 결과였다.
해결책은 시점별 상장폐지 이력을 포함한 데이터를 쓰는 거다. QuantConnect나 Norgate Data 같은 유료 제공자가 이걸 처리해 준다. 무료로 하려면 거래소 공시를 크롤링해서 직접 만들어야 한다.
선행편향 — 미래 정보가 과거로 새나가나요?
선행편향은 시점 t에서 매매 결정을 내리는데 t+1 이후 데이터를 쓰는 실수다. 가장 흔한 케이스는 당일 종가 기준 신호 → 당일 종가 진입이다.
# 🔴 잘못된 예: 오늘 종가로 신호 계산 → 오늘 종가에 매수
signal = (close > sma_20)
position = signal.shift(0) # 당일 진입
# ✅ 올바른 예: 오늘 종가로 신호 계산 → 내일 시가에 매수
signal = (close > sma_20)
position = signal.shift(1) # 다음 날 진입
2026년 5월에 발표된 한 연구는 LLM을 백테스팅에 쓸 때 선행편향이 더 심각하다고 지적한다. 2024년에 학습된 모델이 2020년 주가 움직임을 이미 알고 있어서, 과거 데이터로 테스트하면 부풀려진 결과가 나온다는 거다. 출처: Summoning the Oracle to Slay It 논문
점검 방법은 타임스탬프 추적이다. 매 거래 시점마다 사용한 데이터의 타임스탬프를 찍어서, 거래 시점보다 미래 데이터가 섞였는지 확인한다.
# 거래 로그에 데이터 시점 기록
def check_lookahead(trade_date, data_date):
if data_date > trade_date:
print(f"🚨 선행편향 발견: {trade_date}에 {data_date} 데이터 사용")
return True
return False
저는 이 점검을 안 해서 6개월을 날렸어요. 재무제표 데이터를 당일 장 마감 후 바로 쓸 수 있다고 가정했는데, 실제론 공시 후 2~3일 뒤에야 정제된 데이터를 받을 수 있더라고요.
과적합 — 파라미터가 과거에만 맞춰져 있나요?
이동평균 교차 전략에서 단기 기간을 5~50일까지 스캔하고, 장기 기간을 50~200일까지 스캔해서 최적 조합을 찾는다. 백테스트 구간에선 (13, 89)가 최고 성과를 낸다. 이게 과적합이다.
파라미터 자유도가 높을수록 과거 데이터에 우연히 맞는 조합을 찾을 확률이 올라간다. 실전에선 안 먹힌다.
점검은 워크포워드 검증으로 한다. 전체 기간을 In-Sample(IS)과 Out-of-Sample(OOS)로 나눠서, IS로 파라미터를 찾고 OOS로 성과를 측정한다.
# 2018~2021: In-Sample (파라미터 최적화)
# 2022~2023: Out-of-Sample (실제 성과 측정)
is_sharpe = backtest(data_is, params_optimized) # IS: 2.8
oos_sharpe = backtest(data_oos, params_optimized) # OOS: 0.9
# OOS가 IS의 50% 미만이면 과적합 의심
if oos_sharpe / is_sharpe < 0.5:
print("⚠️ 과적합 가능성 높음")
제가 겪은 최악의 케이스는 IS Sharpe 3.5, OOS Sharpe -0.2였다. 15개 파라미터를 동시에 튜닝했더니 2018~2020년 특정 구간에만 맞춰진 쓰레기 전략이 나온 거죠.
해결책은 파라미터 개수를 줄이고, 롤링 윈도우로 여러 구간에서 검증하는 거다. 파라미터가 3개 이하면 과적합 리스크가 확 줄어든다.
거래비용 — 슬리피지와 수수료를 빼먹지 않았나요?
백테스트에서 "시가에 매수"라고 하면 정확히 시가에 체결된다. 현실은? 호가 스프레드, 체결 지연, 슬리피지가 있다. 거래 회전율이 높은 전략일수록 이 비용이 수익을 갉아먹는다.
점검 방법은 거래당 비용을 비관적으로 가정하는 거다.
# 편도 기준 비용 가정
commission = 0.0015 # 수수료 0.15%
slippage = 0.0010 # 슬리피지 0.10%
total_cost_per_trade = commission + slippage # 0.25%
# 연간 회전율 계산
annual_turnover = total_trades_per_year * 2 # 매수+매도
cost_drag = annual_turnover * total_cost_per_trade
print(f"연간 비용 차감: {cost_drag * 100:.1f}%")
제 전략은 연 회전율이 8회였다. 왕복 비용 0.5%를 곱하면 연 4%가 비용으로 나간다. 백테스트 수익률 15%에서 4%를 빼면 11%만 남는다. 수수료를 안 넣고 돌렸을 땐 이게 안 보였죠.
슬리피지는 거래량이 적은 종목일수록 심하다. 저는 일평균 거래대금 10억 원 미만 종목은 아예 유니버스에서 제외했다. 백테스트론 수익이 나도 실제론 원하는 가격에 못 사니까요.
실전에서 겪은 삽질 3가지
삽입 오류로 2주 날림: 재무제표 데이터를 병합할 때 pd.merge의 how='left'를 써야 하는데 how='inner'를 썼다. 재무제표 없는 날짜가 통째로 빠지면서 생존편향이 생겼다. 디버깅하는 데 2주 걸렸다.
슬리피지 0.05%와 0.5% 차이: 슬리피지를 0.05%로 잡았더니 백테스트 연 수익 18%가 나왔다. 실전 체결 로그를 보니 평균 슬리피지가 0.48%였다. 다시 돌리니까 수익률 9%로 반토막 났다. 소수점 하나가 전략 생사를 가른다.
OOS 기간을 너무 짧게: IS 3년, OOS 6개월로 나눴더니 OOS 구간이 우연히 상승장이었다. 과적합 전략인데도 통과해 버렸다. 최소 2년은 OOS로 확보해야 한다.
다음엔 뭘 점검하죠?
백테스트를 돌릴 때마다 이 네 가지 체크리스트를 돌린다.
- 상장폐지 종목 포함 여부 확인
- 타임스탬프 로그 찍어서 선행편향 추적
- IS/OOS 분리 후 성과 비율 계산
- 비관적 거래비용 적용 후 재측정
하나라도 걸리면 전략을 수정하거나 폐기한다. Sharpe가 높아 보여도 함정에 걸려 있으면 실전에서 돈을 날린다. 검증은 지루하지만, 실계좌에서 -12% 찍는 것보단 낫다.
자주 묻는 질문
Q. 백테스트 기간은 얼마나 길어야 하나요?
A. 최소 5년, 가능하면 10년 이상을 권장한다. 짧은 구간은 특정 시장 국면에만 맞춰질 위험이 크다. 2008 금융위기나 2020 코로나 같은 극단 구간을 반드시 포함시켜라.
Q. Walk-forward 검증 윈도우는 어떻게 나누나요?
A. IS 2~3년, OOS 1년 단위로 롤링한다. 예를 들어 2018~2020 IS로 최적화 → 2021 OOS 테스트 → 2019~2021 IS로 재최적화 → 2022 OOS 테스트 방식이다. 윈도우를 6개월마다 밀면서 반복한다.
Q. 생존편향 데이터는 어디서 구하나요?
A. 유료 제공자로는 Norgate Data(미국 주식), QuantConnect(글로벌), FnGuide(한국 주식)가 있다. 무료론 거래소 상장폐지 공시를 크롤링해서 직접 만들어야 한다. 한국거래소 KIND에서 엑셀로 받을 수 있다.
Q. 슬리피지를 얼마로 가정해야 하나요?
A. 거래 규모와 유동성에 따라 다르지만, 보수적으론 호가 스프레드의 50% + 추가 0.1~0.3%를 쓴다. 소형주나 비유동 시간대는 0.5% 이상도 가능하다. 실거래 로그가 있다면 그걸 기준으로 삼아라.