[rule]내 백테스트는 너무 자주 거짓말을 했다

백테스트 결과가 너무 예쁘면

십중팔구 거짓말이다

 

코인을 으로 사고팔지 않고 규칙 으로 사고파는 것을 퀀트(quant) 라고 부른다.

그 규칙이 과거에 잘 작동했는지 컴퓨터로 시뮬레이션해 보는 것을 백테스트 라고 한다.

 

문제는, 백테스트가 너무 자주 거짓말을 한다는 것이다.

화면 위에서는 자산이 우상향한다.

근데 그 곡선을 믿고 실제 돈을 넣어보면 환상이었음이 드러난다.

 

지난 7년간 내가 백테스트한테 속은 횟수는 셀 수 없다..

속을 때마다 그 자리에 한 줄씩 을 새겼다.

오늘은 그 룰들을 박제하는 날.

 

룰 15개.

전부 한 번 깨진 자리, 좌절한 자리 에서 나온 것들이다.


1. 데이터를 의심하라

백테스트의 출발점은 과거 가격 데이터.

근데 그 출발점부터 거짓말이 들어와 있는 경우가 많다.

가장 잦은 사고가 여기서 난다.

 

받아온 데이터를 그대로 믿지 마라

코인 거래소는 “API” 라는 통로로 과거 가격 데이터를 자동으로 보내준다.

문제는 이 통로가 가끔 조용히 일부를 빼먹고 보낸다는 것.

에러도 안 띄운다. 그냥 짧게 도착한다.

 

같은 전략, 같은 기간으로 백테스트를 돌렸는데

최대 손실 폭이 매번 4%p 씩 다르게 찍혔다.

알고 보니 1,000개를 받아야 하는 구간에서 어떤 날은 980개만 도착한 것.

조용히 잘려서 들어온 그 20개 차이 가 결과를 흔들고 있었다.

 

요리 레시피 테스트인데 누군가 재료를 가끔 한두 개씩 빼고 배달해 주는 격이다.

그 결과로 “이 레시피는 좋다” 판단할 수는 없다.

룰: 받은 데이터는 (1) 개수 검증, (2) 파일로 저장(캐시), (3) 아직 진행 중인 캔들 은 빼고 사용.

 

데이터 앞부분이 비어 있는 채로 그냥 쓰지 마라

전략에는 워밍업 구간 이라는 게 필요하다.

“이동평균 200일” 을 쓰는 전략이라면 데이터 앞 200일은 계산이 안 된다.

200일치 평균을 내야 하는데 200일이 모이지 않은 구간이니까.

이 구간은 매매 신호가 비어 있는 채로 흘러간다.

 

학습용 구간과 검증용 구간을 따로따로 잘라서 로딩했더니

학습 구간 연 수익률이 5.5%p 낮게 찍혔다.

원인: 학습 구간 앞 200일이 비어 있어서, 그만큼 거래를 안 한 채 시간이 흘렀기 때문.

 

비교가 평등하지 않은 채로 결과만 받아보고 있었다.

룰: 구간을 자르기 전에 워밍업만큼 앞쪽에 더 붙여서 로딩한 다음 잘라낸다.

 

일봉 신호를 시간봉에 그대로 갖다 붙이지 마라

서로 다른 시간 단위 데이터를 합쳐 쓸 때가 있다.

예: “일봉 추세가 위쪽일 때만 시간봉에서 매수” 같은 룰.

 

이걸 무심코 합쳤더니 백테스트 연 수익률이 +109%.

뭔가 이상해서 들여다보니, 오늘의 일봉 종가를 오늘 새벽에 이미 사용하고 있었다.

오늘 자정에야 확정될 종가를, 오늘 아침 9시 시점에서 이미 알고 있는 듯 매매하고 있었던 것.

이걸 미래정보 누수(lookahead bias) 라고 부른다.

미래를 알고 매매하면 당연히 결과가 좋다.

 

버그를 잡고 다시 돌리니 +109% → +4.6%.

거짓말 한 줄이 100%p 가 넘는 환상을 만들어 낸다.

룰: 다른 시간 단위 데이터는 항상 전일 종가까지만 사용. 오늘 정보는 내일부터 쓴다.


2. 숫자가 너무 예쁘면 한 번 더 의심하라

백테스트 결과는 결국 숫자 다.

근데 그 숫자가 무엇으로 만들어졌는지를 자주 잊는다.

 

“fixed”, “clean” 이라는 이름의 파일을 그대로 믿지 마라

