Parehong commit, parehong parquet file, parehong makina, dalawang run na isang oras ang pagitan. Sharpe na 1.34 at Sharpe na 1.19. Kabuuang return na 41.2% at 37.8%. Bilang ng trade na 1,418 at 1,421. At magkapareho ang dalawang equity curve hanggang sa bawat inilimbag na decimal sa loob ng 11,205 sunod-sunod na bar bago nagkahiwalay.
Nakakainis ang unang tatlong numero. Ang huli ang interesante, dahil ipinapakita nitong hindi kalat-kalat na floating-point error sa buong run ang problema. May nangyaring discrete sa isang partikular na bar, at lumaki ang epekto nito sa mga sumunod na bar. Tungkol ang post na ito sa paghahanap sa bar na iyon, at sa epekto ng 0.15 na agwat na iyon sa bawat parameter sweep na pinatakbo mo.
Ang strategy: cross-sectional momentum sa 30 perps na may pinakamataas na volume sa USDⓈ-M, rebalance kada 4 na oras, long sa nangungunang lima ayon sa 12-oras na return, short sa pinakahuling lima, 18 buwan ng history, at singil ang fees at funding sa bawat leg.
Hanapin ang bar kung saan naghiwalay ang mga run
Kung ila-log mo ang equity sa bawat bar nang buong precision, aabutin ito ng mga apat na minuto. I-export ang dalawang run sa CSV na (bar_index, equity, open_positions_hash), i-load ang mga ito, at hanapin ang unang index na hindi magkatugma. Nasulat na namin ang script na iyon para sa ibang bug, at iyon lang ang dahilan kung bakit hindi ako nag-ubos ng isang umaga rito.
Bar 11,206, 2025-03-14T08:00 UTC. Magkapareho ang equity sa naunang bar hanggang 13 significant figure. Sa 11,206, magkaiba na ang position hash: long SOL ang run A, long AVAX naman ang run B. Parehong side, parehong notional, magkaibang symbol. Lahat ng sumunod ay epekto na nito.
Kaya inilimbag ko ang mga input ng ranking para sa bar na iyon sa dalawang run. Magkapareho ang mga ito. Eksaktong magkapareho hanggang sa bawat byte, parehong 30 symbol, parehong 30 score. Dalawa sa mga score ay 0.0. Eksaktong zero ang dalawa dahil may inilimbag na 4-oras na bar na walang trade sa loob ng lookback window ang dalawang asset, kaya naging one ang close-over-close at zero ang log. Hindi ito resulta ng pag-round sa zero. Zero talaga.
Tabla ang dalawang symbol sa rank five. Pinili ang nangungunang lima. Kung alin sa dalawa ang mapasama, nakadepende sa pagkakasunod na iniwan ng sort sa mga ito, at iba pala ang ginagawa ng sort kaysa sa inakala ko.
Tabla, unstable sort, at ang bagay na dalawang taon ko nang hindi pinapansin
Dumaan ang ranking sa grouped aggregation, at nagmula ang frame na pinasahan nito sa dict comprehension na nag-iterate sa set ng mga symbol na binubuo ulit sa bawat bar mula sa async fetch. Nagbabago ang iteration order ng set kasabay ng hash seed, at nira-randomize ng Python ang string hash seed sa bawat process maliban kung itatakda mo ang PYTHONHASHSEED. Kaya nag-iba ang paunang ayos ng mga row bago ang sort sa bawat run, at dahil non-stable ang sort sa magkakatabla ang key, magkaiba ang naging resulta ng tabla.
Hindi pambihirang pangyayari ang ganitong mga tabla. Bahagi ang mga ito ng istruktura. Saanman mag-saturate o mag-clip ang isang feature, nagkakaroon ng eksaktong pagkakapantay: eksaktong zero ang return sa mga bar na walang volume, nakapirmi sa ±3.0 ang clipped z-score, nagkakaroon ng dose-dosenang tabla ang rank-transform na kakaunti lang ang natatanging value, at binibigyan ng score na 1.0 ang lahat ng pumapasa sa boolean filter. Sa loob ng 18 buwan na may 4-oras na bar, nagkaroon ang run na ito ng 47 bar na tabla sa hangganan ng pagpili. Tatlo sa mga iyon ang nagpabago sa napiling basket. Sa natitira, tabla ang dalawang symbol na kapwa kasama o kapwa wala na sa basket.
Mga isang araw akong kumbinsidong nondeterministic ang data loader, dahil iyon ang kapanapanabik na sagot. Hindi iyon ang sagot. Hindi naman talaga. Set ito, tabla, at palagay tungkol sa stability ng sort na walang nagsulat.
Bakit katumbas ng 0.15 Sharpe ang tatlong trade
Dito tumututol ang mga tao, at ang sagot ay path-dependent na sistema ang backtest na gumagamit ng equity-fraction sizing. Kung 8% ng kasalukuyang equity ang sizing kada leg, ang pagkakaiba sa equity sa bar na n ay nangangahulugan ng pagkakaiba sa bawat notional simula sa bar na n pataas.
Napakaliit lang ng direktang epekto ng unang divergence. Nalugi nang 2.1% sa loob ng siyam na oras ang AVAX leg ng run B; kumita naman nang 0.4% ang SOL leg ng run A. Pagkatapos ng trade na iyon, 0.21% ang agwat sa equity. Bale-wala. Pero mula roon, hindi na magkaparehong strategy ang dalawang run. Bahagyang magkaiba ang hawak nilang laki, kaya bahagyang magkaiba rin ang naipong funding nila; nagkaiba ulit ang resulta ng dalawa sa mga sumunod na tabla sa hangganan dahil bahagyang magkaiba na ang mga posisyong hawak na pinagmulan ng mga score. Noong 2025-03-27 nangyari ang isa sa mga iyon, isang araw bago ang anim na araw na trend na naghatid ng humigit-kumulang sangkatlo ng kabuuang PnL ng run. Nakasakay ang run A sa buong galaw; isang rebalance ang huli ng pasok ng run B.
Agwat sa return: 3.4 puntos. Mas malaki ang agwat sa Sharpe kaysa sa ipinahihiwatig ng agwat sa return dahil sumabay ang muling pagkakaayos ng mga trade ng run B sa mas pabagu-bagong yugto, kaya tumaas ang denominator habang bumaba ang numerator. Maliit na sanhi, dalawang nagpapalakas ng epekto.
Kung fixed-notional ang sizing mo at hindi nakadepende sa kasalukuyang holdings ang mga entry mo, mas protektado ka. Hindi ganoon ang karamihan sa mga interesting na strategy.
Ang limang pinagmumulan ng problema
| Pinagmulan | Sintomas | Solusyon |
|---|---|---|
Hindi itinalagang PYTHONHASHSEED at nakaaapekto sa sort ang iteration order ng set/dict | Nag-iiba ang resulta ng tabla sa bawat run; may unang divergence sa isang partikular na bar | Itakda ang seed; gumamit ng tahasang secondary key (symbol) sa sort para deterministiko ang mga tabla |
Non-stable na sort sa magkakatabla ang key (quicksort default sa NumPy/pandas) | Ganoon din ang sintomas sa itaas; nagpapatuloy kahit itakda ang seed | kind="stable", o gawing total ang key |
| Walang seed na RNG sa bootstrap, mga shuffle sa train/test, o synthetic fill jitter | Umaagos ang pagkakaiba sa buong run, walang malinaw na punto ng divergence | Magtakda ng iisang seed para sa bawat component at i-log ito sa run manifest |
| Parallel float reduction (nakadepende sa bilang ng thread ang pagkakasunod ng pagdaragdag) | Nagkakaiba ang huling ilang bit; karaniwan itong walang epekto maliban kung tatawid sa threshold comparison | Itakda ang bilang ng thread para sa mga research run; huwag kailanman ikumpara ang mga float gamit ang == sa hangganan ng desisyon |
| Hindi nakapirming bersyon ng library | Reproducible ngayon, hindi na sa Nobyembre | Ilagay ang lockfile hash sa manifest, katabi ng data snapshot hash |
Hindi kasingdalas ng kinatatakutan ng mga tao mahalaga ang ikaapat na row; ang unang row naman ang palaging sumasabit.
Kasangkapan ang bit-reproducibility, hindi kabutihan
Kailangan mo ang determinism para kapag nagbago ka ng isang linya, matukoy mo na ang pagbabagong iyon ang pinagmulan ng pagkakaiba sa equity curve. Iyon lang ang dahilan. Nagsusulat na ngayon ang bawat agent run sa Stratmill ng manifest na may data snapshot hash, lockfile hash at bawat seed; kapag hindi naulit ng rerun ang dating curve nang eksaktong magkapareho hanggang sa bawat bit, failed build iyon, hindi usisero lang na resulta.
Pero kapag kaya mo nang ulitin ang resulta, sadyain mong guluhin ito. Patakbuhin ang backtest nang 64 na beses gamit ang 64 na seed at tingnan ang saklaw ng mga resulta:
Ang jitter band. Iisang strategy at parehong data, na may 64 na seeded permutation ng tie-break at pagkakasunod ng fill. Sharpe p5 na 1.12, median na 1.27, at p95 na 1.41. Lapad ng band: 0.29.
Ngayon, balikan ang parameter sweep. 1.46 ang score ng pinakamagandang config. 1.31 naman ang score ng config na ika-40 sa 96. 0.15 ang agwat ng mga ito, kalahati ng band. Hindi niranggo ng sweep ang dalawang config na iyon. Kumuha ito ng tig-isang sample mula sa distribusyon ng bawat isa at inayos ang mga sample ayon sa score.
Dahil sa pagbabagong iyon ng pananaw, nagbago ang paraan namin ng pagpili. Ranking lang ang resulta ng sweep kung mas malaki ang agwat sa pagitan ng mga config kaysa sa jitter ng iisang config; sa path-dependent na strategy na may 1,400 trade, karaniwang sapat ang laki ng jitter para magtabla ang nangungunang sangkatlo ng leaderboard. Kapag ganoon, pumili batay sa bagay na hindi natatakpan ng band: mas mababang turnover, mas kaunting parameter, palagay sa cost na maipagtatanggol mo sa isang mapagdududa, o mas maayos na kilos sa walk-forward fold na pinakahindi mo gusto. Tunay na mga panira ng tabla ang mga iyon. Hindi ganoon ang 0.15 na lamang sa Sharpe.
May isa pa akong iminumungkahing gawin bago ka magtiwala sa alinman dito. Patakbuhin ang backtest mo nang dalawang beses ngayon, i-diff ang equity sa bawat bar, at alamin kung kabilang ka sa grupong bit-identical o sa grupong may 0.15 na agwat. Labinlimang minuto lang ang eksperimentong ito, at malalaman mo kung gaano kalaking bahagi ng research history mo ang sumukat sa strategy, at kung gaano kalaki ang sumukat lang sa hash seed.
← Lahat ng post


