27. august 2026 · datateknik

Dit symbolunivers er en tidsmaskine: survivorship bias i backtests af kryptoperpetuals

Dit symbolunivers er en tidsmaskine: survivorship bias i backtests af kryptoperpetuals

Du sendte mig notebooken søndag aften, og jeg har haft den åben på den anden skærm lige siden. Cross-sectionel momentum, de 150 Binance USDⓈ-M-perps med størst dollarvolumen over 30 dage, ugentlig rebalancering, long i den øverste decil og short i den nederste, 2021-01 til og med 2026-06. Sharpe 2.31, maksimalt drawdown på 14.2 % og en omkostningsmodel, der så realistisk ud for mig: taker ind, taker ud, funding optjent pr. interval og et slippage-led, der skalerer med din størrelse i forhold til ordrebogen. Du gjorde de svære ting rigtigt. Så spurgte du, hvorfor seks ugers paper trading er endt i nul, og om edge forsvandt.

Den forsvandt ikke. Den var aldrig med i backtesten. Se på celle 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"]

Du kaldte det endpoint i juni 2026 og brugte svaret til at definere, hvad der kunne handles i marts 2021. Alle symbolerne på listen overlevede frem til juni 2026, per konstruktion. Det er hele fejlen, og den betyder mere end resten af din omkostningsmodel tilsammen.

2.31 → 0.74Sharpe, overleverliste vs. point-in-time
~1 ud af 5af symbolerne i dit 2021-univers findes ikke længere
380 KBet dagligt exchangeInfo-snapshot, gzippet

Hvad endpointet ikke fortæller dig

Der er ingen historik i exchangeInfo. Ingen asOf-parameter, intet arkiv, ingen ændringslog. Det er et øjebliksbillede af lige nu, og Binance har aldrig lovet dig andet. Når en kontrakt afnoteres, slettes dens post fra svaret, og tickeren holder op med at eksistere, så vidt API'et angår. Binance har noteret over 600 USDⓈ-M-perpetuals siden 2019 og trukket langt over hundrede af dem tilbage. BTS, COCOS, TOMO, RAY, FTT, SC og en lang hale af kontrakter fra altcoin-sæsonen i 2021, der havde ét glorværdigt kvartal og derefter svandt ind, indtil børsen fjernede dem.

Spørg dig selv, hvilke navne et 30-dages momentumfilter elsker. Ikke BTC. Det elsker det, der lige er tredoblet på et noteringsrally og en Twitter-bølge. De to grupper overlapper kraftigt: de aktiver, filtret tiltrækkes af, og de aktiver, der til sidst bliver afnoteret. Dit universfilter fjernede overlapningen.

Jeg genkørte din strategi mod vores snapshotarkiv med et point-in-time-univers, samme signal og samme omkostninger. Sharpe endte på 0.74 med et drawdown på 31 %. Omtrent to femtedele af forskellen skyldes afnoterede navne, som du aldrig måtte have i porteføljen. Endnu en fjerdedel skyldes det omvendte problem, som er mere subtilt, og som jeg tror, du vil bryde dig mindre om.

Den anden ende af tidsmaskinen: symboler, der endnu ikke fandtes

Din feature er et 90-dages afkast. Dit rullende vindue har min_periods=20, fordi du satte det én gang for at undgå, at opvarmningsperioden ødelagde prøvens første kvartal, og aldrig så på det igen. Så får en kontrakt, der blev noteret for 21 dage siden, en momentumscore beregnet ud fra tre ugers prisudvikling efter noteringen. Den placerer sig i den øverste decil, når noteringen gik godt, og kommer med i din portefølje.

Den del er sådan set legitim. Her er det, der ikke er: Din dataleverandør har tilbagedateret spot- eller indeksdata for nogle af symbolerne til tiden før perp-kontrakten fandtes, så en håndfuld kontrakter har kline-historik, der går forud for deres egen onboardDate. Du kan tjekke det på omkring et minut. Join din pristabel med noteringsdatoerne, og tæl rækkerne før noteringen. I dine data har 41 symboler bars fra før noteringen, og ét af dem bidrager i sommeren 2023 med en gevinst på 6 % på en enkelt uge til aktiekurven, fra en position i en kontrakt, der først ville blive oprettet ni dage senere.

En tickers streng er ikke en identifikator. Det er en etiket, børsen udlejer, nogle gange to gange.

Det fører os til navneændringer. MATICUSDT blev til POLUSDT. FTMUSDT blev til SUSDT ved en 1:1-konvertering. LUNA-situationen i maj 2022 efterlod en LUNCUSDT og senere en helt ny LUNAUSDT, der deler tickerrod med noget, som mistede mere eller mindre alt. Hvis din loader bruger symbolstrengen som nøgle og sammenkæder alle de filer, den finder, har du mindst én serie med et diskontinuitet, der ikke skyldes en prisbevægelse. Din momentumfeature vil læse det som det stærkeste signal i tværsnittet.

Alt det andet, der ændrer sig under dig

Når du først accepterer, at symbollisten ændrer sig over tid, gælder samme argument for alle andre felter i svaret. Du bruger dagens værdier for dem alle.