결과 파일들이 쌓이다 보면 “fixed_xxx”, “clean_xxx” 같은 이름이 등장한다.

정리된 결과 라는 뉘앙스라 그대로 인용하기 쉽다.

 

그 깨끗한 파일에서 최대 손실 폭이 -8% 로 찍혀 좋아했다.

알고 보니 월말 시점만 모아서 합성한 데이터였다.

월 중간에 어떤 깊은 손실 골짜기가 있었는지는 전혀 보이지 않는다.

매일매일의 자산 곡선으로 다시 돌리니 -16%.

거의 두 배.

 

월말에 찍은 가족 사진만 보면 한 달이 평온해 보인다.

그 사이에 무슨 싸움이 있었는지는 사진에 없다.

룰: 최대 손실 폭은 매일 단위 자산 곡선 을 직접 계산한 값만 신뢰한다.

 

현금 잔고 차트를 “최대 손실” 이라고 부르지 마라

진짜 손실의 깊이는 총자산 = 현금 + 보유 포지션의 평가액 으로 계산해야 한다.

이걸 영어로 equity 라고 한다.

현금만 떼어 보면 그건 현금이 들락날락한 정도 일 뿐, 손실의 깊이가 아니다.

 

예시:

100원으로 시작.

코인 100원어치 매수 → 현금 0원.

코인 가격이 반토막 → 보유 포지션 평가액 50원.

현재 내 진짜 자산 은 50원이다.

근데 현금만 보면 0원에서 변동 없어 보인다. 평온해 보인다.

실제론 -50% 손실 중인데.

 

룰: 최대 손실 폭은 무조건 총자산 기준 으로 계산. 현금 잔고는 “현금 회전율” 같은 다른 이름으로 부른다.

 

레버리지를 비율 개선의 도구로 착각하지 마라

레버리지는 빌린 돈으로 더 큰 포지션을 잡는 것.

3배 레버리지면 100원 자본으로 300원어치 포지션을 굴린다.

수익도 3배, 손실도 3배.

 

여기서 흔한 오해.

“레버리지를 줄이면 위험 대비 수익이 좋아지지 않을까?”

3배 → 2배로 낮춰 보면, 연 수익률도 줄지만 최대 손실 폭도 같이 준다.

둘이 같은 비율로 줄어들기 때문에 비율은 그대로.

 

수익 / 손실 비율은 전략 자체가 더 똑똑해지거나, 다른 자산과 분산이 잘 될 때 만 움직인다.

레버리지로는 그래프의 세로 축만 늘었다 줄었다 할 뿐.

 

룰: 레버리지는 변동성 조절 노브. 위험 대비 수익을 개선하려면 전략 자체 또는 분산 을 건드려야 한다.


3. 시뮬레이션 자체를 의심하라

전략이 멀쩡해도, 시뮬레이터가 거짓말 하는 경우가 있다.

이게 가장 찾기 어렵다.

전략을 백날 만져봐야 답이 안 나온다.

 

청산할 때 마진 환원을 빼먹지 마라

레버리지 매매를 하려면 마진(증거금) 이라는 돈을 일부 잠가 둔다.

100원짜리 포지션을 3배 레버리지로 잡으면 약 33원이 마진으로 묶인다.

포지션을 정리(청산)하면 그 33원이 다시 자본 잔고로 돌아와야 한다.

 

근데 시뮬레이션 코드에서 그 환원을 빼먹은 적이 있다.

자본이 시간이 갈수록 마치 줄어드는 듯 흘러갔고

검증 구간 최대 손실 폭이 -40% 로 찍혔다.

전략 탓인 줄 알고 한 달 가까이 매달렸다.

 

알고 보니 시뮬레이터의 단순 버그.

청산 코드에 마진 환원 한 줄을 넣으니 -16%.

전략은 멀쩡한데 시뮬레이터가 거짓말하고 있었다.

룰: 청산 코드 한 줄마다 잠긴 마진이 자본으로 되돌아오는지 눈으로 검수.

 

강제청산이 일어날 자리를 시뮬에서 빼먹지 마라

레버리지가 높을 때 손실이 일정 한계를 넘으면 거래소가 자동으로 포지션을 닫아 버린다.

이걸 강제청산(liquidation) 이라고 한다.

남은 자본이 0 이 되거나 거의 0 에 수렴한다.

게임 끝.

 

내 시뮬레이션은 이걸 빼먹어서, 손익률 -95% 에서도 살아 있는 듯이 돌아갔다.

