내 모델은 내일자 신문을 읽고 있었다

한동안 내 모델 중 하나는 천재처럼 보였다. 백테스트는 깨끗했고, 자신감 넘쳤으며, 거의 너무 매끄러웠다. 나는 마침내 제대로 작동하는 무언가를 만들어냈다고 믿고 싶었다.

그 모델은 간직할 만한 것을 아무것도 배우지 못했다. 그것이 배운 것은 부정행위였다 — 조용히, 내가 직접 만들어 놓은 누수를 통해, 과거에 무엇을 할지 결정하기 전에 미래의 정보를 읽을 수 있게 해 주는 누수를 통해서.

수상하리만치 우등생인 학생

기대했던 것보다 조금 더 잘하는 모델은 흥분된다. 그럴 자격이 전혀 없는데도 훨씬 더 잘하는 모델은 당신을 두렵게 만들어야 한다. 내 것은 두 번째 부류였다. 실제 시장이 들쭉날쭉한 곳에서 매끄러웠고, 불확실해야 할 곳에서 자신만만했다.

이전 글에서 나는 가장 위험한 결과는 당신을 우쭐하게 만드는 결과라고 썼다. 이것은 더 미묘한 옷을 입은 그 교훈의 반복이었다. 이번에는 숫자가 터무니없지는 않았다 — 그저 너무 좋았을 뿐이고, “너무 좋다”는 그 자체로 일종의 적신호다. 시장에서의 진짜 엣지는 작고, 노이즈가 많으며, 어렵게 얻어지는 것이기 때문이다. 시장을 풀어낸 것처럼 보이는 모델은 거의 언제나 그 대신 당신의 시뮬레이션에서 틈을 찾아낸 것이다.

미래가 들어온 곳

그 틈은 정규화에 있었다 — 모델이 학습할 수 있도록 원시 입력값의 척도를 다시 조정하는, 화려하지 않은 단계 말이다. 정규화를 하려면 통계량이 필요하다. 중심값, 산포도. 그리고 나는 그 통계량을 전체 데이터셋에 대해 한꺼번에 계산했다.

바로 그 하나의 선택이 미래가 새어 들어온 지점이다. 모든 역사를 가로질러 통계량을 계산한 다음 그것을 과거의 한 시점에 적용하면, 그 과거 순간의 모든 “정규화된” 값은 아직 일어나지 않은 데이터에 관한 정보를 조용히 담아내게 된다. 모델은 주어진 어떤 순간을 바라보면서, 그 미래를 포함한 전체 타임라인의 형태에 관한 무언가를 암묵적으로 전달받고 있었다. 모델은 내일의 가격을 직접 본 적이 없다. 그럴 필요가 없었다. 모델은 내일이 섞여 들어간 공기를 들이마시고 있었다.

룩어헤드는 왜 그렇게 만들기 쉬운가

룩어헤드 편향을 위험하게 만드는 것은 그것이 만들기 어렵다는 점이 아니다. 그것이 다른 면에서는 평범한 전처리를 통해 슬그머니 들어온다는 점이다. 코드를 작성하는 가장 자연스럽고 읽기 좋은 방법 — 데이터에 스케일러를 적합시킨 다음, 데이터를 변환하는 것 — 은 시계열에 대해서는 이미 잘못된 것인데, 아무것도 불평하지 않는다. 예외도 던져지지 않는다. 어떤 테스트도 빨갛게 변하지 않는다. 숫자는 완전히 합리적으로 보인다. 유일한 증상은 당신의 모델이 마땅한 것보다 더 좋다는 점인데, 그것은 당신이 가장 조사하고 싶지 않은 바로 그 단 하나의 증상이다.

그것이 누수의 함정이다. 그것은 버그처럼 보이지 않는다. 그것은 성공처럼 보인다.

단서: 여기서는 훌륭하고, 저기서는 죽는다

다만 그 자체로 증명은 아니지만, 한 가지 시사적인 징후가 있다. 룩어헤드로 오염된 모델은 자신이 튜닝의 대상으로 삼았던 데이터에서는 훌륭한 경향이 있고, 엿볼 수 없는 데이터를 만나는 순간 무너지는 경향이 있다. 표본 내(in-sample)의 영광과 표본 외(out-of-sample)의 붕괴 사이의 큰 격차는 누수의 경고 신호다 — 다만 그것은 과적합, 레짐의 변화, 혹은 당신이 과소평가한 비용에서 비롯될 수도 있다. 내 경우에는 누수가 범인이었다.

