Sex veckor efter att vi kört ett backtest körde vi det igen med samma kod, konfiguration och datumintervall. Slutvärdet på kapitalet var annorlunda. Inte dramatiskt annorlunda, men tillräckligt för att Sharpe-kvoten skulle sjunka från 1.31 till 1.08 och för att ändra vilken parameteruppsättning som såg bäst ut.
Strategin hade inte ändrats. Det hade däremot de historiska uppgifterna. En leverantör hade reviderat flera dagars avslut och staplar, och i den första körningen hade vi inte sparat den exakta ögonblicksbilden av indata. De närmaste dagarna försökte vi ta reda på om det nya resultatet berodde på en datakorrigering, ett fel i datapipelinen eller en forskningsprocess som vi inte kunde reproducera.
Måndag: omkörningen gav ett annat resultat
Den första ledtråden var ett litet gap i ett likvidt spotpar. I vår ursprungliga stapel var högsta priset 62,418. I omkörningen var det 62,431. Skillnaden verkade harmlös tills vi spårade strategins ingång: dess tröskel låg mellan de priserna. Några ändrade staplar gav andra signaler, vilket ledde till andra positioner flera dagar senare.
Vi jämförde antalet avslut per dag och upptäckte att avvikelsen började ett visst datum. Sedan jämförde vi rådataposterna som låg till grund för det datumet. Leverantörens aktuella svar innehöll några korrigerade avslut och en reviderad aggregerad stapel. Svaret vi hade använt åtföljdes inte av någon ändringslogg.
Det här är ett normalt beteende för data. Börser rättar poster; leverantörer fyller i luckor, korrigerar felaktiga avslut och justerar aggregeringslogik. En fråga om ”data för januari” kan ge ett annat svar i mars.
Tisdag: vi hittade det saknade kvittot
Vi hade sparat leverantörens namn, symbolen och datumintervallet i körningsposten. Vi hade inte sparat själva svaret eller en innehållshash för varje indatapartition. Det innebar att vi kunde bevisa vad vi hade frågat efter, men inte vilka byte leverantören hade skickat tillbaka.
Vi hämtade de äldsta cachade filerna vi kunde hitta och jämförde dem med det aktuella svaret. Skillnaden berodde inte på en splitjustering eller tidszonskonvertering. Det var en liten uppsättning korrigerade avslutsposter som flödade genom vår stapelbyggare. Det uteslöt en teori, men cachen var ofullständig, så vi kunde fortfarande inte återskapa varje berörd stapel exakt.
| Vad vi registrerade | Vilken fråga det besvarade | Vad som saknades |
|---|---|---|
| Leverantör, symbol, datumintervall | Vilken begäran körningen gjorde | Svarsinnehållet |
| Pipelineversion | Vilken kod som byggde staplarna | De exakta indataposterna |
| Körningens tidsstämpel | När vi körde jobbet | Vilken leverantörsrevision som fanns tillgänglig då |
Vi hade likställt ”samma fråga” med ”samma data”. Det antagandet fick experimentet att verka mer reproducerbart än det var.
Onsdag: korrigeringen ändrade bilden
Vi spelade upp de berörda datumen på nytt med båda versionerna av de tillgängliga uppgifterna. De reviderade staplarna tog bort två ingångar och flyttade en utgång en stapel framåt. De tre avsluten stod för större delen av Sharpe-förändringen. Strategin var känslig för några få observationer nära sin utlösningsnivå, trots att den övergripande prisserien såg nästan identisk ut i ett diagram.
Det betydde inte att det ursprungliga resultatet var falskt eller att det reviderade resultatet automatiskt var bättre. Det betydde att påståendet behövde avgränsas: den gamla körningen beskrev den dataögonblicksbild vi hade då, medan omkörningen beskrev leverantörens aktuella historik. Vi kunde inte ärligt kalla den ena en reproduktion av den andra.
Torsdag: vi ändrade forskningsdokumentationen
Vi började lägga till oföränderliga ögonblicksbilder av indata i nya forskningskörningar. Varje partition får en innehållshash, och körningsmanifestet registrerar källa, hämtningstid, schemaversion, justeringspolicy och version av transformationskoden. För stora datamängder sparar vi de faktiska filerna i innehållsadresserad lagring, så att identiska partitioner kan delas utan att tidigare versioner skrivs över.
När lagrings- eller licensvillkor hindrar oss från att behålla rådata sparar vi det starkaste kvitto som villkoren tillåter: hashvärden, frågeparametrar, källans revisionsidentifierare och ett litet urval av tolkade poster för felsökning. En hash kan inte återskapa data, men den kan tala om för oss när två körningar inte använde identiska byte.
Ett reproducerbart backtest behöver en identitet för indata, inte bara en data-URL. Registrera vad som kom in, när det kom in och hur det blev de poster som strategin använde.
Fredag: vad vi skulle hoppa över nästa gång
Vi skulle hoppa över att bara jämföra övergripande mätvärden. En ändrad Sharpe-kvot säger att något har rört sig, men inte var orsaken finns. Börja med radantal och hashvärden för partitionerna och hitta sedan det första datumet då indata skiljer sig åt. Jämför därefter transformerade poster, signaler, order och avslut, i den ordningen.
Vi skulle också hoppa över att i tysthet ersätta gamla data med korrigerad historik. Korrigeringar är användbara, men gamla körningar bör förbli knutna till sina ursprungliga indata. En ny datamängd bör skapa en ny körning med en synlig koppling till den äldre.
Vår praktiska regel är nu enkel: om en omkörning ska reproducera ett experiment låser vi dataögonblicksbilden. Om den ska dra nytta av korrigerad historik märker vi den som ett nytt experiment. Det tog oss några dagar att lära oss skillnaden, och nu ryms den på en rad i körningsmanifestet.
← Alla inlägg