그 후 다시 회복하는 그래프가 그려졌다.

현실에선 -95% 가기 한참 전에 이미 청산되어 복구는 없는 게임 이었는데.

있을 수 없는 미래 를 시뮬레이터가 그려 주고 있었던 것이다.

 

룰: 일정 손실 임계 미만(예: -90%)이 되면 시뮬을 강제 종료시키는 가드를 명시적으로 박는다.

 

차트로 보던 시각 매매 룰을 그대로 시스템화하지 마라

추세선, 박스권, 오더블록(OB), ICT, SMC.

차트 위에 직접 그리며 트레이딩하는 사람들이 쓰는 시각 매매법들이다.

눈으로 보면 너무 잘 맞는 것 같다.

 

이 룰들을 코드로 옮겨 자동 백테스트를 돌리면 마이너스 기댓값 이 나오는 경우가 많다.

(기댓값 = 거래 한 번당 평균 손익. 마이너스면 거래할수록 잃는다.)

 

이유는 단순하다.

사람은 차트에서 맞은 것만 추세선이라고 부르고 있었다.

지나고 나서야 보이는 추세를, 마치 그 시점에 알았던 것처럼 그렸을 뿐.

이 사후적 편향을 holding bias 라고 부른다.

 

룰: 시각 룰 백테스트가 학습 구간에서 너무 좋다면, 사후 편향을 의심한다. 시스템화 대신 알람 봇이 답일 때도 많다.


4. 검증 구간(OOS)은 신성하다, 절대 만지지 마라

백테스트에서 데이터를 두 개로 나누는 것 이 기본이다.

  • 학습 구간(IS, In-Sample): 전략을 만들고 다듬는 구간.
  • 검증 구간(OOS, Out-of-Sample): 그 전략이 처음 보는 데이터에서도 작동하는지 확인하는 구간.

OOS 의 역할은 시험 이다.

 

OOS 결과를 보고 파라미터를 다시 고치지 마라

규칙은 단 하나.

OOS 는 마지막에 한 번만 본다.

OOS 결과를 보고 “이 숫자를 0.3 에서 0.4 로 바꿔볼까?” 하는 순간

그 OOS 는 더 이상 OOS 가 아니다.

뇌는 한 번 본 것 을 잊지 못하기 때문.

 

시험지 미리 본 학생이 그 시험에서 100점 받은 게 무슨 의미가 있겠는가.

내가 가장 자주 깨졌고, 가장 위험한 룰이다.

 

룰: 파라미터 선정은 학습 구간 + 보조 검증 구간(OOS2)으로만. 최종 OOS 는 마지막에 단 한 번 만 본다.

 

약세장을 통째로 빼버리지 마라

흔한 유혹.

“하락장에선 매매 안 하면 손실 안 나는 거 아냐?”

예를 들어 “200일 이동평균 아래에선 거래 정지” 같은 룰을 넣고 싶어진다.

 

이렇게 하면 학습 구간에서는 눈부시게 좋아진다.

근데 검증 구간에서는 -31%p 무너졌다.

 

이유: 약세장 자체가 알파의 일부 였다.

약세장의 변동성을 이용해 반대로 들어가는 매매 가 수익을 만들고 있었던 것.

통째로 차단하면 그 수익원도 같이 사라진다.

손실만 사라지는 게 아니라 수익도 같이 사라진다.

 

룰: 약세장은 제외 가 아니라 사이즈 축소 로 다룬다.

 

서브 전략 단독 성과로 채택 결정하지 마라

봇이 여러 서브 전략(sleeve)을 동시에 굴리는 경우가 있다.

각 서브 전략은 자본의 일부(예: 25%) 만 굴린다.

위험을 분산하기 위해서다.

 

서브 전략 하나를 새로 추가했더니 단독으로는 +0.30 Sharpe 개선.

(Sharpe = 위험 대비 수익을 나타내는 지표. 0.30 개선이면 꽤 큰 폭.)

엄청난 개선이라고 흥분했는데

포트폴리오 전체로 합쳐 계산하니 +0.025.

비중이 25% 였기 때문에 12배 희석 된 것.

 

전체 봇 성과는 거의 그대로.

 

룰: 채택 여부는 서브 단독 수치 가 아니라 포트폴리오 전체 합산 으로 결정한다.

 

이미 OOS 까지 다 써서 만든 전략을 새 baseline 으로 깔지 마라

전략을 처음 만들 때는 학습 구간만 봤지만, 검증 과정에서 OOS 까지 다 보고 다듬게 된다.

