하루에 백테스트 버그를 다섯 번 잡았다 — 전부 "좋은 숫자"가 먼저 나왔다
look-ahead, 상한가 착시, 분위 방향 반전, 집중도 미확인, 사후 종목선택. 다섯 개 모두 결과가 좋아 보이는 쪽으로 틀려 있었다.
하루에 백테스트 버그를 다섯 번 잡았다. 전부 "좋은 숫자"가 먼저 나왔다.
기록해두는 이유는 하나다. 다시 안 밟으려고.
1. look-ahead — 승률 76%가 통째로 무효
신호봉 종가에 진입해놓고, 같은 봉의 고가·저가로 익절·손절을 판정했다. 그 봉의 고가가 얼마인지는 종가 시점에 알 수 없는데도.
승률 76%, 누적 +7.0%가 나왔고 전부 가짜였다.
진입가가 종가면 청산 판정은 반드시 다음 봉부터.
2. 상한가 착시 — 체결할 수 없는 수익
"+15% 급등주를 종가에 사서 다음날 시가에 팔면 +1.88%"라는 결과가 나왔다. 좋아 보였다.
해부해보니 수익의 98%가 상한가 종목에서 나왔다. 그런데 상한가는 매수 호가만 쌓이고 매도 물량이 없다. 종가에 살 수가 없다.
상한가를 빼니 +1.88%가 +0.03%가 됐다.
체결 가능성을 확인하고 수치를 읽을 것.
3. 분위 방향 반전 — 저PBR 포트폴리오에 고PBR이 들어있었다
qcut(x.rank(ascending=False), ...) + topq=4.
내림차순으로 순위를 매긴 뒤 4번 분위를 담으면, 값이 가장 작은 종목이 담긴다. "저PBR"이라고 이름 붙인 포트폴리오에 고PBR 성장주가 들어갔고, 전 팩터가 거꾸로 측정됐다.
발견한 경로가 중요하다. 고ROE가 최하위로 나온 게 이상했다. 퀄리티 팩터가 전 세계에서 역방향인 일은 없다. 결과가 상식을 위배한 것이 버그 신호였다.
rank(ascending=True)로 고정하고 "높을수록 좋음" 플래그를 따로 둘 것. 이중부정을 만들지 말 것.
4. 집중도 미확인 — 40종목인 줄 알았는데 3.8종목
"시총가중 퀄리티 포트폴리오 40종목이 지수와 동률"이라는 결과가 나왔다. 처음으로 지수를 안 진 결과라 반가웠다.
해부해보니,
- 유효종목수(1/Σw²) = 3.8 (명목 40종목)
- 최대비중 중앙값 49.2%, 삼성전자가 126개월 중 71개월 1위
- 최대비중 1종목을 빼자 +0.00%가 −1.36%(t=−3.27)로 붕괴
팩터 성과가 아니라 삼성전자 수익률이었다.
시총가중 백테스트는 반드시 유효종목수를 확인할 것.
5. 사후 종목선택 — 누적 +23,778%
ETF 60종목을 전 구간 t값으로 줄 세워 1등을 고르고 "오버나이트 전략 누적 +23,778%"라고 적었다. 2015년에 어느 ETF가 10년 뒤 1등일지 알 수 없는데도.
직전 6개월 성적으로 고르게 바꾸자 −1.97%(t=−4.43)가 나왔다.
선정 규칙은 그 시점에 알 수 있는 정보만 쓸 것.
공통점
1번과 2번은 내가 먼저 "좋은 결과"라고 보고했다가 교차검증에서 무너진 것이다.
다섯 개 모두 결과가 좋아 보이는 쪽으로 틀려 있었다. 우연이 아니라고 본다. 코드를 짜는 사람이 결과를 기대하고 있으면, 검증이 느슨해지는 자리가 하필 유리한 방향이다.
그래서 규칙을 하나 더 넣었다. 좋은 결과일수록 먼저 의심한다. 나쁜 결과는 굳이 의심할 필요가 없다 — 어차피 버릴 거니까.