22 oktober 2026 · gegevens

We hebben een backtest opnieuw opgebouwd nadat een dataleverancier zijn historische gegevens had aangepast

We hebben een backtest opnieuw opgebouwd nadat een dataleverancier zijn historische gegevens had aangepast

Zes weken nadat we een backtest hadden uitgevoerd, draaiden we hem opnieuw met dezelfde code, configuratie en hetzelfde datumbereik. De eindwaarde van de equity was anders. Niet dramatisch anders, maar genoeg om de Sharpe-ratio te laten dalen van 1.31 naar 1.08 en de parameterset die er het beste uitzag te veranderen.

De strategie was niet veranderd. De historische gegevens wel. Een leverancier had verschillende dagen aan trades en bars herzien, en bij onze eerste run hadden we de exacte input-snapshot niet bewaard. De daaropvolgende dagen onderzochten we of het nieuwe resultaat het gevolg was van een datacorrectie, een pipelinebug of een onderzoeksproces dat we niet konden reproduceren.

Maandag: de rerun gaf een ander resultaat

De eerste aanwijzing was een klein verschil in een liquide spotpaar. Onze oorspronkelijke bar toonde een high van 62,418. De rerun toonde 62,431. Dat verschil leek onschuldig, totdat we de entry van de strategie naliepen: de drempel lag tussen die prijzen. Een paar gewijzigde bars betekenden andere signalen, en dus andere posities enkele dagen later.

We vergeleken de dagelijkse aantallen trades en ontdekten dat de afwijking op één datum begon. Daarna vergeleken we de ruwe rijen die voor die datum waren gebruikt. De huidige respons van de leverancier bevatte een handvol gecorrigeerde trades en een herziene aggregatiebar. Bij de respons die we hadden gebruikt, zat geen changelog.

Dit is normaal datagedrag. Beurzen herstellen records; leveranciers vullen hiaten aan, corrigeren foutieve transacties en passen aggregatielogica aan. Een query voor ‘gegevens van januari’ kan in maart een ander antwoord opleveren.

Dinsdag: we vonden het ontbrekende ontvangstbewijs

We hadden de naam van de leverancier, het symbool en het datumbereik in het runrecord opgeslagen. De respons zelf of een contenthash voor elke inputpartitie hadden we niet bewaard. Daardoor konden we aantonen wat we hadden opgevraagd, maar niet welke bytes de leverancier had teruggestuurd.

We haalden de oudste gecachte bestanden op die we konden vinden en vergeleken die met de huidige respons. Het verschil kwam niet door een splitcorrectie of een tijdzoneconversie. Het ging om een kleine set gecorrigeerde trade-records die door onze barbuilder liep. Daarmee viel één theorie af, maar de cache was onvolledig, dus we konden nog steeds niet elke getroffen bar exact reconstrueren.

Wat we vastlegdenWelke vraag we daarmee konden beantwoordenWat ontbrak
Leverancier, symbool, datumbereikWelk verzoek de run had gedaanDe inhoud van de respons
PipelineversieWelke code de bars had opgebouwdDe exacte inputrijen
Tijdstip van de runWanneer we de job hadden uitgevoerdWelke leveranciersrevisie toen beschikbaar was

We hadden ‘dezelfde query’ gelijkgesteld aan ‘dezelfde gegevens’. Door die aanname leek het experiment beter reproduceerbaar dan het was.

Woensdag: de correctie veranderde het verhaal

We speelden de getroffen datums opnieuw af met beide beschikbare versies van de gegevens. De herziene bars verwijderden twee entries en verschoof één exit met één bar. Die drie trades verklaarden het grootste deel van de verandering in de Sharpe-ratio. De strategie was gevoelig voor enkele observaties rond de trigger, ook al leken de totale prijsreeksen op een grafiek vrijwel identiek.

Dat betekende niet dat het oorspronkelijke resultaat frauduleus was of dat het herziene resultaat automatisch beter was. Het betekende dat we de claim moesten afbakenen: de oude run beschreef de data-snapshot die we destijds hadden, terwijl de rerun de huidige historie van de leverancier beschreef. We konden de ene niet eerlijk een reproductie van de andere noemen.

Donderdag: we pasten het onderzoeksrecord aan

We voegen nu onveranderlijke inputsnapshots toe aan nieuwe onderzoeksruns. Elke partitie krijgt een contenthash, en het runmanifest registreert de bron, het ophaaltijdstip, de schemaversie, het correctiebeleid en de versie van de transformatiecode. Voor grote datasets bewaren we de bestanden zelf in content-addressed storage, zodat identieke partities kunnen worden gedeeld zonder eerdere versies te overschrijven.

Wanneer opslag- of licentieregels ons verbieden ruwe gegevens te bewaren, bewaren we het sterkste ontvangstbewijs dat de voorwaarden toelaten: hashes, verzoekparameters, bronrevisie-ID's en een kleine steekproef van geparste rijen voor diagnose. Een hash kan de gegevens niet terughalen, maar kan ons wel laten zien wanneer twee runs niet dezelfde bytes hebben verwerkt.

Voor een reproduceerbare backtest heb je een identificatie van de input nodig, niet alleen een data-URL. Leg vast wat er binnenkwam, wanneer het binnenkwam en hoe het de rijen werd die je strategie verwerkte.

Vrijdag: wat we de volgende keer zouden overslaan

We zouden het vergelijken van alleen de kerncijfers overslaan. Een gewijzigde Sharpe vertelt je dat er iets is veranderd, maar niet waardoor. Begin met het aantal rijen en de partitiehashes, en zoek dan de eerste inputdatum waarop iets afwijkt. Vergelijk daarna in deze volgorde de getransformeerde rijen, signalen, orders en fills.

We zouden ook het stilzwijgend vervangen van oude gegevens door gecorrigeerde historie overslaan. Correcties zijn nuttig, maar oude runs moeten aan hun oorspronkelijke inputs gekoppeld blijven. Een nieuwe dataset moet een nieuwe run opleveren, met een zichtbare relatie tot de oudere run.

Onze praktische regel is nu eenvoudig: als een rerun bedoeld is om een experiment te reproduceren, pinnen we de data-snapshot vast. Als de rerun bedoeld is om van gecorrigeerde historie te profiteren, bestempelen we hem als een nieuw experiment. Het kostte ons een paar dagen om dat onderscheid te leren; nu staat het in één regel van het runmanifest.

marktgegevensbacktestingdata-engineeringreproduceerbaarheid
← Alle artikelen