84,120에 롱 진입. 손절가 84,036, 목표가 84,271. 다음 1분 봉이 들어옵니다. 시가 84,118, 고가 84,290, 저가 84,010, 종가 84,240.
두 가격대가 모두 그 봉 안에 있습니다. 손절가에도 닿았고 목표가에도 닿았지만, OHLC 숫자 4개에는 어느 쪽이 먼저였는지에 관한 정보가 전혀 없습니다. 그래도 백테스트는 결과를 냈습니다. 반복문 어딘가의 한 줄이 결정을 내린 겁니다. 코드를 쓸 때 모델링 가정이라고 생각하지 않았을 법한 줄이었겠죠.
제가 보는 단기 전략의 가짜 성과를 만들어 내는 가장 큰 원인은 이겁니다. 수수료나 슬리피지보다도 큽니다. 가정처럼 보이지 않기 때문입니다. 그냥 코드가 돌아가게 하는 배관처럼 보이죠.
잘못된 방식 1: if문 순서에 맡기기
흔히 이런 형태입니다.
if bar.high >= target:
exit(target, "tp")
elif bar.low <= stop:
exit(stop, "sl")
누구도 “목표가가 손절가보다 먼저 처리된다”고 선택한 게 아닙니다. 목표가 확인이 먼저 입력됐을 뿐입니다. 그게 원하는 결과이고, 작성자가 생각하던 흐름이었으니까요. 두 분기 순서를 바꾸면 자산 곡선이 달라집니다. 전략의 손익이 편집기에서 코드를 입력한 순서에 좌우된다는 사실만으로도 문제를 알 수 있습니다.
BTCUSDT 무기한 계약에서 지극히 평범한 평균회귀 스캘퍼를 돌려 봤습니다. 1분 봉 3개월, 거래 4,812건, 진입가 대비 손절 0.10%, 목표가 0.18%였습니다. 시그널과 수수료는 그대로 두고, 모호한 경우의 처리 방식만 바꿨습니다.
거래 8건 중 1건이 전체 결과를 좌우합니다. 손절과 목표가 간격이 좁으면 모호한 거래는 가격이 양쪽으로 움직인 경우이므로 흥미로운 거래 대부분이 여기에 속하고, 동전 던지기처럼 어느 쪽으로 판정하느냐에 따라 손절가부터 목표가까지의 전체 차이만큼 결과가 달라집니다. 거래의 12.6% × 가격 범위 0.28%는 표본 단위당 총 명목 거래대금의 3.5%입니다. 전략의 실제 엣지를 압도하는 규모죠.
이 비율은 봉 크기와 가격대 간격의 함수이며, 봉이 길어질수록 빠르게 커집니다. 손절가와 목표가, 시그널은 그대로 두고 봉 간격만 바꿔 봤습니다.
| 봉 간격 | 한 봉 안에서 두 가격대에 모두 도달한 거래 | Sharpe (목표가 우선) |
|---|---|---|
| 1s | 0.3% | 0.91 |
| 1m | 12.6% | 2.31 |
| 5m | 34% | 3.60 |
| 15m | 49% | 4.42 |
| 1h | 71% | 5.88 |
이 표가 실제로 무엇을 말하는지 보세요. 봉이 거칠수록 백테스트 결과가 좋아졌습니다. 연구자라면 누구나 시간 단위 봉이 더 보수적이라고 생각합니다. 노이즈가 적고 미시구조에 과적합할 가능성도 낮으니까요. 하지만 봉 내부 상황을 자신에게 유리하게 판정하는 규칙을 적용하면, 거친 봉은 운이 좋았다고 가정할 수 있는 더 큰 상자일 뿐입니다. 1시간 봉에서는 거래 10건 중 7건이 처리 규칙에 따라 결과가 정해집니다. 이 백테스트는 전략을 검증하는 게 아니라, if문 두 개의 순서를 3,400번 시험하는 셈입니다.
잘못된 방식 2: 손절가에서 체결됐다고 가정하기
처리 순서를 정했다고 해 보죠. 손절가를 먼저 적용해 정확히 0.10% 손실에 taker 수수료를 더해 기록하면, 이제 엄밀하게 처리했다고 느낄 겁니다. 하지만 여전히 두 가지가 잘못됐습니다.
첫째, 스톱은 체결이 아니라 발동 조건입니다. Binance USDⓈ-M에서 STOP_MARKET 주문은 발동 조건이 충족되는 순간 시장가 주문으로 전환된 다음, 호가창에 있는 물량을 가져갑니다. 조용한 분에는 슬리피지가 1~2틱 정도입니다. 하지만 손절을 실제로 발동시킨 분, 즉 40포인트짜리 캔들 몸통과 그 아래 청산 물량이 쏟아진 분에는 내가 체결하는 바로 그쪽 호가가 얇습니다. 제 표본에서 스톱 발동 시점을 체결 테이프와 대조했더니, 체결 가격은 발동가보다 중앙값 기준 1.4 bps, 95백분위 기준 11 bps 더 불리했습니다. 손절폭이 10 bps라면 꼬리 구간에서 처음 설정한 위험의 10분의 1이 추가 비용으로 발생합니다.
둘째는 좀 더 미묘하고 무기한 계약에 특유한 문제입니다. 어떤 가격이 주문을 발동시키는가입니다. Binance는 스톱 주문의 기준 가격을 기본적으로 mark price로 설정합니다. mark price는 해당 거래소의 최근 체결가가 아니라 지수와 평활화된 베이시스를 바탕으로 계산됩니다. OHLC 시계열은 최근 체결가입니다. 서로 다른 시계열이며, 스톱이 발동되는 바로 그 순간에 차이가 가장 커집니다.
| 최근 체결가 (사용하는 kline) | Mark price (기본 발동 기준) | |
|---|---|---|
| 출처 | 이 거래소의 체결 내역 | 여러 거래소의 지수 + 베이시스 |
| 꼬리 움직임 | 전체 변동폭 반영 | 강하게 평활화됨 |
| 일반적인 괴리 | 평온할 때 1–3 bps, 청산이 몰리는 분에는 20–35 bps | |
| 백테스트상의 결과 | 발동되지 말았어야 할 스톱이 발동되거나 그 반대 | |
그러므로 최근 체결가 테이프에 25 bps 꼬리가 생기면 백테스트에서는 손절되지만, 실제 mark price는 발동가에서 10 bps 이내로도 접근하지 않았을 수 있습니다. 지수가 움직이고 거래소가 뒤처지는 날에는 반대 상황도 가능합니다. workingType를 CONTRACT_PRICE로 설정하면 실제 작동 방식과 데이터가 적어도 일치합니다. 연구자에게는 대개 이 설정이 맞습니다. mark price 기준 발동을 제대로 시뮬레이션하려면 체결 엔진 전체에서 두 번째 시계열을 다뤄야 하기 때문입니다.
가장 기억에 남는 사례는 이렇습니다. 우리 팀 누군가가 익절가를 0.18%에서 0.21%로 옮겨 전략을 “개선”했습니다. Sharpe는 2.3에서 3.1로 올랐죠. 새로운 엣지는 없었습니다. 목표가가 1분 꼬리 분포의 밀집 구간을 벗어나자 코드가 조용히 승리로 처리하던 모호한 거래가 줄어든 것뿐입니다. 동점 판정 규칙을 최적화한 셈이죠.
잘못된 방식 3: 항상 최악을 가정하고 보수적이라고 부르기
반사적으로 떠올리는 해결책은 비관적으로 처리하는 겁니다. 두 가격대에 모두 닿으면 손절로 처리한다. 끝. 더는 낙관적이지 않으니 바로 배포하면 된다고 생각하죠.
저도 예전엔 이렇게 했습니다. 다른 방법보다는 낫지만, 그래도 틀렸습니다. 이유는 두 가지입니다.
괜찮은 전략까지 없애버립니다. 거래의 12.6%를 비관적으로 처리하자 이 전략의 Sharpe는 틱 기준 판정값 0.94보다 2.1포인트 낮아졌습니다. 실제 값이 0.94인데 처리 규칙 때문에 0.18로 나온다면, 아이디어를 버리고 더 나쁜 일에 매달리게 됩니다. Sharpe를 2포인트나 잘못 낮추는 보수성은 보수성이 아닙니다. 도덕적인 태도를 두른 노이즈일 뿐입니다.
더 나쁜 점은 최적화까지 왜곡한다는 겁니다. 비관적인 동점 판정을 파라미터 스윕에 넘기면 모호한 경우를 피하도록 최적화기가 학습합니다. 이제 모호함 자체가 순수한 페널티이기 때문입니다. 그러면 손절폭은 넓고 목표가는 가까운 쪽이나, 두 가격대가 한 봉에 함께 나타나는 일이 드문 느린 봉 쪽으로 탐색이 치우칩니다. 시장이 아니라 체결 규칙에 맞춰 고른 파라미터를 내놓게 됩니다. 후하게 판정하는 방식과 똑같은 실패지만 방향만 반대이고, tearsheet에서는 똑같이 알아차리기 어렵습니다.
무엇보다 먼저 확인하는 경험칙이 있습니다. stop_distance + target_distance가 사용하는 봉 간격의 75백분위 범위보다 작다면, 봉 내부 가정이 시그널보다 손익에 더 큰 영향을 줍니다. 두 값을 모두 계산해 보세요. 4줄이면 충분하고, 다른 어떤 단일 점검보다 더 많은 전략 검토를 여기서 끝냈습니다.
실제로 효과 있는 방법
봉 내부의 가격 경로는 데이터입니다. 데이터를 구하거나, 구할 수 없는 부분의 범위를 정하세요.
- 가능한 한 세밀한 시계열로 판정하세요. 해당 분의 Binance aggTrades는 수백 행 정도면 충분하고, 무엇이 먼저 닿았는지와 쓸어 담을 때 얼마에 체결됐는지를 명확히 알 수 있습니다. 백테스트 전체에 틱 데이터가 필요한 건 아닙니다. 모호한 봉에만 있으면 됩니다. 제 표본에서는 129,600분 중 606분이 해당했습니다. 인프라 프로젝트를 벌일 일이 아니라, 적은 양의 데이터를 내려받으면 됩니다.
- 틱 데이터를 구할 수 없다면 판정에만 1~2단계 더 짧은 시간 단위를 사용하세요. 15분 봉에서 시그널을 생성하고, 청산은 1초 또는 1분 봉으로 판정합니다. 모호한 경우가 49%에서 1% 미만으로 줄고, 남는 부분은 솔직하게 무시할 수 있을 만큼 작아집니다.
- 항상 결과 범위를 보고하세요. 모든 백테스트를 낙관적 판정과 비관적 판정으로 각각 실행하고, 두 Sharpe를 세밀한 판정 결과와 함께 표시하세요. 두 결과의 차이가 봉 내부 불확실성입니다. Sharpe 자체에 신뢰구간을 붙이듯 tearsheet에도 이 범위를 넣어야 합니다. 범위가 0.2–2.3이라면, 그 안에 있는 결론은 어느 것도 실재한다고 볼 수 없습니다.
- 모호한 거래 비율을 핵심 지표로 추적하세요. 우리는 모든 전략 카드 상단에 거래 수와 회전율 옆에 이 비율을 표시합니다. 5%를 넘으면 테스트 대상은 진입 로직이 아니라 청산 로직입니다.
- 발동 조건과 체결을 따로 모델링하세요. 거래소가 실제 사용하는 가격 시계열을 기준으로 발동시키고, 발동가에 체결 테이프로 보정한 슬리피지 추출값을 더해 체결하세요. 발동가에 체결됐다고 가정하면 안 됩니다.
주식에도 같은 문제가 다른 모습으로 나타납니다. 밤사이 58.40으로 갭 하락하는 종목의 62.00 손절가는 62.00에 체결되지 않습니다. 시가보다 낮은 가격에 체결됩니다. 갭 하락 때 슬리피지를 −$0.00으로 기록하는 일봉 백테스트는 손절 오버레이가 낙폭을 줄였다고 자신 있게 말할 겁니다. 실제로는 그렇지 않습니다. 중요한 날에 한 번도 시험되지 않았을 뿐입니다. 거래 정지도 마찬가지입니다. 손절 주문이 실제로 정리되는 시점은 거래 재개 경매이며, 그 가격은 봉의 저가에 나타나지 않습니다.
이런 문제는 특이한 일이 아닙니다. 봉은 요약 정보일 뿐이고, 손절과 목표가를 함께 쓰는 전략은 그 요약에서 빠진 사건의 순서에 베팅한다는 사실을 알아야 합니다. 페이퍼 트레이딩 엔진이 마침내 실시간 테이프를 대상으로 전략을 돌리면, 테이프는 사건의 순서를 그대로 보여줍니다. 여러분이 if문에서 어느 분기를 먼저 입력했는지는 단 한 번도 신경 쓴 적이 없습니다.
← 전체 글


