이 블로그에서 백테스트 글을 여러 편 쓰면서 결과가 너무 좋아서 오히려 이상한 순간을 몇 번 겪었다. 들여다보면 매번 데이터나 설계 문제가 있었고, 바로잡을 때마다 결과가 달라졌다. 이 글은 그때 겪은 오류 여섯 가지와, 그것을 잡아낸 방법을 정리한 기록이다. 비슷한 실험을 하는 분께 도움이 되길 바란다.
오류 1. 액면분할 때문에 이익수익률이 비정상적으로 높았다
증상: 가치 지표 백테스트에서 어떤 종목의 2012년 이익수익률(주당순이익 ÷ 주가)이 423%로 나왔다. 회사가 주가의 4배가 넘는 이익을 낸다는 뜻이라 말이 안 된다.
원인: 공시 데이터의 주당순이익(EPS)은 그 수치를 마지막으로 보고한 시점의 주식 수 기준이다. 그 뒤에 액면분할이 있으면, 오늘 기준으로 조정된 주가와 과거 EPS의 기준이 어긋난다. 그러면 나중에 분할하고 크게 오른 종목이 과거에 싸 보이는 잘못된 신호를 내고, 백테스트는 "미래에 오를 종목을 미리 알고 고른" 결과가 된다.
영향: 분할 시점을 반영해 EPS를 보정하자 린치 PEG 전략의 연 수익률이 16.9%에서 13.5%로 내려갔다. (같은 기준으로 비교한 값이다. 이 수치는 오류 3에서 설명할 시작일 문제가 섞여 있어서, 실제 글의 수치와는 기준이 다르다.)
대처: 주당 지표(EPS, 주당장부가치)는 분할 기준을 반드시 확인한다. 더 안전한 방법은 주식 수와 무관한 달러 기준 지표(순이익, 잉여현금흐름, 자기자본)를 시가총액으로 나누는 것이다. 또 이익수익률 분포의 최댓값을 한 번 찍어 보면 이런 극단값이 바로 보인다.
오류 2. 종가가 비어 있는 날 보유 종목을 0원으로 평가했다
증상: 자산 곡선에서 하루만 -30% 가까이 떨어졌다가 다음 날 +40~50%로 돌아오는 구간이 여러 번 나왔다. 슬리피지를 바꿔도 같은 날짜에 똑같이 나타났다. 매매 때문이 아니라 데이터 때문이라는 신호다.
원인: 거래정지나 데이터 누락으로 보유 종목의 종가가 비어 있는 날, 코드가 그 종목을 0원으로 평가하고 있었다. 자산이 일시적으로 그 종목 몫만큼 사라진 것처럼 계산됐다.
영향: 연 수익률과 최대 낙폭은 거의 그대로였지만, 가짜 변동 때문에 샤프지수가 0.69로 낮게 나왔다. 고치고 나서는 0.91이었다. 결과를 눈으로 보면 문제없어 보여서 발견하기 어려운 종류의 오류다.
수정 코드: 종가가 비면 직전 유효 종가로 평가한다.
# price: 날짜 x 종목 종가표, shares: 날짜 x 종목 보유 수량
last = price.ffill() # 종가가 비면 직전 종가로 채운다
value = (shares * last).sum(axis=1) # 종목별 평가금액의 합
점검 코드: 자산 곡선에서 "급락 직후 급반등"하는 날을 찾는다.
r = equity.pct_change().dropna()
suspect = r[(r < -0.08) & (r.shift(-1) > 0.08)] # 8% 넘게 빠졌다가 다음 날 8% 넘게 오른 날
print(suspect)
이 코드는 오류가 있던 버전에서 4건의 가짜 급락을 정확히 잡아냈다. 수정 후에는 1건이 남았는데, 그날 KOSPI도 -12.1%, KOSDAQ도 -14.0%로 폭락하고 다음 날 반등한 실제 시장 움직임이었다. 그래서 적발한 날은 반드시 지수와 대조해서 오류인지 실제 사건인지 확인해야 한다.
오류 3. 전략과 비교 대상의 시작일이 달랐다
증상: 린치 PEG 전략이 SPY보다 낮게 나왔는데, 자산 곡선을 그려 보니 전략 선이 두 해 가까이 100에 붙어 평평했다.
원인: PEG는 재무 이력이 3년 이상 필요해서 전략은 2013년 5월에야 매매를 시작했다. 그런데 비교 대상은 2011년 7월부터 계산돼 있었다. 전략만 두 해 동안 현금으로 있던 셈이라 비교가 불공정했다.
영향: 같은 시작일로 맞추자 전략의 연 수익률이 13.5%에서 15.5%로 올랐다. 이번에는 오류가 전략에 불리한 쪽으로 작용한 경우다.
first = strat[strat != strat.iloc[0]].index[0] # 자산이 처음 움직인 날 = 실제 시작일
bench = bench[bench.index >= first]
bench = bench / bench.iloc[0] * 100 # 비교 대상도 같은 날 100으로 다시 맞춘다
오류 4. 시간에 따라 변하는 비용을 고정값으로 넣었다
증상: 3배 레버리지 ETF를 닷컴 버블 시기까지 포함해서 검증하려고, 나스닥100 수익률을 3배로 만든 "합성 3배" 데이터를 썼다. 실제 TQQQ와 겹치는 구간에 맞춰 연 비용을 하나로 고정했다.
원인: 3배 상품은 순자산의 2배를 빌리는 구조라서 금리가 비용이다. 그런데 비용을 맞춘 구간(2010년 이후)은 저금리 시기였다. 1995~2000년의 단기금리는 연 4.6~5.8%였으니, 그 시기에는 비용을 훨씬 적게 잡은 셈이었다.
영향: 그때그때의 단기금리(T-bill)를 차입 비용으로 반영해서 다시 계산하자, 합성 3배를 끝까지 들고 간 연 수익률이 5.6%에서 4.2%로 내려갔다. 이미 발행한 글을 이 기준으로 수정했다.
교훈: 시기에 따라 크게 변하는 비용(금리, 수수료, 세금)은 한 구간에 맞춰 고정하지 말고 그 시점의 값으로 넣는다.
오류 5. 지금 살아남은 종목만 대상으로 했다 (생존 편향)
이건 완전히 해결하지 못한 문제다. 무료로 구할 수 있는 종목 목록은 현재 구성종목뿐이라, 그동안 상장폐지되거나 지수에서 빠진 종목이 없다. 오르는 종목을 사는 전략이라면 더 심각하다. 나중에 지수에 편입될 만큼 오른 종목이 미리 들어 있기 때문이다.
그래서 완전한 해결 대신 완화와 명시를 했다.
- 전략 성과를 같은 종목군의 균등가중과 비교했다. 종목군의 편향이 양쪽에 똑같이 들어가서 상대 비교는 덜 왜곡된다.
- 거래의 일부가 -60% 사고로 끝났다고 가정하는 스트레스 테스트를 돌렸다. 국내 미너비니 글에서는 이 가정으로 연 18.9%가 10.1%까지 낮아졌다. (2026-10-03 정정 후 수치. 정정 전에는 21.4%가 12.9%로 낮아졌다.)
- 글마다 "생존 편향이 있다"고 밝히고, 결과를 상한선으로 해석하도록 썼다.
오류 6. 같은 데이터로 설정을 골랐다 (과적합)
증상: 국내 미너비니 백테스트의 연 21.4%(정정 후 18.9%)가 너무 좋아 보였다.
점검: 진입 조건의 설정값 두 개를 이웃 값으로 바꿔 20가지 조합을 돌렸더니 연 수익률이 1.0%에서 22.0%까지(정정 후 0.7%에서 21.7%까지) 퍼졌다. 내가 고른 설정은 그중 가장 좋은 좁은 구역 안에 있었다. 좋은 설정 주변이 넓게 좋은 게 아니라 좁은 지점에서만 좋았다는 뜻이다. 그 규칙을 손대지 않고 미국 주식에 적용하자 연 7.1%로, SPY(11.3%)에도 못 미쳤다. (국내 글, 미국 글)
대처: 설정 주변(이웃 값)에서의 성과 분포를 본다. 설정을 고른 기간과 그 이후 기간을 나눠서 비교한다. 가능하면 다른 시장이나 다른 기간에 그대로 적용해 본다.
백테스트 결과가 좋을 때 점검할 체크리스트
- 미래 정보를 쓰고 있지 않은가? (재무 공시는 결산 후 일정 기간이 지난 뒤부터, 신호는 종가·체결은 다음 날 시가)
- 가격이 분할·배당 조정되어 있고, 주당 지표는 그 기준과 맞는가? 거래량도 같은 기준으로 조정되어 있는가?
- 자산 곡선의 극단적인 하루(급락 직후 급반등)를 지수와 대조해 봤는가?
- 전략과 비교 대상의 시작일이 같은가?
- 시기에 따라 변하는 비용(금리, 수수료, 세금)을 그 시점의 값으로 넣었는가?
- 슬리피지를 0.3%, 0.5%로 키워도 결과가 유지되는가?
- 기간을 나눠 봤을 때 특정 강세장 하나에 결과가 몰려 있지는 않은가?
- 설정 주변 값에서도 성과가 비슷한가? 고른 기간 밖에서도 유지되는가?
- 상위 몇 개 거래가 전체 손익의 대부분을 차지하지는 않는가?
- 생존 편향이 있는가? 있다면 어느 방향으로 결과를 왜곡하는지 글에 적었는가?
정리
여섯 가지 오류 중 세 가지(분할, 종가 누락, 고정 비용)는 결과가 이미 그럴듯하게 나온 뒤에 발견했다. 눈으로 보면 문제없어 보이는 오류가 더 위험하다. 그래서 나는 결과가 좋을수록, 글을 쓰기 전에 위 체크리스트를 한 번 더 돈다. 이 글에서 링크한 글들은 모두 이 과정을 거쳐 수정되거나 작성됐다. 관련 글: 가치 지표 비교, 다른 3배 ETF 백테스트
이어지는 글: 이 글을 쓴 뒤 백테스트 전체를 다시 점검하다가 일곱 번째 오류(거래량이 액면분할 기준으로 보정되지 않은 문제)를 찾았다. 그 과정과 수정 결과는 여기에 정리했다.
※ 본 글은 개인 학습 기록이며 특정 종목의 매수·매도를 권유하지 않습니다. 코드는 설명을 위한 예시이며, 실제 적용 시 본인의 데이터 구조에 맞게 검증해야 합니다.
'투자 도구·자동화' 카테고리의 다른 글
| 백테스트용 무료 시장 데이터, 소스별로 무엇을 조심해야 할까: FinanceDataReader·pykrx·DART·SEC 함정 지도 (0) | 2026.10.06 |
|---|---|
| 연금저축·IRP 세액공제를 수익률로 환산하면 몇 %p일까? 납입 기간별 계산표 (0) | 2026.10.05 |
| 백테스트를 전부 다시 점검했다: 일곱 번째 데이터 오류와 정정 기록 (0) | 2026.10.02 |
| DART OpenAPI로 상장사 재무제표(분기 포함)를 파이썬으로 받기: 차단당하지 않는 방법까지 (0) | 2026.10.01 |