여기까지는 정상.

문제는 그 전략을 “확정된 base” 로 두고 그 위에 새 모듈을 얹어 다시 검증을 시작할 때.

 

뼈대 자체가 이미 OOS 정보를 알고 있으니, 그 위에 무엇을 쌓아도 결국 학습 구간 안의 검증 이 된다.

겉보기엔 새 검증인데, 속은 in-sample.

 

진짜 검증은 OOS 한 번도 보지 않은 상태 에서 시작해야 한다.

 

룰: 새 모듈 검증의 baseline 은 OOS 미사용 상태로 동결한 버전 을 사용한다.


5. 백테스트가 멀쩡해도, 라이브로 옮기면 또 거짓말한다

여기까지 다 통과한 전략이라도, 자동매매 봇으로 옮기는 순간 결과가 어긋나기 시작한다.

원인은 둘.

언제 가격을 보느냐, 어떤 가격으로 진입하느냐.

 

같은 봉인데, 보는 시점마다 결과가 다르다

백테스트는 완성된 봉 만 본다.

봉이 닫히고 종가가 확정된 시점에서 신호를 평가하고,

다음 봉이 시작하는 시가 에 진입한다고 가정한다.

깨끗한 가정이다.

 

라이브 봇은 다르다.

매분 가격을 들여다보면서 그 순간의 임시 가격 으로 신호를 평가한다.

4시간 봉이 막 시작해서 7분이 지났다면, 봇이 보는 종가 는 그 7분 동안 흔들린 진행 중인 가격 이다.

같은 봉인데도 언제 들여다봤느냐 에 따라 신호 결과가 달라진다.

 

내 봇이 첫 매매를 시작했을 때 정확히 이 일이 났다.

봉 마감 직전 미리 본 시점에는 매수 조건을 만족한 코인이 3개.

실제로는 7분 뒤에 봇이 처리했고, 그 사이에 가격이 살짝 움직이면서 2개는 조건에서 빠져나갔다.

결국 1개만 들어갔다.

3개 모두 들어가야 하는 거래였는데.

 

룰: 라이브 봇도 완성된 봉 종가 로 신호 평가, 다음 봉 시가 로 진입. 진행 중인 봉은 무조건 빼고 본다. 백테스트와 같은 가격 을 보게 만든다.

 

봇이 일하는 시각이 어긋나면, 매번 늦게 들어간다

봇이 매 10분마다 한 번씩 일하도록 되어 있다고 치자.

이 봇이 3시 57분 에 켜지면, 다음 일은 4시 7분 에 한다.

4시 봉이 마감된 7분 뒤.

 

이 7분 사이에 가격은 가만히 있지 않는다.

백테스트는 4시 정각의 가격 으로 진입을 가정했는데, 봇은 4시 7분의 가격 으로 진입한다.

매번 늦는다.

 

해결은 단순하다.

봇이 분 정각 에 맞춰 일하게 강제한다.

4시 0분 0초, 4시 1분 0초, 이런 식으로.

봉 마감 시각과 봇의 처리 시각의 간격을 0~1초 까지 줄여 버린다.

 

룰: 라이브 봇은 분 정각 정렬 로 일하게 만든다. 봉 마감 시각과 진입 시각의 간격을 1초 이내로 줄인다.

 

일하는 도중에 끄지 못하게 안전장치를 박는다

봇이 주문을 처리하는 도중 에 강제로 꺼지면 어떻게 될까.

신호 평가는 끝났는데 진입 기록이 안 남았거나, 일부만 처리되고 죽거나.

이 상태로 다시 켜면 반쯤 처리된 거래 위에서 새 거래가 시작된다.

장부가 어긋난다.

 

이 사고를 막으려면 봇에 안전 종료 장치를 박는다.

종료 신호가 들어오면 지금 하는 일을 끝까지 마무리하고 종료한다.

새 일은 시작하지 않는다.

 

룰: 라이브 봇은 종료 신호를 기다림 으로 받는다. 도중에 자르지 않는다. 도중에 자르면 장부가 깨진다.


마무리

룰 15개.

전부 한 번 깨진 자리 에서 새겼다.

깨질 때마다 비싼 수업료를 냈다 — 시간이거나, 돈이거나, 둘 다였다.

 

그래도 다행인 건, 백테스트가 거짓말하는 패턴 은 정해져 있다는 것.

매번 똑같이 속아주진 않는다.

 

다음에 또 속겠지.

또 한 줄 새기면 된다.

 

댓글