2026. szeptember 11. · reprodukálhatóság

Ugyanaz a kód, ugyanazok az adatok, két különböző Sharpe: a nemdeterminizmus-audit, amire a backtesztednek szüksége van

Ugyanaz a kód, ugyanazok az adatok, két különböző Sharpe: a nemdeterminizmus-audit, amire a backtesztednek szüksége van

Ugyanaz a commit, ugyanazok a parquet-fájlok, ugyanaz a gép, két futtatás egy óra különbséggel. Sharpe 1.34 és Sharpe 1.19. Teljes hozam 41.2% és 37.8%. Ügyletek száma 1,418 és 1,421. A két tőkegörbe pedig 11,205 egymást követő gyertyán át minden kiírt tizedesjegyig megegyezett, mielőtt szétvált volna.

0.15Sharpe-különbség, azonos bemenetek
3 / 1,418eltérő ügylet
11,205azonos gyertya a szétválás előtt

Az első három szám bosszantó. Az utolsó az érdekes, mert azt mutatja, hogy nem a teljes futáson végigszórt pontatlan lebegőpontos számítás okozza a problémát. Egy adott gyertyán történt valami diszkrét, utána pedig minden továbbgyűrűzött. Ez a bejegyzés arról szól, hogyan találjuk meg azt a gyertyát, és mit jelent a 0.15-ös eltérés az összes paramétervizsgálatodra nézve, amit valaha lefuttattál.

A stratégia: keresztmetszeti momentum a 30 legnagyobb forgalmú USDⓈ-M perpetual kontraktuson, 4 óránkénti újrasúlyozás, long a 12 órás hozam alapján legjobb öt eszközön, short a legrosszabb ötön, 18 hónapnyi előzmény, a díjakat és a finanszírozást pozíciónként számoljuk fel.

Megkeressük a gyertyát, ahol a futások szétválnak

Ha teljes pontossággal naplózod a gyertyánkénti tőkeértéket, ez nagyjából négy perc. Mentsd mindkét futás eredményét egy (bar_index, equity, open_positions_hash) CSV-fájlba, töltsd be mindkettőt, és keresd meg az első indexet, ahol eltérnek. Ezt a szkriptet már korábban megírtuk egy másik hibához, és csak ezért nem töltöttem vele egy egész délelőttöt.

11,206. gyertya, 2025-03-14T08:00 UTC. Az előző gyertyán a tőkeérték 13 értékes számjegyig azonos. A 11,206. gyertyán eltérnek a pozícióhash-ek: az A futás SOL-ban long, a B futás AVAX-ban. Az irány és a névérték azonos, az eszköz más. Innentől minden eltérés ennek a következménye.

Ezért kiírtam mindkét futás rangsorolási bemeneteit erre a gyertyára. Azonosak voltak. Bájt szinten megegyeztek: ugyanaz a 30 szimbólum, ugyanaz a 30 pontszám. Két pontszám 0.0 volt. Pontosan nulla, mindkettő, mert mindkét eszköz olyan 4 órás gyertyát produkált, amelyben nem történt kötés a visszatekintési ablakban, így a záróárak hányadosa egy lett, a logaritmus pedig nulla. Nem kerekítés miatt lett nulla. Nulla volt.

Két eszköz holtversenyben az ötödik helyen. A kiválasztás az első ötöt vette. Hogy a kettő közül melyik került be, attól függött, milyen sorrendben hagyta őket a rendezés, a rendezés pedig nem úgy működött, ahogy feltételeztem.

Holtverseny, nem stabil rendezés és valami, amit két éve figyelmen kívül hagytam

A rangsorolás csoportosított összesítést használt, a bemeneti keret pedig egy szimbólumhalmazon végzett dict comprehensionből jött, amely minden gyertyánál újraépült egy aszinkron lekérésből. A halmaz bejárási sorrendje a hash-seedtől függ, a Python pedig folyamatonként véletlenszerű karakterlánc-hash-seedet használ, hacsak nem rögzíted a PYTHONHASHSEED értékét. Így a sorok rendezés előtti sorrendje eltért a futások között, és mivel holtversenyes kulcsnál nem stabil rendezést használtunk, a holtverseny is másképp dőlt el.

Az ilyen holtverseny nem ritka kivétel. A rendszer szerkezetéből fakad. Ahol egy jellemző eléri a plafont vagy levágjuk, ott pontos egyezéseket hozunk létre: a nulla forgalmú gyertyák hozama pontosan nulla, a levágott z-pontszám ±3.0-n rögzül, a kevés különböző értéket adó rangtranszformáció tucatnyi holtversenyt eredményez, egy logikai szűrő pedig minden átjutó elemnek 1.0 pontszámot ad. Ennél a futásnál 18 hónapnyi, 4 órás gyertyákon 47 gyertyán volt holtverseny a kiválasztási határon. Három esetben változott meg a kiválasztott kosár. A többi alkalommal két olyan eszköz volt holtversenyben, amelyek mindketten bekerültek vagy mindketten kimaradtak.

Körülbelül egy napig meg voltam győződve róla, hogy az adatbetöltő nem determinisztikus, mert az lett volna az izgalmas válasz. Nem az volt. Soha nem az. Egy halmaz, egy holtverseny és egy feltételezés a rendezés stabilitásáról, amit senki nem dokumentált.

Miért ér 3 ügylet 0.15 Sharpe-ot

Ezen szoktak vitatkozni az emberek, a válasz pedig az, hogy a tőkerész-arányos pozícióméretezést használó backteszt útfüggő rendszer. Ha pozíciónként az aktuális tőke 8%-ával méretezel, akkor az n. gyertyán fennálló tőkeeltérés azt jelenti, hogy az n. gyertyától kezdve minden névérték eltér.

