Seks uker etter at vi kjørte en backtest, kjørte vi den på nytt med samme kode, konfigurasjon og datoperiode. Sluttverdien på egenkapitalen var annerledes. Ikke dramatisk annerledes, men nok til å flytte Sharpe-forholdet fra 1.31 til 1.08 og endre hvilket parametersett som så best ut.
Strategien hadde ikke endret seg. Det hadde de historiske dataene. En leverandør hadde revidert flere dagers handler og bars, og vi hadde ikke lagret det nøyaktige inndatasnapshotet fra den første kjøringen. De neste dagene brukte vi på å finne ut om det nye resultatet skyldtes en datakorrigering, en feil i datapipelinen eller en forskningsprosess vi ikke kunne gjenskape.
Mandag: ny kjøring ga et annet resultat
Det første tegnet var et lite avvik i et likvid spotpar. Den opprinnelige baren vår viste en topp på 62,418. Den nye kjøringen viste 62,431. Forskjellen virket ubetydelig helt til vi sporet strategiens inngang: terskelen lå mellom disse prisene. Noen få endrede bars ga andre signaler, som igjen ga andre posisjoner flere dager senere.
Vi sammenlignet daglige antall handler og fant ut at avviket startet på én bestemt dato. Deretter sammenlignet vi rådataene for den datoen. Leverandørens nåværende svar inneholdt noen korrigerte handler og en revidert aggregert bar. Svaret vi hadde brukt, hadde ingen endringslogg.
Dette er vanlig for data. Børser retter poster; leverandører fyller inn manglende data, korrigerer feilaktige handler og justerer aggregeringslogikken. Et søk etter «januardata» kan gi et annet svar i mars.
Tirsdag: vi fant den manglende kvitteringen
Vi hadde lagret leverandørnavn, symbol og datoperiode i kjøringsloggen. Vi hadde ikke lagret selve svaret eller en innholdshash for hver inndatapartisjon. Dermed kunne vi dokumentere hva vi hadde bedt om, men ikke hvilke byte leverandøren hadde sendt tilbake.
Vi hentet de eldste hurtigbufrede filene vi kunne finne og sammenlignet dem med det nåværende svaret. Forskjellen skyldtes verken en splitjustering eller en tidssonekonvertering. Det dreide seg om et lite antall korrigerte handler som ble brukt videre av barbyggeren vår. Dermed kunne vi utelukke én teori, men hurtigbufferen var ufullstendig, så vi kunne fortsatt ikke rekonstruere alle de berørte barene nøyaktig.
| Dette registrerte vi | Dette kunne vi svare på | Dette manglet |
|---|---|---|
| Leverandør, symbol, datoperiode | Hvilken forespørsel kjøringen sendte | Innholdet i svaret |
| Pipelineversjon | Hvilken kode som bygget barene | De nøyaktige inndataradene |
| Kjøringstidspunkt | Når vi kjørte jobben | Hvilken leverandørrevisjon som var tilgjengelig da |
Vi hadde behandlet «samme forespørsel» som «samme data». Den antakelsen fikk eksperimentet til å virke mer reproduserbart enn det var.
Onsdag: korrigeringen endret fortellingen
Vi spilte av de berørte datoene på nytt med begge versjonene av de tilgjengelige dataene. De reviderte barene fjernet to innganger og flyttet én utgang én bar. Disse tre handlene sto for det meste av endringen i Sharpe. Strategien var følsom for noen få observasjoner nær utløsningsterskelen, selv om den samlede prisserien så nesten identisk ut på et diagram.
Det betydde ikke at det opprinnelige resultatet var uredelig, eller at det reviderte resultatet automatisk var bedre. Det betydde at påstanden måtte avgrenses: Den gamle kjøringen beskrev datasnapshotet vi hadde på det tidspunktet, mens den nye kjøringen beskrev leverandørens nåværende historikk. Vi kunne ikke med rette kalle den ene en reproduksjon av den andre.
Torsdag: vi endret forskningsloggen
Vi la til uforanderlige inndatasnapshots for nye forskningskjøringer. Hver partisjon får en innholdshash, og kjøringsmanifestet registrerer kilde, hentetidspunkt, skjemaversjon, justeringsregler og versjonen av transformasjonskoden. For store datasett lagrer vi de faktiske filene i innholdsadressert lagring, slik at identiske partisjoner kan deles uten å overskrive tidligere versjoner.
Når lagrings- eller lisensregler hindrer oss i å beholde rådata, tar vi vare på den beste dokumentasjonen vilkårene tillater: hasher, forespørselsparametere, kildeversjonsidentifikatorer og et lite utvalg parsede rader til feilsøking. En hash kan ikke gjenopprette dataene, men den kan fortelle oss når to kjøringer ikke brukte identiske byte.
En reproduserbar backtest trenger en identitet for inndataene, ikke bare en data-URL. Registrer hva som kom inn, når det kom inn, og hvordan det ble til radene strategien din brukte.
Fredag: dette ville vi gjort annerledes neste gang
Vi ville droppet å sammenligne bare hovedtallene. En endret Sharpe forteller deg at noe har flyttet seg, men ikke hvor årsaken ligger. Begynn med radantall og partisjonshasher, og finn deretter den første inndatodatoen som skiller seg ut. Sammenlign så de transformerte radene, signalene, ordrene og utførte handlene, i den rekkefølgen.
Vi ville også droppet å erstatte gamle data i stillhet med korrigert historikk. Korrigeringer er nyttige, men gamle kjøringer bør fortsatt være knyttet til de opprinnelige inndataene. Et nytt datasett bør føre til en ny kjøring, med en synlig kobling til den eldre.
Den praktiske regelen vår er enkel nå: Hvis en ny kjøring skal reprodusere et eksperiment, låser vi datasnapshotet. Hvis den skal dra nytte av korrigert historikk, merker vi den som et nytt eksperiment. Det tok oss noen dager å lære forskjellen, og nå står den i én linje i kjøringsmanifestet.
← Alle innlegg