FeltHvordan det ændrer sigHvad det ødelægger
statusTRADING → SETTLING → vækSurvivorship; fiktive exits til en lukkekurs, der aldrig blev handlet
onboardDateNye noteringer hver uge; mangler for døde symbolerHandel med kontrakter, før de fandtes
tickSize / stepSizeJusteres, når prisniveauerne ændrer sigAfrunding af ordrer og limitpriser, der ville være blevet afvist
minNotionalHæves over tid på illikvide ordrebøgerSmå positioner, som din live-router ville afvise
fundingIntervalHours8h i årevis, derefter 4h eller 1h for mange symbolerCarry bliver skæv med en faktor på 2–3× præcis på de altcoins, dit filter holder
leverage bracketsNiveauer og vedligeholdelsesmargin revideresLikvidationsmodellering og marginkapacitet

Funding-problemet gør mest ondt i dit tilfælde. Din optjeningsløkke antager tre betalinger om dagen gennem hele historikken. En betydelig del af altcoin-porteføljen gik over til funding hver fjerde time, og det er shortpositioner i navne med høj funding, der står for en stor del af din simulerede PnL. Det er ikke en afrundingsfejl. Det er en faktorfejl.

Selve afnoteringen er en hændelse, og du modellerer den ikke

I din point-in-time-genkørsel gav jeg strategien en gavmild exit. I virkeligheden følger afnoteringer et fast forløb: en meddelelse, som regel syv til fjorten dage i forvejen, derefter en periode med kun reduktion af positioner og så tvungen afregning til en markpris. Meddelelsen er offentlig information, og du kan reagere på den, så den realistiske simulering er et exit ved luk på meddelelsesdagen. Men den lukkekurs ligger allerede typisk 10-20 % under den foregående uge ved en altcoin-afnotering, ordrebogen er tynd, og din slippage-model skal vide, at den arbejder i et andet regime. Hvis din fill-motor giver dig afregningsprisen uden markedspåvirkning, har du lydløst gjort døende aktiver omsættelige til dagsværdi.

Hvis du aldrig arkiverede exchangeInfo, er du ikke fortabt. Det offentlige dump på data.binance.vision/data/futures/um/monthly/klines/ gemmer stadig mapper for afnoterede symboler længe efter, at API'et har glemt dem. Gennemgå mapperne; den første og sidste månedsfil for hvert symbol giver dig et brugbart interval for notering og afnotering uden nogen dataleverandør. Det er en rekonstruktion, ikke en registrering, og den genskaber ikke tickstørrelser eller fundingintervaller. Men den fortæller dig, hvad der fandtes hvornår, og det dækker 80 % af det, du skal bruge på mandag.

Det her ville jeg bygge, før du rører signalet igen

  1. Et cron-job, der dagligt henter exchangeInfo fra alle de børser, du undersøger, og skriver det til objektlager sorteret efter dato. Gzippet fylder det under en halv megabyte. Ti år koster dig en afrundingsfejl i S3 og giver dig en forskningsmulighed, du ikke kan købe dig til senere.
  2. Et aktivregister udledt af snapshots: én række pr. (venue, symbol, valid_from, valid_to) med hele feltsættet. Sammenlign fortløbende snapshots for at generere det, og behandl enhver feltændring som en ny række.
  3. En universfunktion med et obligatorisk tidsstempelargument. universe(ts), aldrig universe(). Gør standarden umulig, så ingen, heller ikke dig selv i fremtiden kl. 1, ved et uheld kan vælge overleverlisten.
  4. En kontrol for data før noteringen i dataloaderen: ingen bar må findes før onboard_ts minus én dag. Stop kørslen; nøjes ikke med en advarsel.
  5. Et stabilt internt instrument-ID, der overlever navneændringer, mens tickeren bliver en attribut. Knyt POL og MATIC til samme ID, og markér redenomineringer, så kontinuitetslogikken kan nægte at sammenkæde dem.

Gør det, og kør igen. Mit gæt er, at du lander tæt på de 0.74, jeg fik. Så bliver det interessante spørgsmål, om en 0.74, der medregner de døde navne, rummer noget, der er værd at paper trade. Måske. Cross-sectionel momentum i perps er ikke værdiløs, og en del af det, der er tilbage, når du har rettet universet, er reel carry fra short-siden. Du vil også se, at dine paper-resultater og din backtest begynder at stemme overens, fordi paper trading altid har kørt med et point-in-time-univers. Den havde ikke noget valg.

P.S. Det her er ikke et særtilfælde for krypto, det er bare tydeligere her. Aktieforskere har kæmpet med afnoteringsafkast og genbrugte tickere, siden CRSP begyndte at levere dem, og forudsigelsesmarkeder går hele vejen: Alle kontrakter udløber med vilje, så universet består kun af noteringer og afnoteringer. Hvis du en dag overfører dette filter til Kalshi, så byg aktivregistret først. Der findes ikke andre former for historik dér.

survivorship biaspoint-in-time-datakryptofuturesbacktestingdatateknik
← Alle indlæg