그래서 중요했던 검증은 결코 “백테스트가 얼마나 좋은가?”가 아니었다. 그것은 “시뮬레이션을 정직하게 만들면 얼마나 더 나빠지는가?”였다. 누수를 막았을 때 성능은 떨어졌다 — 그리고 그 하락은, 아무리 고통스러웠어도, 그 시스템이 내게 보여준 최초의 신뢰할 만한 숫자였다.

해결책: 아직 알 수 없었던 것은 결코 쓰지 마라

그 수리는 한 줄의 코드가 아니라 하나의 원칙이다. 시뮬레이션의 어느 순간이든, 모델은 그 순간 또는 그 이전에 존재했던 정보만 사용할 수 있다. 정규화의 경우, 그것은 과거의 후행 윈도우(trailing window)로부터 통계량을 도출하는 것을 의미한다 — 주어진 시점까지 가용했던 것에 적합시키되, 결코 전체 타임라인에 적합시키지 않고, 결코 미래의 단 한 점에도 적합시키지 않는 것이다.

그것은 더 느리다. 더 손이 많이 간다. 더 못나 보이는 백테스트를 만들어낸다. 이 세 가지는 모두 비용이 아니라 기능(feature)인데, 각각이 모델의 시간 여행을 시뮬레이션이 거부하는 것이기 때문이다. 정직한 파이프라인은 겉보기 성능에서 세금을 치르고, 그 세금이 당신이 실제로 신뢰할 수 있는 숫자의 대가다.

누수는 단일 버그가 아니라 하나의 가문이다

이것을 고치면서 가장 어려웠던 부분은 정규화기가 아니었다. 그것은 정규화기가 그저 하나의 문일 뿐이며, 미래는 여러 문을 통해 걸어 들어올 수 있다는 것을 깨닫는 일이었다. 같은 실수가 레이블이 만들어지는 방식 속에, 전체 시계열을 은밀하게 요약해 버리는 특징들 속에, 과거와 미래를 함께 건드리는 모든 전처리 단계 속에 숨어 있다. 누수 하나를 막았다고 해서 당신이 누수를 막은 것은 아니다. 당신은 단지 그것이 어떻게 생겼는지 배운 것뿐이고, 그래서 나머지를 찾아 나설 수 있게 된 것이다.

나는 룩어헤드를 고쳐야 할 버그로 생각하기를 멈추고, 감사(audit)해야 할 속성으로 다루기 시작했다 — 결과가 너무 깨끗해 보인다면 내가 아직 확인하지 않은 어딘가로 미래가 들어오고 있다는 가정 아래, 이제 내가 의도적으로 사냥하는 무언가로서 말이다.

백테스트는 왜 부정행위에 보상하는가

이 모든 것의 밑바탕에는 백테스트에 관한 불편한 진실이 있다. 백테스트는 실력과 사후 통찰을 구분하지 못한다. 백테스트는 시장을 진정으로 예측하는 모델과 조용히 정답지를 읽고 있는 모델에게 정확히 똑같이 지급한다. 둘 다 아름다운 곡선을 만들어낸다. 엄격하게 전방향이며 보지 못한 데이터는 그 둘을 가르기 시작하는 더 강력한 검증이다 — 다만 그것조차 그 자체만으로는 파이프라인에 누수가 없음을 증명할 수 없다.

그래서 “백테스트에서 잘했다”는 조사의 끝이 아니라 시작이다. 보기 좋은 에쿼티 곡선은 하나의 질문이다. 이 모델이 이것을 벌어들인 것인가, 아니면 내가 실수로 부정행위를 허용한 것인가? 정직한 연구라는 학문 전체는 그저 시장이 당신에게 그 질문을 던지기 전에 당신이 먼저 그 질문을 던지는 법을 배우는 것일 뿐이다.

내일자 신문을 읽는 모델은 언제나 천재처럼 보일 것이다. 그 천재성은 모델이 아니라 당신의 것이다 — 그리고 그것은 당신이 막는 것을 잊은 그 누수 안에 온전히 살고 있다.

— 시그널 없음, 수익 없음, 투자 자문이 아님.