Az első eltérés önmagában alig került valamibe. A B futás AVAX-pozíciója 2.1%-ot veszített kilenc óra alatt; az A futás SOL-pozíciója 0.4%-ot nyert. Az ügylet utáni tőkeeltérés: 0.21%. Elhanyagolható. De innentől a két futás már nem ugyanazt a stratégiát követi. Kicsit eltérő méretű pozíciókat tartanak, ezért kissé eltérő finanszírozási díjakat halmoznak fel, és a későbbi határ menti holtversenyek közül kettő is máshogy dőlt el, mert az alapjukul szolgáló pontszámok már kissé eltérő tartott pozíciókból származtak. Az egyik 2025-03-27-én történt, egy nappal egy hatnapos trend előtt, amely a futás teljes PnL-jének nagyjából harmadát hozta. Az A futás végig benne volt a mozgásban; a B futás egy újrasúlyozással később lépett be.

Hozamkülönbség: 3.4 százalékpont. A Sharpe-különbség nagyobb annál, mint amit a hozamkülönbség sugall, mert a B futás átrendezett ügyletei egy volatilisabb időszakkal estek egybe: a nevező nőtt, miközben a számláló csökkent. Kis kiváltó ok, két erősítő hatás.

Ha rögzített névértékkel méretezel, és a belépéseid nem függenek az aktuális pozícióktól, sokkal védettebb vagy. Az érdekes stratégiák többségére egyik sem igaz.

Az öt hely, ahol ez a gyakorlatban előfordul

ForrásTünetMegoldás
Rögzítetlen PYTHONHASHSEED, ahol a halmaz vagy dict bejárási sorrendje hat a rendezésreA holtverseny feloldása futásonként változik; az első eltérés egy konkrét gyertyánál jelenik megRögzítsd a seedet; a holtversenyek determinisztikus feloldásához rendezz egy kifejezett másodlagos kulcs (szimbólum) szerint
Nem stabil rendezés holtversenyes kulccsal (quicksort NumPy/pandas-alapértelmezés)Ugyanaz, mint fent; a seed rögzítése után is fennmaradkind="stable", vagy alakítsd a kulcsot teljes rendezéssé
Rögzítetlen RNG bootstrapnél, tanító/teszt adatok keverésénél vagy szintetikus fill-ingadozásnálEltérés a teljes futáson át, tiszta szétválási pont nélkülKomponensenként egy kifejezett seed, naplózva a futásjegyzékben
Párhuzamos lebegőpontos redukció (a szálak számától függő összegzési sorrend)Eltérések az utolsó néhány bitben, általában ártalmatlanok, amíg át nem lépnek egy küszöbértékes összehasonlítástRögzítsd a szálak számát a kutatási futásoknál; döntési határon soha ne hasonlíts lebegőpontos értékeket == használatával
Nem rögzített könyvtárverziókMa reprodukálható, novemberben már nemA lockfile hash-ét rögzítsd a jegyzékben az adatpillanatkép hash-e mellett

A negyedik sor ritkábban okoz gondot, mint sokan gondolják, az első viszont állandóan.

A bitre reprodukálhatóság eszköz, nem erény

Azért van szükséged determinizmusra, hogy amikor egy sort megváltoztatsz, a tőkegörbén látható eltérést annak a sornak tulajdoníthasd. Ennyi az egész. A Stratmillen minden agentfutás jegyzéket ír az adatpillanatkép hash-ével, a lockfile hash-ével és az összes seeddel; ha egy újrafuttatás nem állítja elő bitre pontosan a korábbi görbét, az sikertelen build, nem érdekesség.

De amint képes vagy reprodukálni, törd meg szándékosan. Futtasd le 64-szer, 64 seeddel, és nézd meg a szóródást:

Az ingadozási sáv. Ugyanaz a stratégia, ugyanazok az adatok, 64 seedelt variáció a holtverseny feloldásában és a fill-sorrendben. Sharpe p5 1.12, medián 1.27, p95 1.41. A sáv szélessége 0.29.

Most térj vissza a paramétervizsgálathoz. A legjobb konfiguráció 1.46-ot ért el. A 96 közül a 40. helyen álló konfiguráció 1.31-et. A köztük lévő különbség 0.15, vagyis a sáv fele. A vizsgálat nem rangsorolta ezt a két konfigurációt. Mindkettő eloszlásából egy-egy mintát vett, majd sorba rendezte őket.

Ez a nézőpontváltás megváltoztatta, hogyan választunk. A vizsgálat eredménye csak akkor rangsor, ha a konfigurációk közötti különbség nagyobb egyetlen konfiguráció ingadozásánál; egy 1,400 ügyletet kezelő, útfüggő stratégiánál pedig az ingadozás rendszerint elég nagy ahhoz, hogy a ranglista felső harmadát holtversenybe lapítsa. Ilyenkor olyan szempont alapján válassz, amelyet a sáv nem fedhet el: alacsonyabb forgalom, kevesebb paraméter, egy szkeptikusnak is megvédhető költségfeltevés, vagy jobb viselkedés abban a walk-forward részidőszakban, amelyikben a legkevésbé bízol. Ezek valódi holtverseny-felbontó szempontok. A 0.15-ös Sharpe-előny nem az.

Még egy dolgot érdemes megtenni, mielőtt megbízol az eredményekben. Futtasd le most kétszer a backtesztedet, hasonlítsd össze a gyertyánkénti tőkeértéket, és derítsd ki, hogy a bitre azonos vagy a 0.15-ös táborba tartozol-e. Ez egy tizenöt perces kísérlet, és megmutatja, hogy a kutatási előzményeid mekkora része mérte a stratégiát, és mekkora része egy hash-seedet.

determinizmusbacktesztelésadatmérnökségtúlillesztéspython
← Összes bejegyzés