AI

Codex가 짚은 미래 정보 누수로 백테스트가 뒤집혔다

장중 점수를 장 시작 시점에 이미 안 것처럼 계산했다 — 다시 계산하자 코스피 급등 효과는 사라지고 나스닥 +1% 효과는 더 뚜렷해졌다

한 줄로

자동매매 봇의 매수 신호 후보를 백테스트로 비교했다. Codex 교차 검토에서 ‘그날 장중에 나온 점수를 장 시작 시점에 이미 안 것처럼 계산했다’는 지적이 나왔다. 실제로 살 수 있는 시점을 기준으로 다시 계산하자 코스피 급등 다음 날 효과는 사라졌고, 나스닥 +1% 다음 날 효과는 오히려 더 뚜렷해졌다.

미래 정보 누수란

백테스트(과거 데이터로 전략을 돌려 보는 검증)에서 그 시점에는 알 수 없었던 정보를 판단에 쓰는 실수를 미래 정보 누수(look-ahead bias)라고 한다. 코드에는 오류가 없고 숫자도 그럴듯하게 나온다. 그래서 결과만 봐서는 알아채기 어렵다.

흔한 형태는 두 가지다.

  • 시점 누수: 장이 끝나야 확정되는 값을 장 시작 시점의 판단에 쓴다.
  • 공개 지연 누락: 데이터가 실제로 공개되기까지 걸리는 시간을 무시하고, 해당 날짜에 바로 알았다고 가정한다.

이번에 둘 다 다뤘다. 첫 번째는 Codex가 잡아 줬고, 두 번째는 수급 데이터를 분석할 때 직접 반영했다.

배경: 코드는 멀쩡하고 남은 건 수익성이다

장중에 실제 동작부터 확인했다. 14:02 기준으로 매수 4건이 들어갔고 모두 점수 0.25 이상인 A등급이었다. 4건 모두 executed로 기록됐다. 전략이 막은 매수는 추세 61건, 거래량 43건, 시간대 12건이었고, 막을 때의 값이 detail에 남는다. 3분봉 2,600개도 KST로 정상 저장됐다.

그래서 코드는 손대지 않기로 했다. 남은 과제는 전략이 돈을 버느냐다.

