미래의 치팅을 구조로 막기: 덮어쓰이는 메모를 학습에 사용하기 전, AI에게 '대조'를 요청한 경험
요약
주식 AI 모델의 백테스트 신뢰성을 높이기 위해, 개발자는 Claude Code를 활용하여 '대조(Comparison)' 과정을 거쳤습니다. 기존 기술적 지표 학습에 재무/뉴스 정보를 결합하는 과정에서, 메모가 덮어쓰여져 과거 시점의 정보가 왜곡될 위험을 발견했습니다. 이를 해결하기 위해 'Point-in-Time (PIT) 대장'을 구축하여, 각 날짜별로 알 수 있었던 값만 기록하도록 시스템을 개선했습니다.
핵심 포인트
- 백테스트 신뢰성 확보를 위해 데이터의 시간적 누수 방지가 중요합니다.
- 단순히 기능을 요청하기보다 '현재 구현과의 대조'를 통해 잠재적 오류를 발견해야 합니다.
- 정보가 덮어쓰여지는 메모 대신, 날짜별로 값을 쌓아 올리는 PIT(Point-in-Time) 대장을 구축했습니다.
- 학습 시에는 반드시 학습 행보다 이전의 데이터만 연결하도록 로직을 설계해야 합니다.
백테스트(Backtest) 성적이 너무 좋을 때, 가장 먼저 의심하는 것은 '과거 데이터에 미래 정보가 섞이지 않았는지'입니다. 게다가 이런 종류의 실수는 에러로 나타나지 않습니다. 코드는 정상적으로 작동하고, 검증 성적은 오히려 좋아집니다.
저는 일본 주식 AI 종목 스크리닝 및 지속 학습 단말기인 "ALPHA FORGE (알파포지)"를 Claude Code와 함께 개인 개발하고 있습니다. 이번 글에서는 아직 발생하지 않은 사고를 설계 단계에서 발견하여 막을 수 있었던 경험에 대해 이야기합니다. 계기는 '뉴스나 재무 정보도 학습에 넣고 싶다'는 요청이었습니다. 이때 저는 AI에게 단순히 '만들어 달라'가 아니라, '기존 구현과의 대조(突き合わせ)'를 부탁했습니다.
※본 기사는 note 연재 [ALPHA FORGE 개발 비화 #10]을 개발자들을 위해 재구성한 것입니다.
환경
| 항목 | 내용 |
|---|---|
| 개발 | Claude Code (CLI) |
| ... |
'정확도를 높이고 싶다'기 전에, 대조를 요청하다
ALPHA FORGE의 기계 학습 모델은 매일의 예측과 그 이후의 실제 움직임을 재료로 학습합니다. 다만, 이때까지 학습에 사용된 것은 주가에서 계산한 기술적 지표(テクニカル指標)뿐이었습니다. 종목별 재무 수치나 뉴스 내용의 좋고 나쁨은 손안의 메모(Obsidian의 종목 노트)에 정리되어 있었지만, 학습에는 포함되지 않았습니다.
9월 18일 아침, 저는 Claude Code에게 다음과 같이 요청했습니다 (요약).
이 구현에 더해, 종목의 뉴스나 분기보고서(四季報) 정보 등을 결합하여 모델 학습의 정확도를 높이고 싶다. 기존 구현과의 대조를 수행하고, 문서를 업데이트하며, 구현 계획을 세워 달라.
'만들어 달라'고 말하는 대신, 먼저 '현재 어떻게 되어 있는지'를 조사해 달라고 요청한 방식입니다.
발견된 구멍: 덮어쓰이는 메모

덮어쓰여 업데이트되는 메모를 과거 날짜의 행에 연결하면, 그 시점에서는 아직 알지 못했던 값이 섞이게 된다
대조 결과, 세 가지 사실을 알게 되었습니다.
- 학습에 사용되는 특징량(feature)은 기술적 지표뿐이었다.
- 예측을 기록할 때 판단의 근거가 된 원시 수치(生の値)를 남기지 않았다.
- 종목 노트는 새로운 결산이 나올 때마다 덮어쓰여 업데이트된다. 과거 특정 날짜에 어떤 값이 적혀 있었는지 복원할 수 없다.
문제는 세 번째입니다. 학습에서는 '3개월 전 이 날, 이 종목은 이랬다. 그 후 주가는 이렇게 움직였다'라는 행을 많이 만듭니다. 여기에 종목 노트의 재무 수치를 연결하면, 3개월 전 행에 '오늘 시점의 수치'가 들어갈 수 있습니다. 그 수치에는 3개월 전에는 아직 발표되지 않은 결산이 반영되어 있을 수도 있습니다.
해결책: 대장과 틀릴 수 없는 결합

날짜별로 값을 쌓아 올리는 대장을 만들고, 학습하는 날보다 이전의 기록만 연결하도록 했다
대처는 2단계로 이루어집니다.
- 덮어쓰여지는 메모와 별도로, '그날 알 수 있었던 값'을 날짜별로 쌓아 올리는 대장(台帳)을 만듭니다. 재무 수치와 뉴스 내용의 좋고 나쁨을 매일 그날의 날짜로 기록하고, 덮어쓰지 않습니다. 이를 '포인트 인 타임(Point-in-Time, PIT) 대장'이라고 부릅니다.
- 대장의 값을 학습 행에 연결할 때는
pandas.merge_asof의 `direction=
await _upsert_fund(snapshot_date="2026-01-10", code="7203", per_forecast=99.0) # 미래 (학습 행 이후)
panel = _panel([("2026-01-05", "7203")])
out = await pfs.attach_pit_fundamental_features(panel, tolerance_bdays=_TOLERANCE)
...
학습 행 (1월 5일) 이후의 날짜에만 기록이 있으면 결합되지 않고 누락된 상태로 남게 됩니다. '조심해야 한다'가 아니라, '틀릴 수 없도록' 만든 것입니다.
추가한 데이터가 효과적인지 측정하기
새로운 정보원을 추가했을 때, 그것이 정말 정확도에 도움이 되는지를 측정하는 메커니즘도 넣었습니다. 정보원 그룹을 하나씩 제외하고 재학습시켜서, 홀드아웃(holdout) 성적이 얼마나 변하는지 비교하는 소스 제거 평가(source ablation evaluation)를 수행합니다 (ablation_service의 run_source_ablation). 추가한 데이터가 효과적이지 않다면, 그렇지 않다는 것을 알 수 있도록 했습니다.
사용 시작 조건: 커버리지 게이트
원장(台帳)은 만들자마자 유용하지 않습니다. 값이 쌓일 때까지 기다려야 합니다. 그래서 '얼마나 쌓여야 학습에 사용하기 시작할지'의 기준을 정했습니다.
| 항목 | AI 권장 | 채택한 값 |
|---|---|---|
| 필요한 영업 일수 | 60영업일 | 120영업일 |
| 빠져있지 않은 비율 | 50% 이상 | 70% 이상 |
처음에 학습에 투입할 데이터는 안정적인 것이길 바랐기 때문에, 사람의 판단으로 엄격하게 설정했습니다. 설정값은 PIT_MIN_COVERAGE_DAYS와 PIT_MIN_COVERAGE_RATIO로 가져가고, 심지어 학습에 투입하는 것 자체를 PIT_FEATURES_ENABLED (기본값 OFF)의 옵트인(opt-in)으로 했습니다.
# 학습 패널에 PIT 열을 투입하는 것 자체의 opt-in (기본값 OFF, 원장이 쌓일 때까지 명시적으로 활성화하지 않음).
pit_features_enabled: bool = Field(default=False, validation_alias="PIT_FEATURES_ENABLED")
pit_min_coverage_days: int = Field(default=120, validation_alias="PIT_MIN_COVERAGE_DAYS", ge=1)
...
뉴스 내용의 좋고 나쁨을 모으는 범위 역시, AI의 권장은 '후보가 될 만한 종목과 보유 종목으로 한정'이었지만, 도쿄 증권거래소(東証) 전체 종목으로 넓혔습니다 (PIT_SENTIMENT_SCOPE의 기본값은 universe). 가져오는 곳에 제한이 걸릴까 봐 걱정했지만, 학습에 사용할 수 있는 종목이 많은 쪽을 선택했습니다.
이 판단은 나중에 사건으로 이어졌습니다 (연재 7화). 가져오는 곳으로부터 일시적으로 접근이 차단되어 '가져올 수 없었다'고 기록되었지만, '뉴스 건수는 0건'으로 원장에 쓰여 있었습니다. 학습에는 아직 사용하고 있지 않았기 때문에 실질적인 피해는 없었고, 원장의 일부를 지우고 다시 가져왔습니다. 원장은 '미래를 섞지 않는 것'뿐만 아니라, '잘못된 값을 섞지 않는 것'도 중요하다는 것을 나중에 배웠습니다.
지금도, 원장의 값은 학습에 넣고 있지 않습니다. 120 영업일치가 쌓이는 것을 기다리는 상태입니다.
배움: '만들기' 전에 '대조하기'
이번에 성공한 것은 첫 번째 요청에 '구현과의 대조를 수행하고, 문서를 업데이트하며, 구현 계획을 생각하라'고 넣었기 때문이었습니다. 만약 갑자기 '뉴스와 재무도 학습에 넣어라'라고 부탁했다면, Claude Code는 순순히 종목 노트의 현재 값을 학습 행에 연결하는 코드를 작성했을지도 모릅니다. 코드는 작동했고, 검증 성적은 올라갔지만, 아무도 눈치채지 못했을 것입니다.
새로운 데이터를 추가할 때 요청하는 방법으로는 다음 3가지를 하나의 요청에 넣었습니다.
- 구현과의 대조 (현재 어떻게 되어 있는지 먼저 조사하게 하기)
- 문서 업데이트 (발견한 것을 설계서에 남기게 하기)
- 구현 계획 (만들기 전에, 만드는 방법에 대한 초안을 내게 하기)
요약
- 덮어쓰여지는 정보를 과거 날짜의 학습 행에 그대로 연결하면 미래 정보가 섞인다. 오류는 나지 않고, 검증 성적은 오히려 좋아 보인다.
- '그날 알고 있던 값'을 날짜와 함께 쌓아두는 원장을 별도로 가진다.
- 결합은
merge_asof의direction="backward"
미래의 기록이 연결되지 않음을 테스트로 고정하는 것 – 사용을 시작할 조건(덮어쓰기 게이트)과 opt-in을 결정하고, 충분히 쌓일 때까지 학습에 넣지 않습니다. 추가된 데이터가 효과적인지는 소스 제거(source ablation)를 통해 측정합니다.
- 결합 메커니즘은 '값이 올바르다'는 것까지 보장하지는 않습니다. 취득 실패를 0건으로 기록해버리는 사고는 별도로 방지할 필요가 있습니다.
뉴스 및 금융 정보를 추가하겠다는 방침, 덮어쓰기 게이트를 엄격하게 하는 것, 수집 범위를 전 종목으로 넓히는 것을 결정한 것은 인간입니다. 구현과 대조하며 덮어쓰기로 인한 허점을 발견하고, 원장(ledger)·결합·평가를 설계 및 구현한 것은 Claude Code였습니다. 사용을 시작하는 시기는 쌓인 양을 보고 사람이 결정합니다.
다음 번은, 사기보(四季報)의 자동 취득을 '사퇴'했던 이야기입니다. 기술적으로 가능한 것과 해도 되는 것은 별개였다는 이야기가 될 것입니다.
☕ note.com에서 개발 비화와 매일 아침 검증 로그를 연재 중
note.com에서는 ALPHA FORGE의 개발 비화와, 매일 아침 알고리즘 검증 로그를 공개하고 있습니다.
👉 원문 기사 (note 연재 제10화):
【ALPHA FORGE 개발 비화 #10】 '미래의 부정행위'를 구조로 막기 ― 포인트 인 타임 원장(Point-in-Time Ledger)
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn ML의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기