서론
저는 일본 주식용 AI 퀀트 시스템인 "QuantG"를 개인적으로 만들고 있습니다. QuantG에서는 LLM을 이용해 인자(Factor) (어떤 종목을 살지 결정하는 지표)를 생성하게 하고, 매일 그 인자를 사용해 다음 날 구매할 종목을 결정합니다.
이 시스템이 실제로 유용한지 확인하기 위해 **히스토리컬 리플레이(Historical Replay)**라는 기능이 있습니다. 이는 "만약 이 시스템을 반년 전부터 가동했다면, 지금은 어떤 상태였을까"를 과거의 하루하루를 추적하여 재현하는 것입니다.
리플레이에서 가장 중요하게 생각하는 원칙은 그날 알 수 없었던 정보는 일절 사용하지 않는 것입니다. 주가뿐만 아니라 "그날 어떤 인자가 사용되었는지"까지 포함하여 당시의 상태를 재현합니다. 본 글에서는 이러한 사고방식을 최대한 전문 용어를 사용하지 않고 소개하겠습니다.
인자 풀은 '팀 명단'과 같습니다
QuantG에는 "밸류에이션", "거래량 이상" 등 연구 주제(인자 정의)가 20여 개가 있으며, 각 주제별로 정규 인자는 최대 1개씩 존재합니다. 이는 스포츠 팀에서 포지션별로 주전 선수가 한 명 있는 것과 같습니다.
이 정규 인자는 고정되어 있지 않습니다. 영업일 기준 3일마다 주제별로 '선발' 과정이 있습니다.
- LLM이 새로운 후보 인자를 작성합니다.
- 시스템이 최근 과거 데이터를 이용해 후보를 점수화합니다.
- 기준을 충족한 후보 중 가장 좋은 후보가 다음 정규 인자가 됩니다.
- 기준을 충족하는 후보가 없으면, 이전 회차의 후보나 기준을 약간 완화한 후보 중에서 선택합니다. 그래도 안 되면 해당 자리는 공석이 됩니다.
선발 과정은 빈번하여, 반년 동안 15회나 정규 인자가 교체된 주제도 있습니다.
여기서 리플레이의 첫 번째 원칙이 나옵니다. 과거 특정 날짜를 재현할 때는 그날의 명단을 사용해야 합니다. 현재의 정규 인자로 반년 전 경기를 치르게 되면, "최근 성적이 좋았기 때문에 선택된 선수"를 과거에 투입하는 것이 되어 결과가 실제 실력보다 더 좋아 보이게 됩니다.
선발은 재실행하지 않고 기록을 읽는다
그렇다면 과거의 명단은 어떻게 얻을 수 있을까요?
가장 직관적인 방법은 과거로 거슬러 올라가 모든 선발 과정을 전부 다시 실행하는 것입니다. 하지만 이 과정에는 8일 이상이 걸립니다. 게다가 LLM은 매번 다른 후보를 작성하기 때문에, 재실행하더라도 당시와는 다른 명단이 만들어지게 됩니다.
그래서 QuantG에서는 선발을 재실행하지 않고, 선발 기록을 읽어냅니다. 선발 과정은 과거부터 순차적으로 진행되며, 매번 "그 시점보다 이전의 데이터"만을 가지고 후보를 점수화합니다. 그리고 각 회차의 후보와 점수화 결과는 모두 데이터베이스에 남아 있습니다. 즉, 당시 명단을 만드는 데 필요한 재료는 이미 전부 갖춰져 있는 것입니다.
기록을 읽어낼 때는 당시의 규칙으로 다시 점수화해야 합니다. 선발 규칙은 때때로 개선되기 때문에, 현재의 규칙으로 점수화하면 당시와 다른 후보가 선택될 수 있기 때문입니다. 실제로 2026년 이후에 기록상 정규 인자가 결정되었던 약 650회의 선발 과정을 다시 읽어본 결과, 모두 기록 그대로의 후보가 선택되었습니다.
기록을 그대로 읽기만으로는 부족하다: 3가지 함정
하지만 단순히 기록을 읽는다고 해서 당시 명단이 되는 것은 아니었습니다. 주의해야 할 점이 3가지 있었습니다.
함정 1: 선정된 날과 사용하기 시작한 날은 다르다
선발 과정에서 선택된 인자는 바로 사용되지 않습니다. 선택된 이후에 '답안지 확인' 기간이 있기 때문입니다.
| 시기 | 발생하는 일 |
|---|
| 선발 마감일 | 이 날짜까지의 데이터로 후보를 점수화하여 가장 좋은 후보를 결정합니다. |
| ... | |
| 선발 데이터 마감일부터 실제로 사용되기 시작할 때까지, 총 8 영업일 정도가 걸립니다. 그 사이에는 이전 정규 인자가 계속 활동하고 있습니다. | |
리플레이에서도 이 지연을 그대로 재현합니다. '선택된 날부터 사용했다'고 가정하면, 인자를 실제보다 더 신선한 상태로 사용하게 되어 성적이 지나치게 좋아 보일 수 있기 때문입니다.
함정 2: 나중에 온 후보를 과거 선발에 섞지 않는다
선발 규칙이 크게 변경되었을 때는 해당 주제의 선발 과정을 처음부터 다시 진행하는 경우가 있습니다(재구축). 이때 시스템은 이전 회차의 선발 과정도 재계산하지만, 그 후보들 중에는 나중에야 비로소 만들어진 후보가 포함되어 있었습니다.
예를 들어 기록상으로는 9월 말에 처음 등장한 후보가 3월 선발에서 승리했습니다. 하지만 현실의 3월에는 그 후보는 아직 존재하지 않았습니다. 팀으로 치면, 9월에 입단한 선수가 3월 경기에 출전하는 것과 같습니다.
따라서 리플레이에서는 후보군별로 '처음 등장한 날'을 조사하고, 해당 회차의 선정일 이후에 등장한 후보는 선정 대상에서 제외합니다. 조사해 본 결과, 승자 중 약 5분의 1(474건 중 20건)이 이에 해당했습니다.
함정 3: 재구축 과정의 결과로 명부를 덮어쓰지 않기
재구축을 할 경우, 새로운 선정은 2025년부터 한 번씩 다시 진행되므로 현재 시점까지 따라잡는 데 시간이 걸립니다. 그 사이에 같은 테마에 '구 버전'과 '재구축 중인 새 버전'의 두 가지 기록이 나란히 존재하게 됩니다.
단순히 '새 버전을 사용한다'고 결정하면, 아직 2025년 중간까지만 진행된 새 버전의 결과를 2026년까지 늘리게 됩니다. 현실적으로는 새로운 버전이 따라잡을 때까지 구 버전으로 선정된 레귤러가 활동하고 있었습니다.
그래서 어느 날짜의 명부에는 '그날까지 선정이 진행되고 있는 버전 중 가장 최신 버전'을 사용합니다. 어떤 버전도 그날까지 진행되지 않았다면, 끝까지 진행된 버전의 마지막 결과를 사용합니다.
명부는 처음에 고정한다
이렇게 만든 '일자별 명부'는 리플레이를 시작할 때 단 한 번만 만들고 지문(해시)을 붙여 저장합니다. 명부를 만드는 데 사용된 원래 기록도 함께 저장합니다.
선정은 그 이후에도 매일 진행되지만, 한번 시작한 리플레이의 명부는 바뀌지 않습니다. 같은 리플레이는 언제 봐도 같은 결과가 나옵니다.
일별 흐름
명부가 완성되면, 과거 거래일을 하루씩 진행합니다. 각 날짜에 수행하는 작업은 다음과 같습니다.
- 그날의 명부를 읽는다
- '휴식 중인' 요소를 제외한다 (건강진단. 다음 장에서 설명하겠습니다)
- 남은 요소로 익일 매수 종목을 결정한다
- 익일 이후 실제 움직임으로 매매를 기록한다
- 건강진단을 통해 익일에 휴식시킬 요소를 결정한다
- 하루치 결과를 모아서 저장한다
여기서 유념해야 할 몇 가지 사항이 있습니다.
- 실제와 동일한 프로그램을 사용합니다. 종목 선정이나 매매 기록은 매일의 실제 판단과 같은 프로그램으로 진행합니다. 리플레이용으로 다른 방식을 쓰면, 결과가 다를 때 어느 것이 정확한지 알 수 없기 때문입니다. -
실제 판단을 방해하지 않습니다. 리플레이 결과는 전용 장소에 저장하며, 실제 데이터에는 기록하지 않습니다. 또한, 실제 매일의 판단이 진행되는 동안은 리플레이가 기다립니다. -
멈추더라도 재개할 수 있습니다. 하루씩 결과를 확정하고 있기 때문에, 중간에 멈춰도 다음 날부터 다시 시작할 수 있습니다. -
사용 가능한 요소가 없는 날은 아무것도 사지 않습니다. 명부가 비어 있거나 모두 휴식 중인 날은 그날 매수하지 않습니다. 보유 종목의 매각은 평소대로 진행합니다.
또 하나 중요한 것은, 어떤 요소를 사용할지 사람에게 선택하게 하지 않는 것입니다. 당일 명부에 올라온 요소는 전부 사용해야 합니다. 사람이 '이 테마는 제외하자'라고 선택하는 순간, 현재 알고 있는 결과를 보고 선택하게 되므로 미래를 엿보게 됩니다.
한 번의 리플레이로는 자금이나 일일 매수 종목 수, 종목 배열 방식, 건강진단의 엄격함 등 다른 설정을 여러 개 나란히 비교할 수 있습니다. 요소 계산이나 모델 학습과 같이 시간이 많이 걸리는 작업은 같은 요소를 사용하는 설정들끼리 공유되기 때문에, 설정을 늘려도 시간 증가는 크지 않습니다.
건강진단도 리플레이 내에서 재현한다
QuantG에는 컨디션이 떨어진 요소를 자동으로 휴식시키는 '건강진단' 기능이 있습니다. 감독이 슬럼프에 빠진 선수를 벤치에 앉히는 것과 같습니다. 다만, 이 시스템은 작동한 지 얼마 되지 않아 과거 기록이 없습니다. 그래서 리플레이 내에서 당시의 정보만을 사용해 재현하고 있습니다.
방식은 실제와 같은 순서입니다.
- 당일 종목을 결정한 후, 밤에 각 요소의 최근 적중률 (요소가 부여한 순위와 그 후의 실제 움직임이 얼마나 일치했는지)을 측정한다 -
적중률이 기준 이하인 요소는 휴식시키고, 회복된 요소는 복귀시킨다
- 이 판단은 다음 거래일로부터 반영된다
휴식 중인 요소는 종목 선정에는 사용하지 않지만, 회복 여부를 판단하기 위해서는 적중률을 계속 측정해야 합니다. 그래서 휴식 중인 요소에 대해서도 '종목 선정에는 사용하지 않고 순위만 내는' 계산을 별도로 진행합니다.
건강진단의 엄격함은 3단계 중에서 선택할 수 있습니다.
'엄격하게'로 1개월이라는 짧은 기간을 보는 데는 이유가 있습니다. 선정은 직전 3개월 정도의 성적으로 후보를 선택하기 때문에, 막 뽑힌 요소는 그 기간 동안의 성적이 좋을 수밖에 없습니다. 긴 기간만 보고 있으면, 새로운 요소의 컨디션이 떨어져도 알아차리기 어렵습니다.
또한, 실제 운영과 마찬가지로 "최소 3개의 인자는 쉬지 않고 남기기", "결산 등의 공시 반응 유형의 인자는 판단하지 않기"와 같은 규칙이 있습니다. 또한, 한 번 측정한 적중률은 나중에 다시 계산하지 않습니다. 과거 값이 나중에 변하는 것을 방지하기 위해서입니다.
실행 전 동작 확인
명부에는 몇 달 전에 작성된 인자들이 나옵니다. 당시 사용했던 데이터의 항목명이 이후 변경되었다면, 현재 데이터로는 작동하지 않습니다. 이것이 리플레이 도중에 발견되면, 그날의 계산 전체가 실패해 버립니다.
그래서 리플레이를 시작하기 전에, 명부에 나오는 모든 인자(100여 개)를 첫날 데이터를 이용해 시험적으로 작동시켜 봅니다. 하나라도 작동하지 않는 것이 있으면, 아무것도 시작하지 않고 멈추며, 어떤 인자가 왜 작동하지 않는지 목록으로 보여줍니다.
여전히 남아있는 한계
이렇게 주의를 기울이더라도, 리플레이는 '완벽하게 당시 그대로'가 되지는 않습니다. 솔직히 말씀드립니다.
- 연구 주제, 선발 규칙, 매매 규칙, 건강 검진 기준 등은 모두 최근 데이터를 본 후에 결정된 것입니다. LLM 자체가 최근 시장 상황을 알고 있을 가능성도 있습니다.
- 선발 기록은 '선발이 한 번도 중단되지 않고 매번 예정대로 끝났다'는 경우에 해당합니다. 현실에서는 중단되거나 재작성된 적도 있으므로, 다소 낙관적입니다.
- 기록의 상당 부분은 재작성 과정에서 나중에 계산된 것이기 때문에, 당시 실제 운영되었던 인자와 완전히 일치하지 않을 수 있습니다.
따라서, 리플레이 결과는 '미래 수익 예측'이 아니라, 구조가 논리적인지 확인하기 위한 도구로 사용하고 있습니다.
요약
QuantG의 리플레이 사고방식을 요약하면 다음과 같습니다.
- 과거의 날에는,
그날의 명부(그날에 사용되었던 인자)를 사용한다 - 명부는 선발을 다시 하지 않고,
당시 규칙으로 기록을 읽어 재구성하여 만든다 - 인자를 사용하기 시작하는 것은, 선발된 날이 아니라,
답안지 확인이 끝난 다음 날부터 -
아직 존재하지 않았던 후보나, 재작성 도중의 결과는 사용하지 않는다 - 명부는
처음에 확정하여 나중에 변하지 않도록 한다 - 건강 검진도,
당시 정보만으로 리플레이 안에서 재현한다 - 어떤 인자를 사용할지
사람이 선택하지 않는다
백테스트라고 하면, 매매 계산을 정확하게 재현하는 것에 초점을 맞추기 쉽습니다. 하지만 실제로 만들어 보니, 가장 많은 노력이 필요했던 것은 '그날 무엇이 알려져 있었고, 무엇이 아직 알려지지 않았는가'를 재현하는 것이었습니다.