Mi-ai trimis notebookul duminică seara și îl am deschis pe al doilea monitor de atunci. Momentum cross-sectional, primele 150 de contracte perpetue Binance USDⓈ-M după volumul în dolari pe 30 de zile, rebalansare săptămânală, long pe decila superioară și short pe cea inferioară, 2021-01 până în 2026-06. Sharpe 2.31, drawdown maxim 14.2%, și un model de costuri care mi s-a părut realist: taker la intrare, taker la ieșire, funding acumulat pe interval, un termen de slippage care crește odată cu dimensiunea poziției tale în raport cu lichiditatea din order book. Ai făcut bine lucrurile dificile. Apoi m-ai întrebat de ce șase săptămâni de paper trading au ieșit plate și dacă s-a erodat avantajul.
Nu s-a erodat. N-a fost niciodată în backtest. Uită-te la celula 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"]
Ai apelat acel endpoint în iunie 2026 și ai folosit răspunsul ca să stabilești ce putea fi tranzacționat în martie 2021. Prin construcție, toate simbolurile de pe acea listă supraviețuiseră până în iunie 2026. Asta e toată problema și valorează mai mult decât restul modelului tău de costuri la un loc.
Ce nu-ți va spune endpointul
exchangeInfo nu conține istoric. Niciun parametru asOf, nicio arhivă, niciun jurnal de modificări. E o fotografie a momentului prezent, iar Binance nu ți-a promis niciodată altceva. Când un contract e delistat, intrarea lui e ștearsă din răspuns, iar tickerul încetează să existe din perspectiva API-ului. Binance a listat peste 600 de contracte perpetue USDⓈ-M din 2019 încoace și a retras mult peste o sută dintre ele. BTS, COCOS, TOMO, RAY, FTT, SC, o coadă lungă de contracte din sezonul altcoinurilor din 2021, care au avut un trimestru de glorie, apoi s-au împuținat până când platforma le-a retras.
Întreabă-te ce active adoră un filtru de momentum pe 30 de zile. Nu BTC. Îi place activul care tocmai s-a triplat pe fondul unui pump la listare și al unui ciclu pe Twitter. Aceste active se suprapun masiv cu cele care ajung să fie delistate, iar filtrul tău de univers a eliminat tocmai această suprapunere.
Am refăcut rularea ta folosind arhiva noastră de instantanee, univers point-in-time, același semnal, aceleași costuri, iar Sharpe a ieșit 0.74, cu un drawdown de 31%. Aproximativ două cincimi din diferență vin de la simbolurile delistate pe care nu ar fi trebuit să le poți deține. Încă un sfert vine din problema opusă, mai subtilă, și bănuiesc că o să-ți placă și mai puțin.
Celălalt capăt al mașinii timpului: simboluri care încă nu existau
Caracteristica ta e randamentul pe 90 de zile. Fereastra mobilă are min_periods=20, fiindcă ai setat-o o dată ca să nu-ți anuleze încălzirea primul trimestru al eșantionului și n-ai mai revenit asupra ei. Așa că un contract listat acum 21 de zile primește un scor de momentum calculat din trei săptămâni de evoluție a prețului de după listare, ajunge în decila superioară cam de fiecare dată când listarea a mers bine și intră în portofoliul tău.
Partea asta poate fi considerată legitimă. Iată ce nu e legitim: furnizorul tău a completat retroactiv istoricul unor simboluri cu date spot sau de index de dinainte ca perp-ul să existe, așa că istoricul kline al câtorva contracte începe înainte de propriul lor onboardDate. Poți verifica asta în aproximativ un minut. Unește tabelul de prețuri cu datele listării și numără rândurile dinaintea listării. În datele tale sunt 41 de simboluri cu bare dinainte de listare, iar unul dintre ele, în vara lui 2023, contribuie cu un câștig de 6% într-o singură săptămână la curba de capital, datorită unei poziții într-un contract care urma să apară abia peste nouă zile.
Un ticker nu este un identificator. E o etichetă pe care exchange-ul o închiriază, uneori de două ori.
Asta ne aduce la redenumiri. MATICUSDT a devenit POLUSDT. FTMUSDT a devenit SUSDT printr-o conversie 1:1. Situația LUNA din mai 2022 a lăsat în urmă un LUNCUSDT și, mai târziu, un LUNAUSDT complet nou, care are aceeași rădăcină de ticker ca un activ care și-a pierdut aproape toată valoarea. Dacă loaderul tău folosește șirul simbolului drept cheie și concatenează toate fișierele găsite, ai cel puțin o serie cu o discontinuitate care nu e o mișcare de preț, iar caracteristica ta de momentum va interpreta acea discontinuitate drept cel mai puternic semnal din secțiunea transversală.
Tot restul care se schimbă pe nesimțite
După ce accepți că lista de simboluri se schimbă în timp, același argument se aplică tuturor celorlalte câmpuri din acel răspuns. Folosești valorile de azi pentru toate.
| Câmp | Cum se schimbă | Ce se strică |
|---|---|---|
| status | TRADING → SETTLING → dispărut | Bias de supraviețuire; ieșiri fantomă la un preț de închidere la care nu s-a tranzacționat niciodată |
| onboardDate | Listări noi în fiecare săptămână; absent pentru simbolurile dispărute | Tranzacționarea contractelor înainte să existe |
| tickSize / stepSize | Ajustate odată cu schimbarea nivelurilor de preț | Rotunjirea ordinelor și prețuri-limită care ar fi fost respinse |
| minNotional | Mărit în timp pentru order book-uri cu lichiditate redusă | Poziții mici pe care routerul tău live le-ar refuza |
| fundingIntervalHours | 8h ani la rând, apoi 4h sau 1h pentru multe simboluri | Costul de carry calculat greșit de 2–3× tocmai la altcoinurile pe care le ține filtrul tău |
| leverage brackets | Praguri și marja de mentenanță revizuite | Modelarea lichidărilor și capacitatea de marjă |
Problema fundingului te afectează cel mai mult în cazul ăsta. Bucla ta de acumulare presupune trei plăți pe zi pentru tot istoricul. O parte semnificativă a portofoliului de altcoinuri a trecut la funding la fiecare patru ore, iar pozițiile short pe active cu funding ridicat sunt sursa unei bune părți din PnL-ul simulat. Nu ești pe lângă cu o eroare de rotunjire. Eroarea e de un factor întreg.
Delistarea e un eveniment, iar tu nu-l modelezi
În rerularea ta point-in-time i-am oferit strategiei o ieșire generoasă. Delistările reale urmează un scenariu: un anunț, de obicei cu șapte până la paisprezece zile înainte, apoi o perioadă în care ordinele pot doar să reducă pozițiile, apoi decontare forțată la un preț mark. Anunțul este informație publică și poți acționa în consecință, așa că simularea corectă presupune ieșirea la închiderea zilei anunțului. Dar în mod obișnuit, la acel moment prețul este deja cu 10-20% sub nivelul săptămânii precedente, order book-ul are lichiditate redusă, iar termenul tău de slippage trebuie să țină cont că operează într-un regim diferit. Dacă motorul tău de execuție îți oferă prețul de decontare fără impact de piață, ai făcut pe nesimțite activele pe cale de dispariție tranzacționabile la valoarea justă.
Dacă n-ai arhivat niciodată exchangeInfo, încă nu ești în impas. Dumpul public de la data.binance.vision/data/futures/um/monthly/klines/ păstrează directoare pentru simbolurile delistate mult după ce API-ul le-a uitat. Enumeră directoarele, iar primul și ultimul fișier lunar pentru fiecare simbol îți oferă un interval utilizabil pentru listare și delistare, fără niciun furnizor de date. E o reconstrucție, nu o evidență, și nu-ți va recupera tick size-urile sau intervalele de funding. Dar îți va spune ce simboluri existau și când, adică 80% din ce-ți trebuie până luni.
Ce te-aș pune să construiești înainte să te atingi din nou de semnal
- Un job cron care preia zilnic exchangeInfo de la fiecare platformă pe care o cercetezi și îl scrie în object storage, cu data drept cheie. Comprimat cu gzip, ocupă mai puțin de jumătate de megabyte. Zece ani te costă cât o eroare de rotunjire în S3 și îți oferă o capacitate de cercetare pe care n-o vei mai putea cumpăra ulterior.
- Un registru de active derivat din acele instantanee: un rând pentru fiecare (venue, symbol, valid_from, valid_to), cu setul complet de câmpuri. Compară instantanee consecutive pentru a-l genera și tratează orice schimbare de câmp ca pe un rând nou.
- O funcție de univers cu un argument pentru marcaj temporal obligatoriu.
universe(ts), niciodatăuniverse(). Fă imposibilă folosirea valorii implicite, ca nimeni, inclusiv tu pe viitor la 1am, să nu poată apela din greșeală lista supraviețuitorilor. - O aserțiune în loaderul de date: nicio bară nu poate exista înainte de
onboard_tsminus o zi. Oprește rularea, nu afișa doar un avertisment. - Un ID intern stabil pentru instrument, care supraviețuiește redenumirilor, iar tickerul să fie doar un atribut. Mapează POL și MATIC la același ID și marchează redenumirile de valoare nominală astfel încât logica de continuitate să poată refuza unirea seriilor.
Fă astea și rulează din nou. Presupun că ajungi aproape de 0.74 cât am obținut eu, iar întrebarea interesantă devine dacă un 0.74 care include simbolurile dispărute conține ceva ce merită testat în paper trading. E posibil. Momentum cross-sectional pe perps nu e de neglijat, iar o parte din ce rămâne după ce repari universul e carry autentic din partea short. Vei vedea și că rezultatele din paper trading încep să se alinieze cu backtestul, fiindcă paper tradingul a folosit întotdeauna un univers point-in-time. N-a avut de ales.
P.S. Nu e o ciudățenie specifică crypto, doar că aici se vede mai tare. Cercetătorii de acțiuni se luptă cu randamentele delistărilor și ticker-ele reutilizate încă de când CRSP a început să le distribuie, iar piețele de predicție duc lucrurile la extrem: fiecare contract expiră prin definiție, deci universul e alcătuit numai din listări și dispariții. Dacă vei adapta vreodată filtrul ăsta pentru Kalshi, construiește mai întâi registrul de active. Acolo nu există alt fel de istoric.
← Toate articolele


