Algorithmic Trading: Debug Your Backtest Before Upgrading Your Model
알고리즘 트레이딩: 모델을 업그레이드하기 전에 백테스트부터 디버깅하세요
백테스트 성능이 좋아졌다면 모델의 복잡도보다 먼저 데이터 누수, 과적합, 거래 비용과 주문 실행 조건을 점검해야 합니다. 글은 Python과 합성 데이터로 정보 시점 검증, 공정한 모델 비교, 비용 반영, 주문 상태 관리까지 재현 가능한 점검 절차를 설명합니다.
- 주제
AI 요약
알고리즘 트레이딩에서 백테스트 결과가 좋아 보인다는 사실만으로 모델을 신뢰하기는 어렵습니다. 예를 들어 기업이 오후 4시 5분에 실적을 발표했는데, 백테스트가 오후 4시 매수 결정을 내리면서 해당 실적을 사용한다면 모델이 미래를 예측한 것이 아닙니다. 날짜 열 하나로 두 데이터셋을 조인한 파이프라인이 아직 공개되지 않은 정보를 과거 결정에 전달한 결과입니다. 이 글은 모델을 더 크게 바꾸기 전에 백테스트를 만든 시스템 자체를 조사해야 한다고 설명합니다. 예제는 Python 표준 라이브러리와 합성 데이터만 사용하며, 브로커 계정이나 시장 데이터셋, API 키 없이 각 코드를 실행할 수 있게 구성합니다.
■ 정보가 당시 이용 가능했는지 확인합니다
백테스트는 과거 데이터에서 전략이 어떻게 동작했는지 모의 실행합니다. 이때 전략이 당시 알 수 없던 정보를 사용하면 look-ahead bias가 생깁니다. 모든 행에 타임스탬프가 있어도 충분하지 않습니다. 타임스탬프가 어떤 사건을 가리키는지 구분해야 합니다. 실적 문서라면 `published_at`은 정보가 공개된 시점이고, `received_at`은 시스템이 받은 시점이며, `processed_at`은 전략이 사용할 수 있도록 처리가 끝난 시점입니다.
예시에서 결정 시각은 2025년 2월 3일 오후 4시이며 UTC 오프셋을 포함합니다. 실적 공개 시각은 오후 4시 5분, 시스템 수신 시각은 오후 4시 5분 2초, 처리 완료 시각은 오후 4시 5분 4초입니다. 날짜만 비교하면 두 날짜가 같으므로 문서가 조인됩니다. 하지만 실제 이용 가능 시각을 세 타임스탬프의 최댓값으로 계산하면 오후 4시 5분 4초입니다. 따라서 오후 4시 결정에는 사용할 수 없습니다. 글의 Python 검증도 `joined_by_date`가 참이고 `eligible_at_decision`이 거짓임을 확인합니다. 날짜 조인은 문법적으로 유효하지만 전략이 가진 정보의 상태를 표현하지 못합니다.
실제 파이프라인에서는 문서 버전도 보존해야 합니다. 기업이 나중에 수치를 수정했을 때 현재의 수정값이 과거 결정에 조용히 들어가면 안 됩니다. 의사결정 시각까지 이용 가능했던 최신 버전을 조회해야 합니다. 다만 정보가 의사결정에 적합하다는 확인만으로 주문 체결이 보장되지는 않습니다. 처리 지연, 주문 제출 시각, 거래소 운영 시간, 당시 이용 가능한 가격까지 시뮬레이션에 넣어야 합니다.
■ LLM 실험에서도 데이터 누수를 따집니다
2021년 헤드라인만 LLM 프롬프트에 넣더라도 모델 학습 데이터에 해당 기업의 2022년 결과가 이미 포함되어 있을 수 있습니다. 검색 문서를 제한하는 것만으로 모델 내부에 인코딩된 정보를 제거하지는 못합니다. Glasserman과 Lin의 2023년 연구 프리프린트는 과거 헤드라인 평가를 조사했습니다. 연구 결과는 단순하지 않았습니다. 기업에 관한 모델의 지식이 감성 측정을 방해할 수 있었고, 실험의 학습 기간 안에서는 익명화한 헤드라인이 더 나은 결과를 보였습니다. 그렇다고 모든 과거 LLM 결과가 같은 정도로 부풀려졌다고 가정할 근거가 되지는 않습니다.
Kong과 공동 저자들이 2026년 2월 발표한 포지션 페이퍼는 2023년부터 2025년까지 금융 LLM 논문 164편을 검토했습니다. 연구에서 추적한 다섯 가지 편향 범주 가운데 어느 것도 28%를 넘는 연구에서 다뤄지지 않았습니다. 이 결과는 편향 보고가 충분했는지를 말할 뿐이며, 빠진 편향이 모든 실험에 영향을 줬다는 뜻은 아닙니다. 새 LLM 실험을 설계할 때는 모델 버전, 프롬프트, 검색 정책, 의사결정 규칙을 고정하고 결과가 나오기 전에 예측을 전향적으로 기록하는 방법을 제안합니다. 다만 이 방식으로도 거래 비용, 선택 편향, 시장 조건 변화까지 자동으로 해결되지는 않습니다.
■ 복잡한 모델은 같은 조건에서 비교합니다
복잡도가 높아지면 단순 모델이 놓친 관계를 표현할 수 있습니다. 예를 들어 모멘텀 신호가 거래량이 적은 자산에서 다르게 작동한다면, 모멘텀과 유동성 항을 따로 둔 선형 모델에는 상호작용 항을 별도로 추가해야 합니다. 트리 모델이나 신경망은 이런 상호작용을 직접 지정하지 않고 표현합니다. Gu, Kelly, Xiu의 `Empirical Asset Pricing via Machine Learning` 연구도 주식 위험 프리미엄 예측에서 트리와 신경망이 강한 성능을 보였으며, 그 배경으로 입력값 사이의 비선형 상호작용을 제시했습니다.
하지만 이 결과가 특정 전략에서 더 큰 모델이 비용 차감 후 성능을 개선한다는 증거는 아닙니다. 먼저 동작을 살펴보기 쉬운 기준 모델을 둬야 합니다. 복잡한 모델에는 동일한 정보, 평가 날짜, 포트폴리오 제약, 비용 가정을 적용해야 합니다. 구조화된 신호를 평가한다면 고정 규칙이나 규제 선형 모델을 트리 모델과 비교합니다. 문서 해석의 가치를 평가한다면 규칙 또는 텍스트 분류기와 LLM을 비교하고 정보 추출 정확도도 따로 측정합니다. 예측이 실제 거래를 개선하는지 보려면 두 모델에 같은 포지션 제한과 체결 가정을 적용합니다. 예측 지표가 좋아져도 포지션을 더 자주 바꾸거나 거래 비용이 큰 자산을 선택하면 실제 거래 결과는 나빠질 수 있습니다.
■ 탐색 기록을 남기고 백테스트 과적합을 점검합니다
룩백 기간 20개, 진입 임계값 10개, 보유 기간 5개를 시험하면 모델 구조를 바꾸기 전부터 1,000개 설정을 탐색한 셈입니다. 가장 좋은 백테스트가 과거의 우연한 잡음에 맞은 설정일 수 있습니다. Bailey, Borwein, López de Prado, Zhu의 `The Probability of Backtest Overfitting`은 표본 안에서 좋은 결과를 고른 뒤 표본 밖에서 성능이 떨어지는 선택 문제를 다룹니다.
실험 이력은 결과의 일부입니다. 최종 승자뿐 아니라 실패한 설정도 보관해야 합니다. 데이터셋 버전, 피처 정의, 코드 리비전, 모델 설정, 평가 기간, 승자를 고른 지표를 기록해야 합니다. 마지막 노트북만 남기면 얼마나 많은 탐색을 거쳤는지 재구성하기 어렵습니다. 시간 순서에 따라 학습 데이터를 먼저 사용하고, 전처리는 학습 구간에서만 적합하며, 설정 선택은 검증 구간에서 수행하고, 더 뒤의 기간을 최종 평가용으로 남겨야 합니다. 학습 라벨이 다음 5일 수익률을 뜻한다면 라벨 구간이 평가 기간까지 뻗는 학습 사례도 제외해야 합니다.
최종 평가 기간 하나만으로는 근거가 제한됩니다. 더 이른 여러 시간 순서 검증 구간에서도 결과가 안정적인지 확인해야 합니다. 최종 결과를 사용해 전략을 다시 설계하는 순간 해당 기간은 개발 데이터로 바뀝니다.
■ 수익 예측을 체결 비용과 함께 계산합니다
가상의 전략이 보유 기간 동안 평균 12 basis points의 유리한 움직임을 예측한다고 가정합니다. 1 basis point는 0.01%포인트이므로 거래 명목금액 10,000달러에서 기대 총수익은 12달러입니다. 왕복 거래 비용을 스프레드 체결 4 basis points, 수수료 2 basis points, 추가 슬리피지와 시장 충격 5 basis points로 두면 비용은 모두 11 basis points입니다. 기대 순수익은 1 basis point, 즉 1달러로 줄어듭니다. 여기서 추가 슬리피지는 이미 반영한 스프레드와 겹치지 않게 계산합니다.
Python 코드에서 추가 비용이 0 basis points이면 기대 순수익은 1달러입니다. 추가 비용을 2 basis points만 더하면 결과는 마이너스 1달러로 뒤집힙니다. 따라서 방향성 정확도만으로 수익성을 판단할 수 없습니다. 손익 규모, 포지션 크기, 체결 비용이 빠져 있기 때문입니다. 총수익과 순수익, turnover, 이전 포트폴리오 고점에서의 하락폭인 drawdown을 함께 기록해야 합니다. 지정가 가격을 과거 가격이 한 번 건드렸다는 사실만으로 주문이 전량 체결됐다고 가정해서도 안 됩니다. 주문 대기열에서의 위치와 실제 체결 방식을 시뮬레이션에 명시해야 합니다.
■ 주문 상태와 장애 시나리오를 테스트합니다
주문 요청이 시간 초과됐을 때 브로커가 거부했는지, 주문은 접수했지만 응답이 사라졌는지 알 수 없다면 무작정 재시도해서는 안 됩니다. 의도한 주문 하나가 두 번 제출될 수 있습니다. 브로커가 지원한다면 안정적인 client order identifier를 사용하고, 제출 상태를 저장하며, 불확실한 요청을 재시도하기 전에 미해결 주문과 포지션을 대조해야 합니다. 다만 식별자 하나만으로 중복 제거가 보장되지는 않으므로 브로커의 실제 동작을 테스트해야 합니다.
모델은 목표 포지션만 제안하고, 별도의 risk layer가 이를 검증하도록 분리하는 방식도 제시합니다. risk layer는 포지션 한도와 주문 크기 제한을 적용하고, 입력 데이터가 최신인지 검사하며, 주문 상태를 대조하지 못하면 새 주문을 중단해야 합니다. 미체결 주문도 잠재적인 익스포저에 포함해야 합니다.
Knight Capital의 2012년 8월 1일 사고는 이런 실행 장치의 영향을 보여주는 문서화된 사례입니다. SEC 기록에 따르면 잘못된 배포가 결함 있는 주문 라우터 기능을 활성화했습니다. 라우터는 45분 동안 고객 주문 212건을 처리하려 하면서 400만 건이 넘는 주문을 보냈고, 회사는 결국 4억 6,000만 달러가 넘는 손실을 입었습니다. SEC는 배포와 위험 통제 실패를 지적했습니다.
실패 시나리오도 상태 전이로 정의하고 테스트해야 합니다. 문서가 늦게 도착하거나 결정 뒤에 수정되는 경우, 시장 데이터 피드가 멈췄는데 전략이 계속 실행되는 경우, 주문은 접수됐지만 확인 응답이 오지 않는 경우, 취소 요청 전에 일부만 체결되는 경우, 프로세스가 재시작됐는데 브로커에 미체결 주문이 남아 있는 경우를 다룹니다. 각 상황에서 시스템이 어떤 상태로 이동해야 하는지 정한 뒤 검증합니다. 페이퍼 트레이딩은 연동 오류를 드러낼 수 있지만 실제 체결과 시장 충격을 모두 재현하지는 못합니다.
모델 업그레이드 뒤 백테스트가 좋아졌다면 개선 원인을 이용 가능한 정보, 공정한 평가, 실행 가능한 주문이라는 세 경계에서 추적해야 합니다. 글은 이 과정을 알고리즘 트레이딩에 개발자식 디버깅을 적용하는 방법으로 제시합니다.
원문: dev.to / 번역·요약: Trawling