지난주에 노트북을 보내주셨죠. 같은 시그널, 같은 유니버스, BTCUSDT 무기한 계약 데이터 14개월. 바뀐 건 체결 방식뿐이었습니다. 스프레드를 넘어서 체결하던 걸 멈추고 한 틱 안쪽에 매수 호가를 대기시켰더니 Sharpe가 0.42에서 2.14로 올랐습니다. 이게 진짜인지, 아니면 뭔가 잘못된 건지 물으셨습니다.
뭔가 잘못됐습니다. 어디서 잘못됐는지 정확히 짚어보겠습니다. 버그는 한 줄이지만, 그 밑에 깔린 교훈은 그 한 줄보다 훨씬 큽니다.
엔진에서 가져온 체결 규칙은 이렇습니다:
if bar.low <= limit_price: fill(limit_price)
이 규칙은 해당 1분 동안 시장 거래가 내 가격 이하에서 발생하면 내 가격에 체결된 것으로 본다는 뜻입니다. 실제로 이 규칙이 표현하는 건 가격이 내 호가에 닿았다는 사실뿐입니다. 가격이 닿는 것과 체결되는 것은 다릅니다. 그 사이에는 큐가 있는데, 큐가 비어 있다고 모델링하셨습니다.
내 앞에 대기 중인 물량
BTCUSDT 무기한 계약에서 84,120.0에 매수 호가를 내면, 이미 그 가격에 대기 중인 주문들의 맨 뒤에 들어갑니다. 해당 계약의 최우선 호가 잔량은 시간대에 따라 대개 4~30 BTC 정도이고, 샘플 구간의 중앙값은 약 12 BTC입니다. 주문은 0.4 BTC입니다. 체결되려면 매도자들이 충분한 총량으로 해당 가격을 때려 내 앞에 도착한 주문을 모두 소화해야 합니다. 그리고 그 전에 해당 호가가 취소되거나 시장 가격이 위로 올라가서는 안 됩니다.
그러니 백테스트가 물어야 할 건 “가격이 84,120.0에 도달했는가?”가 아니라 “내 주문이 대기하는 동안 84,120.0에서 시장가 매도가 최소 12.4 BTC 체결됐는가?”입니다. 두 사건은 전혀 다릅니다. 데이터에서는 발생 빈도가 약 3배 차이 납니다.
세 가지 체결 규칙, 세 가지 다른 전략
같은 진입 신호에 세 가지 체결 모델을 적용해 다시 실행했습니다. 알파, 수수료, 펀딩은 동일하고 체결 로직만 달랐습니다.
| 체결 규칙 | 체결 수 | 체결 후 평균 60초 엣지 | Sharpe |
|---|---|---|---|
접촉: low <= limit | 4,180 | +2.6 bp | 2.14 |
엄격한 관통: low < limit - 1 tick | 1,712 | +0.4 bp | 0.61 |
| 가격대별 거래량 큐 시뮬레이션 | 1,306 | +0.9 bp | 0.77 |
가운데 행은 누구나 가장 먼저 떠올리는 간단한 수정입니다. 시장 거래가 내 가격을 넘어 지나간 경우에만 체결로 계산하는 것이죠. 가격이 지나갔다면 내 주문도 소화했으리라는 생각입니다. 방향은 맞고 대부분의 환상을 걷어냅니다. 하지만 꽤 심한 편향이 생기는데, 그건 잠시 후에 설명하겠습니다.
아래 행이 구현해야 할 방식입니다. L3 피드도, 주문별 재구성도 필요 없습니다. 이미 필요한 데이터 대부분을 갖고 있습니다.
aggTrades로 만들 수 있는 큐 시뮬레이션
K선 데이터 대신 집계 거래 스트림을 가져오세요. 각 체결 기록에는 가격, 수량, 타임스탬프, maker 측 플래그가 들어 있습니다. 이 플래그로 공격자가 매수자인지 매도자인지 알 수 있습니다. 직접 낸 지정가 주문을 그럴듯하게 시뮬레이션하기에 충분합니다:
- 주문이 올라갈 때 해당 가격에 대기 중인 잔량을 기록합니다. 오더북 피드가 100ms 간격뿐이라면 마지막 스냅샷을 사용하세요. 수정하려는 오차에 비하면 이 오차는 작습니다.
queue_ahead = resting_size로 설정합니다. 보수적으로 잡아 맨 뒤에 있다고 가정하세요. 해당 가격대의 호가를 직접 만든 경우가 아니라면 실제로도 맨 뒤입니다.- 거래 스트림을 따라갑니다. 가격이 내 가격 이하인 매도 주도 체결이 발생할 때마다 수량만큼
queue_ahead을 감소시킵니다. 값이 0 아래로 내려가면 그 시점에 내 가격으로 체결된 것입니다. - 가격이 한 틱 멀어져도 큐를 0으로 초기화하지 마세요. 감소시키면 됩니다. 호가가 낡으면 내 앞 주문 중 일부는 취소되고 일부는 남습니다. 호가에 닿지 않은 상태가 1초 이어질 때마다 30-40% 감소시키는 설정이 양쪽 극단보다 실제 체결 기록을 더 잘 재현했습니다.
- 전략이 주문을 취소하고 다시 낸다면, 새 주문은 새 가격대의 맨 뒤에 들어간 것으로 모델링하세요. 사람들이 흔히 생략하는 단계인데, 남아 있는 환상이 여기 숨어 있습니다.
2단계에서는 반박하고 싶을 겁니다. 맞습니다. 가격대가 형성되는 순간 주문을 냈다면 앞쪽에 설 때도 있습니다. 좋습니다. 그걸 계측하세요. 가정하지 마세요. 주문을 낸 시점의 대기 잔량을 기록하고, 실제로 얼마나 이른 타이밍에 들어간 주문인지 데이터로 확인하세요. 이 전략에서는 11%였습니다. 시그널이 가격이 움직인 뒤에 발생하므로, 합류하는 호가대는 이미 형성됐고 대기자도 이미 많기 때문입니다.
체결되는 주문은 안 됐으면 하고 바랐을 주문입니다
이제 실제로 중요한 부분, 그리고 엄격한 관통 규칙에 편향이 생기는 이유를 살펴보죠.
매수 호가가 완전히 소화되는 때를 생각해 보세요. 매도 압력이 해당 호가대 전체 물량을 먹어 치울 만큼 클 때입니다. 즉, 구조상 시장이 내 가격을 뚫고 내려가는 순간에 체결됩니다. 가장 확실하게 체결되는 주문은 바로 체결 직후 손실 상태가 되는 주문입니다.
체결 방식을 나눠 60초 마크아웃을 측정해 보세요:
| 체결 유형 | 체결 비중 | 60초 마크아웃 |
|---|---|---|
| 호가대에서 거래 발생, 가격 반등 | 38% | +3.1 bp |
| 호가대에서 거래 발생, 가격 보합 | 21% | +0.2 bp |
| 가격이 2틱 이상 관통 | 41% | −2.4 bp |
단순 백테스트는 세 구간을 모두 체결로 처리했고 비용도 없는 것으로 봤습니다. 엄격한 관통 규칙은 거의 전부 세 번째 구간만 잡습니다. 그래서 큐 시뮬레이션보다 엣지가 더 크게 무너졌습니다. 둘 다 정답은 아닙니다. 큐 시뮬레이션은 현실적인 구성비를 보여주고, 그 구성비가 핵심입니다. 수동 체결은 스프레드를 벌어들이는 대신 역선택으로 되돌려 줍니다. 둘 사이의 비율이 실제 전략입니다.
예전 주식 데스크의 설명은 여전히 유효합니다. maker 주문은 시장에 써 준 공짜 옵션입니다. 옵션을 행사하는 쪽은 행사할 가치가 있을 때 행사합니다. 백테스트는 프리미엄만 챙기고 옵션의 지급 의무는 잊고 있었습니다.
체결되지 않은 거래는 비용만이 아니라 전략 자체를 바꿉니다
이 대목을 가장 곱씹어 봤으면 합니다. taker 체결을 잘못 모델링하면 거래 자체는 맞게 잡고 가격만 틀리므로 수수료를 보정하면 대부분 해결됩니다. maker 체결을 잘못 모델링하면 애초에 다른 거래들이 체결된 것으로 잡힙니다. 4,180건의 진입 중 약 2,900건은 실제로 체결되지 않았습니다. 그중에는 급등했다가 되돌아온 봉에서 나온 최고의 시그널도 있었을 겁니다. 바로 시장이 우리를 두고 가버린 상황입니다.
그러니 미체결 분기에도 제대로 된 로직이 필요합니다. 시그널이 유효하지 않게 될 때까지 진입 주문이 체결되지 않으면 전략은 어떻게 하나요? taker 주문으로 쫓아가 스프레드와 시장 충격 비용을 내나요? 더 낮은 가격에 다시 주문해 다른 진입 기준가를 받아들이나요? 거래를 건너뛰고 포지션 없이 대기하나요? 선택에 따라 자산 곡선은 크게 달라집니다. 어느 쪽도 “체결된 것으로 가정”하는 건 아닙니다. 정직한 추격 규칙을 추가해 실행했을 때, 미체결 20초 뒤 시장가로 진입하고 슬리피지는 3 bp로 제한하면 누락된 거래의 약 3분의 1을 되살리고 단순 모델과 큐 시뮬레이션 Sharpe 간 격차의 절반가량을 메웠습니다. 꽤 흥미로운 결과였고, 체결 모델이 질문을 의미 있게 만들 만큼 현실적이어야만 드러나는 결과입니다.
간단한 점검, 10분이면 됩니다: 실시간 페이퍼 트레이딩 기록과 백테스트를 같은 구간으로 맞춰 보세요. 손익이 아니라 체결률을 비교하세요. 백테스트에서 대기 주문이 100% 체결되는데 페이퍼 트레이딩에서는 34%라면 전략을 비교하는 게 아닙니다. 체결 모델에 버그가 있는 겁니다. 수익률 숫자는 하나도 보기 전에 이 문제부터 고치세요.
마지막으로 짚고 갈 작은 사항 두 가지
Post-only 주문 거절입니다. maker 수수료 등급을 보장하려고 post-only를 쓰는데, 주문 결정 시점과 거래소 확인 응답 사이에 오더북이 움직이면 주문은 대기 상태가 아니라 거절로 돌아옵니다. 일반적인 시간대에 BTCUSDT에서 페이퍼 트레이딩 기록을 보면 시도 건의 3-6%가 거절되고, 미국 CPI 발표 직후 1분에는 15%를 넘습니다. 거절된 주문은 체결된 주문도, 대기 중인 미체결 주문도 아닙니다. 아예 존재하지 않았던 거래입니다. 백테스트에 이 상태가 없다면 가장 중요하게 보는 시장 상황에서 거래 수가 부풀려집니다.
자전거래 방지와 내 주문의 시장 영향입니다. 0.4 BTC 규모로는 BTCUSDT 가격을 움직이지 않으니 건너뛰어도 됩니다. 하지만 최우선 호가의 명목 가치가 종종 $15k 미만인 중형 알트 무기한 계약에 이 전략을 적용하고 싶다고 하셨죠. 그런 시장에서는 주문이 큐에서 차지하는 비중이 상당합니다. 스냅샷에 기록한 앞선 대기 물량에는 이전에 낸 주문의 영향도 포함됩니다. 주문 규모가 대기 잔량의 약 10%를 넘으면 다른 참여자들이 우리 주문에 반응한다는 점까지 시뮬레이션에 반영해야 합니다. 솔직히 그 정도 규모라면 저나 당신이 작성하는 어떤 시뮬레이션보다 페이퍼 트레이딩을 신뢰하겠습니다.
큐 시뮬레이션으로 다시 실행하고 체결 유형별 마크아웃 표를 보내주세요. 반등 구간이 여전히 엣지 대부분을 차지하고 관통 구간이 그 엣지를 전부 잡아먹지 않는다면 페이퍼 트레이딩에 올려볼 만한 전략일 수 있습니다. 전부 실제로는 얻지 못했을 2,900건의 체결에서 나온 결과라면, 노트북이 약속한 대로 움직이지 않는 페이퍼 계좌를 4주 동안 지켜본 뒤보다 지금 알아내는 편이 낫습니다.
← 전체 글

