여기 봉(bar) 하나가 있다. Binance USDⓈ-M 무기한 선물, BTCUSDT, 13:46:00 UTC부터 시작하는 1분이다. 이 값은 klines 엔드포인트에서 열두 개 값으로 이루어진 순수 배열 형태로 나왔으며, 리테일 및 그 인접 영역의 퀀트 리서치에서 가장 흔하게 쓰이는 입력 단위다. 우리가 생성하는 거의 모든 전략이 이런 형태의 데이터를 다룬다.
그러니 인덱스별로 하나씩 뜯어보면서, 백테스트가 이 중 얼마나 많은 부분을 잘못 해석하는지 살펴보자.
| 인덱스 | 값 | 이름 |
|---|---|---|
| 0 | 1773495960000 | 오픈 시각 (ms) |
| 1 | "84120.50" | 시가 |
| 2 | "84177.90" | 고가 |
| 3 | "84098.10" | 저가 |
| 4 | "84163.40" | 종가 |
| 5 | "38.417" | 베이스 거래량 (BTC) |
| 6 | 1773496019999 | 마감 시각 (ms) |
| 7 | "3232108.94" | 쿼트 거래량 (USDT) |
| 8 | 1204 | 체결 건수 |
| 9 | "21.883" | 테이커 매수 베이스 거래량 |
| 10 | "1841203.55" | 테이커 매수 쿼트 거래량 |
| 11 | "0" | 무시 |
[0]과 [6]: 이건 몇 분봉이고, 그 시각은 누구의 시계 기준인가
오픈 시각은 1773495960000, 마감 시각은 1773496019999다. 후자를 눈여겨보라: 19999로 끝나는데, 이는 다음 봉의 오픈보다 정확히 1밀리초 짧다. 이 구간은 반개구간(half-open)이며, 거래소는 이를 명시적으로 알려주고 있는 셈이다. 내가 추적했던 데이터 버그의 절반은 누군가가 양쪽 끝점을 모두 포함하는 것으로 처리해 경계에서 체결을 이중 계산하거나, 리샘플링을 정렬하면서 5분봉 각각이 옆 봉에서 1밀리초를 빌려오게 만든 데서 시작됐다.
이 타임스탬프는 거래소 매칭 엔진의 시각이다. 당신의 시계도, 데이터 벤더의 수집 시각도, 펀딩 스냅샷이 쓰는 시각도 아니다. kline 시리즈를 다른 엔드포인트에서 가져온 펀딩비 시리즈나 미결제약정 시리즈와 조인할 때, 당신은 대부분의 경우 대략 1초 이내로만 일치하는 소스들을 서로 조인하고 있는 셈이다. 1분봉 전략이라면 조인된 모든 행에 1.7%의 타이밍 오차가 생긴다는 뜻이다. 1분 미만 단위를 다루는 전략이라면 이는 치명적이다.
그리고 이 봉은 오픈 시점에 타임스탬프가 찍힌다. 봉에 담긴 정보는 13:46:59.999가 될 때까지는 알 수 없는 정보다. 인덱싱 컨벤션에 관한 모든 질문 — 한 칸 밀어야 하는가, 이벤트 시각으로 봉의 마감 타임스탬프를 쓸 것인가 오픈 타임스탬프를 쓸 것인가 — 은 사실 같은 룩어헤드(lookahead) 문제가 옷만 바꿔 입은 것이다.
[1] "84120.50": 거래할 수 없는 시가
시가는 해당 구간 안에서 처음 체결된 거래의 가격이다. 이는 다른 두 사람 사이에 이미 완료된 거래다. 이 봉이 당신의 데이터프레임에 한 행으로 존재할 즈음이면, 그 가격은 이미 60초나 지난 값이다.
직전 봉은 84109.80에 마감했으니, 인접한 두 1분봉 사이 갭에는 $10.70의 가격 움직임이 숨어 있는 셈이다. 이는 1.3bp로, 차트상으로는 보이지도 않고 테이커 수수료의 4분의 1 정도밖에 안 된다. 좋다. 하지만 미국 CPI 발표 시점의 알트코인 무기한 선물에서 같은 시리즈를 살펴보면, 이런 봉 간 갭이 15~40bp까지 벌어진다. 봉 마감에서 신호를 내고 다음 봉 오픈에서 체결하는 백테스트는 이 갭이 0이라고 조용히 가정하고 있는 것이며, 그 갭이 정말 0에 가까운 순간은 하필 그게 별로 중요하지 않은 순간뿐이다.
[2]와 [3]: "84177.90" 고가, "84098.10" 저가
대부분의 체결 엔진이 여기서 무너진다. 그래서 이번 섹션이 가장 길다.
고가와 저가는 극단적으로 터치된 가격이다. 사이즈도, 지속 시간도 담고 있지 않다. 같은 1분에 대한 원시 체결 테이프를 보면, 84100.00 이하에서 체결된 물량은 1.4초 구간 안 아홉 건의 체결에 걸쳐 총 0.62 BTC뿐이었다. 그러니 84100에 걸어둔 백테스트 스탑은 "체결"되지만 — 만약 당신 포지션이 3 BTC라면, 하락 도중 보이는 호가창 전체를 먹어치우고 나머지 물량은 84105–84130 사이를 되짚어 올라가며 어딘가에서 체결됐을 것이다. 봉은 저가가 84098.10이었다고 말한다. 봉은 그 가격에 겨우 $52,000어치의 명목가치만 거래됐다는 사실은 말해주지 않는다.
반대 방향은 더 나쁘다. 왜냐하면 당신을 착각하게 만들기 때문이다. 84175에 지정가 매도 주문을 걸어뒀다고 해보자. 봉의 고가는 84177.90이므로, 순진한 엔진은 84175에서 체결됐다고 처리하고 테이커 비용 대신 메이커 리베이트를 장부에 기록한다. 그 체결이 실제로 일어났는지는 해당 가격대의 큐 포지션에 달려 있는데, 이는 봉이 알 수 없는 정보이고 당신도 아마 기록해두지 않았을 것이다. 터치됐다고 체결된 것은 아니다.
우리 체결 엔진에서 최종적으로 정한 규칙은 이렇다: 지정가 주문은 봉이 해당 레벨을 단순히 건드리는 것이 아니라 뚫고 지나갈 때만 체결된다. 정확히 극값 틱에서의 체결은 체결 테이프에서 나온 가격대별 거래량 증거가 있어야 인정되며, 그렇지 않으면 거부된다. 이 규칙만으로 일반적인 평균회귀 전략군에서 체결의 약 6%가 사라졌고, 한 후보 전략의 백테스트 Sharpe는 1.9에서 1.1로 떨어졌다. 그 후보는 애초에 진짜였던 적이 없었다. 체결 규칙은 그 사실을 정직하게 말해준 첫 번째 장치였을 뿐이다.
관련된 함정: 봉 내부 경로(intrabar path) 문제다. 어떤 봉의 레인지가 스탑과 익절가를 동시에 포함한다면, OHLCV만으로는 어느 쪽이 먼저 도달했는지 알 수 없다. 모든 엔진은 나름의 컨벤션을 정해야 한다. 우리는 항상 스탑이 먼저라고 가정하는데, 이는 비관적이고 가끔은 틀리지만 가짜 승리를 절대 만들어내지 않는다. 만약 당신의 엔진이 익절이 먼저라고 가정한다면, 레인지가 넓은 봉들이 모호함으로부터 수익을 조작해낼 것이고, 레인지가 넓은 봉이야말로 당신의 PnL 분포를 지배하는 바로 그 봉들이다.
[4] "84163.40": 이 봉에서 가장 신뢰할 수 없는 숫자
종가는 그 구간 안 마지막 체결가일 뿐이다. 그게 전부다. 13:46:59.8에 어떤 봇이 포지션을 정리하며 낸 0.002 BTC짜리 자투리 주문일 수도 있다. 대부분의 리서치 파이프라인은 이 구조적으로 임의적인 단 하나의 틱을 가지고 모든 신호를 계산하고, 모든 포지션을 마킹하고, 모든 청산을 평가한다.
BTC 무기한 선물에서는 거의 문제가 안 되지만, 04:00 UTC의 유동성 얕은 알트코인 무기한 선물에서는 대단히 중요해지며, 같은 1분에 대한 두 거래소 종가의 차이가 트레이드당 엣지 전체를 초과할 수도 있다. 어떤 전략의 PnL이 특히 종가에 의존한다면, 우리는 대신 인덱스에서 파생되어 훨씬 조작하기 어려운 거래소 마크 프라이스로 마킹해서 다시 실행해본다. 결과가 달라진다면, 그 전략은 데이터 아티팩트를 거래하고 있었던 것이다.
[5]와 [7]: "38.417"과 "3232108.94", 거래량의 단위는 무엇인가
베이스 거래량은 BTC, 쿼트 거래량은 USDT다. 둘 다 여기 담겨 있는데, 이는 모든 거래소가 제공하는 배려는 아니다. 이게 중요한 이유는 여러 거래소에 걸친 집계 때문이다. 코인 마진 계약은 $100 명목가치 단위의 계약 수로 호가된다. 일부 주식 피드는 라운드 랏 단위로 보고한다. 예측시장 거래소는 셰어(share) 개수로 보고하는데, 여기서 셰어란 달러 표시 이진 청구권이다. 서로 다른 단위가 섞인 유니버스 전체의 "거래량"을 단일 명목가치 단위로 정규화하지 않고 그냥 더하면, 완전히 말이 안 되는 유동성 랭킹이 나오고, 그 엉터리는 겉보기에 안정적이어서 검토를 통과해버리는 방식으로 엉터리다.
수집 시점에 모든 것을 쿼트 명목가치로 정규화하라. 원시 필드도 저장은 하되, 전략이 그것을 직접 보게 해서는 안 된다.
[8] 1204: 임팩트 모델을 결정해야 할 필드
체결 건수로 베이스 거래량을 나누면 평균 체결 단위는 0.032 BTC, 약 $2,700이 나온다. 만약 후보 전략이 $250,000를 한 번에 진입하려 한다면, 그건 해당 1분 동안의 전형적인 거래 규모의 약 92배 크기가 되겠다고 요구하는 셈이다. 일반적인 5bp짜리 슬리피지 상수가 아니라 바로 이 숫자가 당신의 제곱근 임팩트 항을 결정해야 한다. 우리는 봉별 참여율(participation rate)을 1급 컬럼으로 계산해두고, 중간값 진입 규모가 봉 명목가치의 몇 퍼센트를 넘는 전략은 거부한다. 그 이후의 모든 것은 소설이기 때문이다.
체결 건수는 또한 이상한 분(minute)들을 저렴하게 잡아낸다. 거래량은 정상인데 체결 건수가 11건으로 뚝 떨어졌다면? 누군가 블록 거래를 한 것이다. 거래량은 정상인데 체결 건수가 9,000건이라면? 그건 청산 캐스케이드가 잘게 쪼개져 처리되고 있는 것이다.
[9]와 [10]: "21.883", 다들 버리는 필드
테이커 매수 베이스 거래량이다. 이 봉의 38.417 BTC 중 21.883 BTC가 매수 주도였으므로, 부호가 있는 거래량 델타는 +5.349 BTC이고 어그레서(aggressor) 비율은 매수 쪽으로 57대43이다. 거래소는 대부분의 사람들이 읽지도 않는 이 필드 하나로, 순서 흐름 불균형(order flow imbalance)을 공짜로 넘겨주고 있다. pandas가 컬럼 이름을 그럴듯하게 붙여주지 않았을 뿐이다.
이 값 하나로 수익률을 예측할 수 있다고 주장하는 건 아니다. 순진한 델타 전략은 수수료 장부에 기부하는 가장 확실한 방법 중 하나다. 하지만 이는 가격과는 완전히 다른 측정치이고, 어차피 이미 보내고 있던 요청에 함께 딸려 오며, 매수세로 인해 오른 랠리와 매도자들이 물러나서 생긴 랠리를 구분할 수 있게 해준다. 이 둘은 OHLC상으로는 똑같아 보이지만 10분 뒤에는 다르게 움직인다. 우리 리서치 에이전트는 테이커 비율을 제공하는 거래소를 대상으로 하면서도 이를 무시하는 가설은, 있는 증거를 그냥 테이블 위에 남겨두는 것으로 취급한다.
[11] "0": ignore — 그리고 여기 없는 모든 것
인덱스 11은 폐기된(deprecated) 필드로, 영원히 0이다. 더 흥미로운 건 이 봉에 담기지 않은 것들의 목록이다: bid도, ask도, 스프레드도, 호가창 깊이도, 펀딩비도, 미결제약정도, 청산도, 마크 프라이스도, 인덱스 프라이스도 없다. 그리고 결정적으로, 당신의 주문이 메이커였을지 테이커였을지 알 방법도 없다 — 이 거래소에서는 그것이 0.045%를 지불하는 것과 0.01%를 버는 것의 차이다.
그러므로 klines만으로 만든 수수료 모델은 숫자의 옷을 입은 가정에 불과하다. 우리는 모든 전략이 미리 체결 방식을 선언하도록 강제하고, 그렇지 않다는 걸 증명하지 못하는 모든 것에는 테이커 수수료를 부과함으로써 이 문제를 해결한다.
한 번도 나타나지 않은 봉
마지막 조각이고, 메이저 종목 밖에서 가장 세게 물어뜯는 놈이다. 체결이 전혀 없던 1분은 kline 자체를 만들어내지 않는다. 벤더와 라이브러리들은 흔히 이를 순방향 채움(forward-fill)으로 처리한다: 시가=고가=저가=종가=직전 종가, 거래량은 0. 당신의 인디케이터는 아무 문제 없이 계산을 마친다. 당신의 전략은 유효한 행을 보고, 이 세상 그 누구도 그 종목을 거래하지 않았던 1분에 대해 신호를 낼 수 있다.
우리가 수집한 어느 중형 무기한 선물 종목에서는, 12개월 구간의 1분봉 중 4.1%가 체결이 전혀 없는 봉이었다. 그 종목에 대한 어느 평균회귀 후보 전략은 진입의 38%를 합성 봉(synthetic bar) 위에서 실행하고 있었는데, 이동평균으로부터의 이탈을 측정하는 모든 것에게 평평한 합성 가격은 마약과도 같기 때문이다. 백테스트는 아름답게 잘 나왔다. 그 전략은 데이터의 공백을 거래하고 있었던 것이다.
그래서 지금은 수집 계층이 봉마다 synthetic 불리언 값을 함께 들고 다니며, 모든 리샘플링 과정에 걸쳐 전파되고, 검증 관문(verification gauntlet)은 그 위에 거래가 몰려 있는 전략을 모두 탈락시킨다. 값싼 컬럼 하나다. 우리가 지금까지 작성한 그 어떤 인디케이터보다 더 많은 후보 전략을 죽인 것도 이 컬럼이다.
열두 개의 값. 그중 네 개는 습관적으로 잘못 해석되고, 두 개는 습관적으로 버려지며, 한 카테고리 전체는 행에 아예 존재하지 않은 채 엔진이 상상해서 채워 넣는다. 다음 백테스트를 돌리기 전에, 당신의 데이터 저장소에서 원시 봉 하나를 직접 꺼내, 당신의 체결 로직이 각 필드에 대해 무엇을 가정하고 있는지에 비추어 필드 하나하나를 소리 내어 읽어보라. 20분이면 되는 연습이고, 이걸 해보고 아무것도 발견하지 못한 사람을 나는 본 적이 없다.
← 전체 글