11. září 2026 · reprodukovatelnost

Stejný kód, stejná data, dvě různá Sharpe: audit nedeterminismu, který váš backtest potřebuje

Stejný kód, stejná data, dvě různá Sharpe: audit nedeterminismu, který váš backtest potřebuje

Stejný commit, stejné soubory Parquet, stejný počítač, dvě spuštění s hodinovým odstupem. Sharpe 1.34 a Sharpe 1.19. Celkový výnos 41.2% a 37.8%. Počet obchodů 1,418 a 1,421. A obě křivky kapitálu se shodovaly do posledního vypsaného desetinného místa po 11,205 po sobě jdoucích svíček, než se rozešly.

0.15rozdíl Sharpe, totožné vstupy
3 / 1,418odlišné obchody
11,205totožných svíček před rozchodem

První tři čísla jsou nepříjemná. To poslední je zajímavé, protože ukazuje, že problémem není nepřesnost plovoucí řádové čárky rozptýlená po celém běhu. Na jednom konkrétním baru se stalo něco diskrétního a všechno ostatní už bylo jen násobení dopadu. Tento článek je o tom, jak ten bar najít, a co velikost rozdílu 0.15 znamená pro každý parametrický sweep, který jste kdy spustili.

Strategie: průřezové momentum na 30 perpetuích USDⓈ-M s nejvyšším objemem, rebalancování každé 4 hodiny, long pěti nejlepších podle 12hodinového výnosu, short pěti nejhorších, historie za 18 měsíců, poplatky a funding účtované za každou nohu.

Hledání baru, kde se běhy rozešly

Pokud zaznamenáváte kapitál po jednotlivých barech s plnou přesností, zabere to asi čtyři minuty. Exportujte oba běhy do CSV s (bar_index, equity, open_positions_hash), oba načtěte a najděte první index, kde se liší. Ten skript už jsme měli napsaný kvůli jiné chybě, a jen díky tomu jsem nad tím nestrávil celé dopoledne.

Bar 11,206, 2025-03-14T08:00 UTC. Kapitál na předchozím baru byl totožný na 13 platných číslic. Na baru 11,206 se liší hashe pozic: běh A je long SOL, běh B je long AVAX. Stejná strana, stejný nominál, jiný symbol. Všechno další už je důsledek.

Vytiskl jsem tedy vstupy do žebříčku pro tento bar v obou bězích. Byly totožné. Bajt po bajtu stejné, stejných 30 symbolů, stejných 30 skóre. Dvě skóre byla 0.0. Přesně nula, obě, protože oba symboly měly v rámci sledovaného okna 4hodinový bar bez obchodů, takže poměr závěrečných cen vyšel jedna a logaritmus nula. Nezaokrouhleno na nulu. Nula.

Dva symboly se dělily o 5. místo. Výběr bral pět nejlepších. Který z nich se dostal dovnitř, záviselo na pořadí, v jakém je řazení ponechalo, a řazení se nechovalo tak, jak jsem předpokládal.

Shoda, nestabilní řazení a věc, kterou jsem dva roky přehlížel

Řazení probíhalo přes seskupenou agregaci a vstupní rámec vznikal z dict comprehension nad množinou symbolů, která se pro každý bar znovu vytvářela z asynchronního načítání. Pořadí průchodu množinou se mění podle hash seedu a Python náhodně volí seed hashování řetězců pro každý proces, pokud nenastavíte PYTHONHASHSEED. Pořadí řádků před řazením se tedy mezi běhy lišilo, a při nestabilním řazení podle klíče se shodným skóre se remíza rozsekla pokaždé jinak.

Takové shody nejsou žádná rarita. Jsou vlastní konstrukci systému. Kdykoli se nějaký příznak saturuje nebo omezí, vznikají přesné shody: bary s nulovým objemem dávají přesně nulové výnosy, oříznuté z-skóre se zastaví na ±3.0, transformace na pořadí s malým počtem různých hodnot vytvoří desítky shod a booleovský filtr přiřadí všemu, co projde, skóre 1.0. V tomto běhu za 18 měsíců při 4hodinových barech bylo 47 barů se shodou na hranici výběru. Tři z nich změnily vybraný koš. Ve zbývajících případech se shodovala skóre dvou symbolů, které už byly oba buď vybrané, nebo vyřazené.

Asi den jsem byl přesvědčený, že nedeterministický je načítač dat, protože to znělo jako ta zajímavá odpověď. Nebyl. Nikdy to tak není. Je to množina, shoda a předpoklad o stabilitě řazení, který si nikdo nezapsal.

Proč tři obchody znamenají rozdíl Sharpe 0.15

Tady lidé namítají. Odpověď je, že backtest s velikostí pozice stanovenou jako podíl kapitálu je systém závislý na průběhu. Pokud na každou nohu riskujete 8% aktuálního kapitálu, rozdíl v kapitálu na baru n znamená rozdíl v každém nominálu od baru n dál.

