Samme commit, samme parquet-filer, samme maskine, to kørsler med en times mellemrum. Sharpe 1.34 og Sharpe 1.19. Samlet afkast 41.2% og 37.8%. Antal handler 1,418 og 1,421. Og de to egenkapitalkurver var enige om hvert eneste viste decimal i 11,205 bars i træk, før de skiltes ad.
De første tre tal er irriterende. Det sidste er interessant, for det viser, at problemet ikke er upræcis floating point, der har spredt sig gennem hele kørslen. Noget diskret skete på én bestemt bar, og resten skyldtes den forstærkende effekt. Dette indlæg handler om at finde den bar og om, hvad størrelsen på de 0.15 betyder for hver eneste parametersweep, du nogensinde har kørt.
Strategien: tværsnitsmomentum på de 30 USDⓈ-M-perps med størst volumen, rebalancering hver 4. time, long på de fem øverste målt på 12-timers afkast, short på de fem nederste, 18 måneders historik, gebyrer og funding opkrævet pr. ben.
Sådan fandt vi baren, hvor kørslerne skiltes ad
Hvis du logger egenkapitalen pr. bar med fuld præcision, tager det cirka fire minutter. Skriv begge kørsler til en CSV med (bar_index, equity, open_positions_hash), indlæs dem begge, og find det første indeks, hvor de er uenige. Vi havde allerede skrevet det script til en anden fejl, og det er den eneste grund til, at jeg ikke brugte en hel formiddag på det.
Bar 11,206, 2025-03-14T08:00 UTC. Egenkapitalen var identisk på den foregående bar med 13 betydende cifre. På bar 11,206 er positionshashene forskellige: kørslen A er long SOL, kørslen B er long AVAX. Samme side, samme notional, forskelligt symbol. Alt derefter følger af det.
Så udskrev jeg rangeringsinputtene for den bar fra begge kørsler. De var identiske. Byte for byte identiske, de samme 30 symboler, de samme 30 scorer. To af scorerne var 0.0. Præcis nul, begge to, fordi begge symboler havde haft en 4-timers bar uden handler i lookback-vinduet, så slutkurs divideret med slutkurs blev én, og logaritmen blev nul. Ikke afrundet til nul. Nul.
To symboler delte femtepladsen. Udvælgelsen tog de fem øverste. Hvilket af de to der kom med, afhang af den rækkefølge, sorteringen efterlod dem i, og sorteringen gjorde ikke det, jeg havde regnet med.
Et lighedstilfælde, en ustabil sortering og noget, jeg havde ignoreret i 2 år
Rangeringen brugte en grupperet aggregering, og datarammen, der blev sendt ind, kom fra en dict comprehension over et sæt symboler, som blev genskabt for hver bar ud fra et asynkront fetch. Sættets iterationsrækkefølge ændrer sig med hash-seedet, og Python randomiserer strengens hash-seed for hver proces, medmindre du fastlåser PYTHONHASHSEED. Derfor var rækkefølgen af rækkerne før sorteringen forskellig mellem kørslerne, og når en ustabil sortering får en nøgle med lighedstilfælde, bliver ligheden brudt forskelligt.
Lighedstilfælde som dette er ikke sjældne undtagelser. De følger af strukturen. Når som helst en feature mættes eller klippes, skaber du præcis lighed: bars med nul volumen giver afkast på præcis nul, en klippet z-score låses fast på ±3.0, en rangtransformation med få forskellige værdier giver dusinvis af lighedstilfælde, og et boolsk filter giver alt, der slipper igennem, scoren 1.0. I løbet af 18 måneder med 4-timers bars havde denne kørsel 47 bars med et lighedstilfælde ved udvælgelsesgrænsen. Tre af dem ændrede den valgte kurv. De øvrige lå mellem to symboler, som begge allerede var med eller begge var ude.
I cirka en dag var jeg overbevist om, at dataloaderen var ikke-deterministisk, fordi det er det spændende svar. Det var den ikke. Det er det aldrig. Det er et sæt, et lighedstilfælde og en antagelse om sorteringsstabilitet, som ingen skrev ned.
Hvorfor 3 handler kan være 0.15 Sharpe værd
Det er her, folk protesterer, og svaret er, at en backtest med positionsstørrelse baseret på en andel af egenkapitalen er et system, der afhænger af forløbet. Når positionsstørrelsen er 8% af den aktuelle egenkapital pr. ben, betyder en forskel i egenkapital på bar n en forskel i hver eneste notional fra bar n og frem.
Den første afvigelse kostede ikke meget i sig selv. AVAX-positionen i kørslen B tabte 2.1% over 9 timer; SOL-positionen i kørslen A steg 0.4%. Forskel i egenkapital efter den handel: 0.21%. Ubetydeligt. Men fra da af følger de to kørsler ikke længere den samme strategi. De har lidt forskellige positionsstørrelser, så deres fundingløb adskiller sig en smule, og yderligere 2 lighedstilfælde ved grænsen blev brudt forskelligt, fordi scorerne nu byggede på positioner, der var marginalt forskellige. Det ene indtraf 2025-03-27, dagen før en 6-dages trend, som skabte omtrent en tredjedel af kørslernes samlede PnL. Kørslen A var med under hele bevægelsen; kørslen B gik ind én rebalancering senere.
Forskel i afkast: 3.4 procentpoint. Sharpe-forskellen er større, end forskellen i afkast antyder, fordi kørslen B's ombyttede handler overlappede en mere volatil periode, så nævneren steg, mens tælleren faldt. Lille årsag, to forstærkende faktorer.
Hvis din positionsstørrelse er fast notional, og dine indgange ikke afhænger af aktuelle positioner, er du langt bedre beskyttet. De fleste interessante strategier er ingen af delene.
De 5 steder, hvor det faktisk opstår
| Kilde | Symptom | Løsning |
|---|---|---|
Ikke-fastlåst PYTHONHASHSEED med iteration over sæt/dict, der påvirker sorteringen | Lighedsbrud skifter mellem kørsler; første afvigelse på en bestemt bar | Fastlås seedet; sortér efter en eksplicit sekundær nøgle (symbol), så lighedstilfælde behandles deterministisk |
Ustabil sortering af en nøgle med lighedstilfælde (quicksort standard i NumPy/pandas) | Samme som ovenfor, består selv når seedet er fastlåst | kind="stable", eller gør nøglen entydig |
| Ikke-seedet RNG i bootstrap, train/test-blandinger eller syntetisk fyldjitter | Afvigelser gennem hele kørslen uden et tydeligt afvigelsespunkt | Ét eksplicit seed pr. komponent, logget i kørselsmanifestet |
| Parallel flydende-punktsreduktion (summeringsrækkefølge afhænger af antal tråde) | Forskelle i de sidste få bit, som regel harmløse, indtil de påvirker en tærskelsammenligning | Fastlås antal tråde i researchkørsler; sammenlign aldrig floats med == ved en beslutningsgrænse |
| Ikke-fastlåste biblioteksversioner | Reproducerbar i dag, ikke i november | Hash af lockfilen i manifestet sammen med hash af datasnapshot |
Det fjerde punkt betyder sjældnere noget, end folk frygter, og det første giver konstant problemer.
Bitreproducerbarhed er et værktøj, ikke en dyd
Du har brug for determinisme, så forskellen i egenkapitalkurven kan tilskrives den ene linje, når du ændrer den. Det er hele formålet. Hver agentkørsel på Stratmill skriver nu et manifest med hash af datasnapshot, hash af lockfilen og hvert seed, og en genkørsel, der ikke gengiver den tidligere kurve bit for bit, er en mislykket build, ikke bare noget kuriøst.
Men når du først kan reproducere resultatet, så bryd det med vilje. Kør det 64 gange med 64 seeds, og se på spredningen:
Jitterbåndet. Samme strategi, samme data, 64 seedede permutationer af lighedsbrud og fyldrækkefølge. Sharpe p5 1.12, median 1.27, p95 1.41. Båndbredde 0.29.
Gå nu tilbage til parametersweepet. Den bedste konfiguration fik 1.46. Konfigurationen som lå som nr. 40 ud af 96, fik 1.31. Forskellen mellem dem er 0.15, hvilket er halvdelen af båndet. Sweepet rangerede ikke de to konfigurationer. Det trak én stikprøve fra hver af deres fordelinger og sorterede stikprøverne.
Det ændrede vores måde at vælge på. Et sweepresultat er kun en rangering, hvis forskellene mellem konfigurationerne overstiger jitteren for én enkelt konfiguration, og i en strategi, der afhænger af forløbet, med 1,400 handler er jitteren som regel stor nok til at flade den øverste tredjedel af ranglisten ud til én stor delt placering. Når det sker, så vælg ud fra noget, båndet ikke kan skjule: lavere omsætning, færre parametre, en omkostningsantagelse, du kan forsvare over for en skeptiker, eller bedre adfærd i den walk-forward-fold, du bedst kan lide. Det er reelle kriterier for at bryde ligheden. En Sharpe-forskel på 0.15 er det ikke.
Der er én ting mere, som er værd at gøre, før du stoler på noget af det. Kør din backtest to gange lige nu, sammenlign egenkapitalen bar for bar, og find ud af, om du er i den bit-identiske gruppe eller 0.15-gruppen. Det er et eksperiment på et kvarter, og det fortæller dig, hvor stor en del af din forskning historisk set målte strategien og hvor stor en del, der målte et hash-seed.
← Alle indlæg


