일요일 밤에 노트북을 보내주셨고, 그 뒤로 쭉 보조 모니터에 띄워 두었습니다. 횡단면 모멘텀, 30일 달러 거래량 기준 Binance USDⓈ-M 무기한 선물 상위 150개, 주간 리밸런싱, 상위 10분위 매수·하위 10분위 매도, 2021-01부터 2026-06까지. Sharpe 2.31, 최대 낙폭 14.2%, 비용 모델도 제 눈에는 타당했습니다. 진입과 청산 모두 taker, 구간별 펀딩 누적, 오더북 대비 주문 규모에 따라 커지는 슬리피지 항. 어려운 부분은 제대로 처리하셨습니다. 그런데 6주간 페이퍼 트레이딩 결과가 제자리인 이유가 뭔지, 엣지가 사라진 건지 물으셨죠.
엣지가 사라진 게 아닙니다. 백테스트에 애초에 없었습니다. 4번 셀을 보세요:
info = requests.get(BASE + "/fapi/v1/exchangeInfo").json()
symbols = [s["symbol"] for s in info["symbols"]
if s["status"] == "TRADING" and s["quoteAsset"] == "USDT"]
2026년 6월에 그 엔드포인트를 호출하고, 그 결과로 2021년 3월에 거래할 수 있었던 종목을 정했습니다. 그 목록의 모든 심볼은 정의상 2026년 6월까지 살아남은 종목입니다. 이게 버그의 전부입니다. 비용 모델의 나머지 부분을 다 합친 것보다 영향이 큽니다.
엔드포인트가 알려주지 않는 것
exchangeInfo에는 과거 기록이 없습니다. asOf 매개변수도, 아카이브도, 변경 로그도 없습니다. 지금 이 순간의 사진일 뿐이고, Binance는 그 이상을 약속한 적이 없습니다. 계약이 상장 폐지되면 응답에서 항목이 삭제되고, API 기준으로는 티커도 존재하지 않게 됩니다. Binance는 2019년 이후 USDⓈ-M 무기한 선물을 600개 넘게 상장했고, 그중 100개 훨씬 넘는 종목을 폐지했습니다. BTS, COCOS, TOMO, RAY, FTT, SC, 그리고 2021년 알트코인 시즌에 한 분기 눈부시게 오른 뒤 거래가 뜸해져 거래소가 폐지한 수많은 종목이 있습니다.
30일 모멘텀 스크리너가 어떤 종목을 좋아하는지 생각해 보세요. BTC가 아닙니다. 상장 펌프와 트위터 열풍으로 막 3배 뛴 종목을 좋아하죠. 그런 종목군과 결국 상장 폐지되는 종목군은 크게 겹치는데, 유니버스 필터가 그 겹치는 종목을 제거해 버렸습니다.
스냅샷 아카이브를 이용해 시점별 유니버스로 같은 시그널과 비용을 적용해 다시 돌려 봤습니다. Sharpe는 0.74, 낙폭은 31%였습니다. 차이의 대략 5분의 2는 애초에 보유할 수 없었던 상장 폐지 종목에서 나옵니다. 또 4분의 1은 정반대 방향의 문제에서 비롯됩니다. 좀 더 미묘하고, 아마 마음에 덜 드실 문제죠.
타임머신의 반대편: 아직 존재하지 않았던 심볼
특징값은 90일 수익률입니다. 롤링 윈도에는 min_periods=20이 설정돼 있습니다. 워밍업 때문에 샘플 첫 분기가 날아가는 걸 막으려고 한 번 설정한 뒤 다시 살펴보지 않았기 때문이죠. 그래서 21일 전에 상장된 계약도 상장 후 3주간의 가격 움직임만으로 모멘텀 점수를 계산하고, 상장이 잘된 경우 대체로 상위 10분위에 들어가 포트폴리오에 편입됩니다.
그 부분은 정당하다고 볼 여지도 있습니다. 정당하지 않은 건 이겁니다. 데이터 제공업체가 일부 심볼의 무기한 선물 출시 전 데이터를 현물이나 지수 데이터로 백필해, 몇몇 계약의 캔들 기록이 자체 onboardDate보다 앞섭니다. 1분 정도면 확인할 수 있습니다. 가격 테이블을 상장일과 조인하고 상장 이전 행이 몇 개인지 세어 보세요. 데이터에 상장 전 봉이 있는 심볼은 41개입니다. 그중 하나는 2023년 여름에, 아직 9일 뒤에나 생길 계약의 포지션으로 주간 수익률 6%를 내 계좌 곡선에 더했습니다.
티커 문자열은 식별자가 아닙니다. 거래소가 빌려주는 라벨이고, 때로는 같은 라벨을 두 번 빌려줍니다.
그러면 이름 변경 문제가 생깁니다. MATICUSDT는 POLUSDT가 됐습니다. FTMUSDT는 1:1 전환을 거쳐 SUSDT가 됐습니다. 2022년 5월 LUNA 사태로 LUNCUSDT가 남았고, 이후 새 LUNAUSDT가 생겼습니다. 티커의 어근은 같지만 기존 종목은 사실상 모든 가치를 잃었죠. 로더가 심볼 문자열을 키로 삼아 찾은 파일을 전부 이어 붙인다면, 가격 변동이 아닌 단절이 포함된 시계열이 적어도 하나 있습니다. 모멘텀 특징값은 그 단절을 횡단면에서 가장 강한 시그널로 읽게 됩니다.
그 밖에 계속 바뀌는 값들
심볼 목록이 시간에 따라 달라진다는 점을 받아들이면, 같은 논리가 응답의 다른 모든 필드에도 적용됩니다. 지금은 모든 필드에 오늘의 값을 쓰고 계십니다.
| 필드 | 어떻게 바뀌는가 | 무엇이 깨지는가 |
|---|---|---|
| status | TRADING → SETTLING → 삭제 | 생존 편향; 실제로 거래된 적 없는 종가에 유령 청산 처리 |
| onboardDate | 매주 새 종목 상장; 폐지된 종목에는 값이 없음 | 존재하기 전 계약을 거래하게 됨 |
| tickSize / stepSize | 가격대가 달라지며 조정됨 | 주문 반올림, 거래소에서 거부됐을 지정가 |
| minNotional | 유동성 얕은 오더북에서 시간이 지나며 상향 조정 | 실시간 주문 라우터가 거부할 소액 레그 |
| fundingIntervalHours | 수년간 8h였지만 이후 많은 심볼에서 4h 또는 1h로 변경 | 스크리너가 보유하는 바로 그 알트코인에서 캐리가 2–3배 어긋남 |
| 레버리지 구간 | 구간과 유지 증거금이 개정됨 | 청산 모델링과 증거금 여력 |
이번 경우 가장 큰 문제는 펀딩입니다. 누적 루프는 전체 기간에 하루 3회 결제가 있었다고 가정합니다. 알트코인 포트폴리오의 상당 부분은 4시간 펀딩으로 바뀌었고, 고펀딩 종목의 매도 레그에서 시뮬레이션 PnL이 많이 나옵니다. 반올림 오차 정도로 틀린 게 아닙니다. 배수 단위로 틀렸습니다.
상장 폐지는 하나의 이벤트인데, 이를 모델링하지 않고 있습니다
시점별 데이터로 다시 돌릴 때 전략에 넉넉한 청산 조건을 줬습니다. 실제 상장 폐지는 정해진 절차를 따릅니다. 공지 후 보통 7~14일이 지나면 reduce-only 기간이 시작되고, 그다음 마크 가격으로 강제 결제됩니다. 공지는 공개 정보이므로 대응할 수 있습니다. 따라서 타당한 시뮬레이션이라면 공지 당일 종가로 청산해야 합니다. 하지만 그 종가는 보통 알트코인 상장 폐지 전 주보다 이미 10-20% 낮고, 오더북은 얕습니다. 슬리피지 항도 다른 시장 국면에서 작동한다는 점을 반영해야 합니다. 체결 엔진이 시장 충격 없이 결제 가격에 체결해 준다면, 가치가 사라지는 자산을 조용히 공정 가치에 거래할 수 있게 만든 셈입니다.
exchangeInfo를 아카이브하지 않았어도 아직 늦지 않았습니다. 공개 덤프 data.binance.vision/data/futures/um/monthly/klines/에는 API가 잊어버린 뒤에도 상장 폐지 심볼의 디렉터리가 남아 있습니다. 디렉터리 목록을 확인하고 심볼별 첫 번째와 마지막 월간 파일을 찾으면, 별도 데이터 제공업체 없이도 실용적인 상장·폐지 기간을 추정할 수 있습니다. 기록 자체가 아니라 재구성이므로 tick size나 펀딩 간격까지 복원되지는 않습니다. 그래도 월요일까지 필요한 것의 80%인, 각 종목이 언제 존재했는지는 알 수 있습니다.
시그널을 다시 건드리기 전에 만들어 둘 것
- 조사하는 모든 거래소에서 매일 exchangeInfo를 가져와 날짜를 키로 오브젝트 스토리지에 저장하는 cron. gzip 압축 시 0.5메가바이트 미만입니다. 10년치를 S3에 저장해도 반올림 오차 수준의 비용이면 되고, 나중에는 살 수 없는 리서치 역량을 확보하게 됩니다.
- 스냅샷에서 파생한 자산 마스터: 전체 필드를 포함하고 (venue, symbol, valid_from, valid_to)마다 한 행씩 저장합니다. 연속된 스냅샷을 비교해 생성하고, 필드가 바뀔 때마다 새 행으로 기록하세요.
- 필수 타임스탬프 인수를 받는 유니버스 함수.
universe(ts), 절대로universe()는 안 됩니다. 기본값으로 생존 종목 목록을 실수로 선택할 수 없게 만들어 두세요. 새벽 1시의 미래의 자신도 예외가 아닙니다. - 데이터 로더에 상장 전 검증을 넣으세요. 봉은
onboard_ts보다 하루 앞선 시점 이전에 존재할 수 없습니다. 경고만 띄우지 말고 실행을 실패 처리하세요. - 이름 변경 후에도 유지되는 안정적인 내부 상품 ID를 만들고, 티커는 속성으로 격하하세요. POL과 MATIC을 하나의 ID에 매핑하고, 액면 변경을 표시해 연속성 로직이 해당 구간을 연결하지 않게 하세요.
이걸 적용하고 다시 돌려 보세요. 제가 얻은 0.74 근처가 나올 것 같습니다. 그러면 사라진 종목까지 반영한 0.74에 페이퍼 트레이딩을 할 만한 게 남아 있는지가 흥미로운 질문이 되겠죠. 있을 수도 있습니다. 무기한 선물의 횡단면 모멘텀은 무의미한 전략이 아니고, 유니버스를 바로잡은 뒤에도 남는 수익 중 일부는 매도 측의 실질적인 캐리입니다. 페이퍼 트레이딩은 처음부터 시점별 유니버스로 돌아갔으니, 페이퍼 결과와 백테스트도 서로 맞아가기 시작할 겁니다. 다른 선택지가 없었으니까요.
추신. 이건 암호화폐만의 특이한 문제가 아니라, 여기서 더 두드러질 뿐입니다. 주식 리서처들은 CRSP가 데이터를 제공하기 시작한 이래 상장 폐지 수익률과 재활용 티커 문제를 다뤄 왔습니다. 예측 시장은 이를 극단으로 끌고 갑니다. 모든 계약이 정해진 대로 만료되므로, 유니버스는 상장과 종료로만 이뤄집니다. 언젠가 이 스크리너를 Kalshi로 옮긴다면 자산 마스터부터 만드세요. 그곳에는 다른 형태의 과거 데이터가 없습니다.
← 전체 글