První odchylka sama o sobě stála jen málo. Noha AVAX v běhu B ztratila za devět hodin 2.1%; noha SOL v běhu A získala 0.4%. Rozdíl v kapitálu po tomto obchodu: 0.21%. Zanedbatelný. Od té chvíle už ale oba běhy nepředstavovaly stejnou strategii. Držely mírně odlišné pozice, takže se u nich mírně lišilo nabíhání fundingu, a další dvě shody na hranici výběru se znovu rozsekly jinak, protože skóre vycházela z trochu odlišných držených pozic. Jedna z těchto situací nastala 2025-03-27, den před šestidenním trendem, který vytvořil zhruba třetinu celkového PnL běhu. Běh A zachytil celý pohyb; běh B vstoupil až o jedno rebalancování později.

Rozdíl ve výnosu: 3.4 procentního bodu. Rozdíl Sharpe je větší, než by naznačoval rozdíl ve výnosu, protože přeskupené obchody v běhu B se překrývaly s volatilnějším obdobím, takže jmenovatel vzrostl, zatímco čitatel klesl. Malá příčina, dva zesilující faktory.

Pokud používáte pevný nominál a vstupy nezávisí na aktuálně držených pozicích, jste vůči tomu mnohem odolnější. Většina zajímavých strategií ale nesplňuje ani jednu z těchto podmínek.

Pět míst, kde se to skutečně projeví

ZdrojProjevNáprava
Nenastavený PYTHONHASHSEED s pořadím průchodu množinou či dict jako vstupem pro řazeníRozhodnutí při shodě se mezi běhy mění; první odchylka nastane na konkrétním baruNastavte seed; řaďte i podle explicitního druhého klíče (symbolu), aby byly shody deterministické
Nestabilní řazení při shodě klíče (quicksort výchozí v NumPy/pandas)Totéž co výše, přetrvá i po nastavení seedukind="stable" nebo zajistěte úplné uspořádání klíče
Nenastavený generátor náhodných čísel při bootstrapu, míchání trénovacích a testovacích dat nebo náhodném rozptylu simulovaných plněníOdchylka v celém běhu bez jasného bodu rozchoduNastavte jeden explicitní seed pro každou komponentu a zaznamenejte ho v manifestu běhu
Paralelní redukce floatů (pořadí sčítání závislé na počtu vláken)Rozdíly v posledních několika bitech, obvykle neškodné, dokud nepřekročí práh při porovnáníNastavte počet vláken pro výzkumné běhy; nikdy neporovnávejte floaty pomocí == na rozhodovací hranici
Nenastavené verze knihovenDnes reprodukovatelné, v listopadu už neDo manifestu přidejte hash lockfilu vedle hashe snímku dat

Čtvrtý řádek je problém méně často, než se lidé obávají, první řádek ale působí potíže pořád.

Bitová reprodukovatelnost je nástroj, ne ctnost

Determinismus potřebujete proto, abyste při změně jednoho řádku kódu mohli připsat rozdíl v křivce kapitálu právě této změně. O to jde. Každý běh agenta ve Stratmillu teď zapisuje manifest s hashem snímku dat, hashem lockfilu a všemi seedy. Opakovaný běh, který bit po bitu nezreprodukuje předchozí křivku, znamená neúspěšný build, ne zajímavost.

Jakmile ale reprodukovatelnost zajistíte, záměrně ji narušte. Spusťte to 64krát s 64 seedy a podívejte se na rozptyl:

Pásmo rozkolísanosti. Stejná strategie, stejná data, 64 permutací rozhodnutí při shodě a pořadí plnění s nastavenými seedy. Sharpe p5 1.12, medián 1.27, p95 1.41. Šířka pásma 0.29.

Vraťte se teď k parametrickému sweepu. Nejlepší konfigurace dosáhla 1.46. Konfigurace na 40. místě z 96 dosáhla 1.31. Rozdíl mezi nimi je 0.15, tedy polovina pásma. Sweep tyto dvě konfigurace neseřadil. Vylosoval jedno pozorování z rozdělení výsledků každé z nich a seřadil tato pozorování.

Tento nový pohled změnil způsob našeho výběru. Výsledek sweepu udává pořadí jen tehdy, když rozdíly mezi konfiguracemi převyšují rozkolísanost jedné konfigurace. U strategie závislé na průběhu s 1,400 obchody bývá rozkolísanost obvykle tak velká, že horní třetinu žebříčku slije do jedné remízy. V takovém případě vybírejte podle něčeho, co se v pásmu neztratí: nižší obrátkovost, méně parametrů, nákladový předpoklad, který dokážete obhájit před skeptikem, lepší chování ve walk-forward části, kterou máte nejméně rádi. To jsou skutečné rozhodovací faktory. Výhoda 0.15 v Sharpe mezi ně nepatří.

Ještě jedna věc, kterou se vyplatí udělat, než tomu všemu uvěříte. Spusťte svůj backtest hned dvakrát, porovnejte kapitál po jednotlivých barech a zjistěte, zda patříte do skupiny s bitově totožnými výsledky, nebo do skupiny s rozdílem 0.15. Je to patnáctiminutový experiment a ukáže vám, jak velká část vaší výzkumné historie měřila strategii a jak velká jen hash seed.

determinismusbacktestovánídatové inženýrstvípřeučenípython
← Všechny články