가상 포트폴리오 재생부터 실제 규칙에 맞췄다 (#110)

수익성을 보려고 가상 포트폴리오를 재생하는 스크립트(scripts/shadow_replay.py)를 돌렸다. 그런데 Codex와 opencode가 둘 다 ‘재생 로직이 실제 매매 규칙과 다르다’고 지적했다. 그래서 아래 네 가지를 고쳤다.

  • 전략 매도는 09:18~15:18 안에서만 판정한다.
  • 3분봉이 6분 넘게 비면 손절 연속 횟수를 처음부터 다시 센다.
  • 아직 팔지 않은 포지션이 30%를 넘으면 ‘판단 보류’로 표시한다.
  • 매수 금지 종목, 유니버스 밖 종목, 보유 중인 종목, 주문 직후 5분은 후보에서 뺀다.

고친 뒤에 나온 결론은 이렇다. 손절선을 -15/-10/-7%로 바꿔도 결과 차이는 ±0.03% 안이다. 전략 매도 승률은 16~29%이고, 비용을 빼면 평균 -0.9~-1.5%다. 손볼 곳은 손절이 아니라 매수 신호다.

매수 신호 후보 실험 (#111, #113, #114)

점수 0.25 이상, 비용 차감 기준으로 나온 첫 결과다.

후보 결과
현행 -1.51%
5팩터 제외 -0.98%
과열 날 보류 +0.72% (5건뿐)
둘 다 적용 -1.15%

확실히 나은 후보는 없었다. ‘과열 날 보류’는 양수지만 5건이라 근거로 삼기 어렵다.

미국 장 가설도 일봉 61거래일로 확인했다. 나스닥이 1% 넘게 오른 다음 날, 신호가 난 종목은 시장보다 -0.69% 약했다(상관 -0.28, p=0.03). 다만 절대 손실은 한국 시장 자체가 빠진 탓이었다(p=0.66). 시장 대비로는 차이가 있었지만, 손실 자체를 이 가설로 설명할 수는 없었다.

공개 지연을 반영해도 수급 데이터는 예측력이 없었다

2024-01부터 27종목을 백필(과거 데이터를 한꺼번에 채워 넣는 작업)했다. 공매도 18,144행, 대차 18,117행, 신용 16,983행, 시장 4,849행이다.

공매도·대차·신용잔고는 예측력이 없었다(IC -0.02~+0.02). IC는 신호와 이후 수익률의 상관을 말한다. 공매도는 2023-11~2025-03이 금지 기간이라 2025-04 이후 데이터만 썼다. 여기서 공개 지연도 따졌다. 신용은 2거래일, 대차는 1일 늦게 공개되므로 그만큼 밀어서 다시 계산했다. 결론은 같았다.

VKOSPI는 ‘급락하면서 공포가 커진 뒤 반등’하는 경향을 보였다(p=0.16). 하지만 2026년은 VKOSPI가 내내 30 이상이었다. 이 값을 기준으로 날을 나누는 규칙은 작동할 수가 없다.

Codex 지적: 장중 점수를 아침에 이미 알고 있었다

Codex·opencode 교차 검토에서 Codex가 짚은 문제는 이것이다.

그날 장중에 나온 점수를 장 시작 시점에 이미 안 것처럼 계산했다.

앞에서 수급 데이터에는 공개 지연을 반영했지만, 정작 신호 점수 쪽에 같은 종류의 누수가 있었다. 장중에야 나오는 값을 기준으로 ‘그날 사면 어땠을까’를 계산했으니, 실제로는 할 수 없는 매수를 성과에 넣은 셈이다.

실제로 살 수 있는 시점을 기준으로 다시 계산했다.

후보 다시 계산한 결과 조치
코스피 급등 다음 날 효과가 사라졌다 가상 포트폴리오에서 뺐다
나스닥 +1% 다음 날 더 뚜렷해졌다 (-2.36% vs -0.07%) 해당일이 12일뿐이라 추적만 한다

같은 누수를 걷어냈는데 한 후보는 사라지고 다른 후보는 더 선명해졌다. 처음 결과만 보고 규칙을 바꿨다면 엉뚱한 후보를 채택했을 것이다.

함께 고친 것 (#115~#118)

  • 수집 실패를 놓치지 않게 했다. 빈 응답이나 누락도 실패로 보고 텔레그램 알림을 보낸다. 실패를 조용히 넘기면 그날 데이터가 빈 채로 분석에 섞이기 때문이다.
  • 효과 없는 후보 3개를 뺐다. 과열 제외, AI앙상블만, 코스피다.
  • 청산 30건 미만은 ‘판단 보류’로 표시한다. 표본이 적을 때 나온 숫자를 결론처럼 읽지 않기 위해서다.

바뀐 것은 매매 엔진 밖의 스크립트뿐이라 재배포는 하지 않았다. scripts/shadow_replay.py도 맥미니 cron이 직접 돌리므로 재배포가 필요 없다.

실제 매매 규칙은 언제 바꾸나

남은 후보는 ‘나스닥 +1% 다음 날 매수 보류’ 하나다. 지금은 가상 포트폴리오에서 추적만 한다.

실제 매매 규칙에 넣는 기준은 이렇게 정했다. 해당일 표본이 30일 이상 쌓이고, 그때도 차이가 유지될 때. 지금 12일로 나온 -2.36% vs -0.07%가 30일에도 남아 있는지 보고 결정한다.

정리

  • 결과만 보고는 미래 정보 누수를 알아채기 어렵다. 숫자는 그럴듯하고 코드도 돈다.
  • 수급 데이터의 공개 지연은 챙겼는데, 신호 점수의 시점 문제는 놓쳤다. 같은 종류의 실수도 데이터마다 따로 점검해야 한다.
  • 다른 모델에 교차 검토를 맡기니 내가 짠 계산의 전제를 의심해 줬다. 재생 로직이 실제 규칙과 다르다는 지적도, 시점 누수 지적도 그렇게 나왔다.
  • 누수를 걷어내면 결론이 바뀔 수 있다. 이번에는 코스피 급등 다음 날 효과가 사라졌고, 나스닥 +1% 다음 날 효과는 더 뚜렷해졌다.