Een kalenderjaar aan 1-minuutbars hoort 525,600 rijen te bevatten. Onze download van BTCUSDT-perpetuals uit 2025 leverde 525,557 rijen op — 43 minuten te weinig, een volledigheid van 99.992%. Een perpetual van een altcoin met middelgrote marktkapitalisatie op dezelfde beurs, met dezelfde download en dezelfde code, kwam 1,247 minuten tekort: 99.76%. En een Amerikaanse aandelenreeks met minuutdata voor hetzelfde jaar had ongeveer 427,000 rijen minder dan de cryptoreeks. Dat is geen datagat, maar gewoon een markt die sluit.
Drie van die getallen zijn niet zo interessant. Het getal dat duizend woorden waard is, is 43.
Vier verschillende dingen die er allemaal uitzien als een gat in je dataframe
Eerst de 43, maar vooraf de indeling, want de meeste code voor het afhandelen van hiaten gaat op dit niveau al mis. Als een rij ontbreekt in je lokale parquet-bestand, weet je niet welke van deze oorzaken daarachter zit. Voor elk geval is een andere reactie nodig.
| Soort | Wat er werkelijk is gebeurd | Hoe het eruitziet | Juiste aanpak |
|---|---|---|---|
| Geen transacties | De markt was open; niemand handelde die minuut over de spread heen | Bar met nulvolume (OHLC gelijk, trades=0) op sommige endpoints, ontbrekende rij op andere | Behouden. Het is echte informatie: niemand wilde handelen. |
| Beurs uitgevallen | Matching-engine offline, gepland of ongepland | Ontbrekende rij, soms een reeks ervan | Markeer als verouderd. Handel er niet overheen. |
| Handel in instrument stilgelegd | LULD-band doorbroken, nieuws in afwachting, aankondiging van schrapping | Ontbrekende rij, gevolgd door een openingsveilingtransactie | Markeer als verouderd en behandel de hervatting als een discontinuïteit. |
| Eigen fout | Paginering met rate limiting, een retry waarbij een pagina wegviel, een bug in de lus op de tijdzonegrens | Ontbrekende rij, niet te onderscheiden van de gevallen hierboven | Detecteer en haal de data opnieuw op. Dit is het geval dat je kunt oplossen. |
Beurzen verschillen in hoe het eerste geval uit die tabel eruitziet, en daar gaat het mis. Het candles-endpoint van Coinbase slaat lege buckets helemaal over, waardoor de historie van een illiquide paar vol letterlijke gaten zit. De klines van Binance leveren meestal een synthetische bar met volume 0 en open=high=low=close, vastgezet op de vorige transactieprijs. Hetzelfde onderliggende gegeven, twee verschillende vormen. Een loader die de data opnieuw indexeert op een volledig minutenraster zet de ene vorm om in de andere zonder dat te melden. Controleer wat jouw beurs doet voordat je de code voor het opvullen van hiaten schrijft, niet erna.
De 1,247 ontbrekende minuten bij de altcoin vielen vrijwel allemaal in categorie één: een dun orderboek om 04:00 UTC op zondag, niemand die handelde. Vervelend, makkelijk te begrijpen en grotendeels onschadelijk, want de strategie zou toen toch niet hebben gehandeld. Precies daarom hield ik ermee op ernaar te kijken en ging ik terug naar de 43.
De 43 minuten lagen niet verspreid
Als de ontbrekende data gelijkmatig verdeeld was, zouden 43 minuten verspreid over een jaar 43 losse uitschieters opleveren, één per acht en een halve dag, elk met een verwaarloosbare invloed. Dat was niet wat we zagen. Ze kwamen in zes reeksen: één van 19 opeenvolgende minuten, één van 11, twee van 4 en een paar tweetallen. Zes gebeurtenissen, geen 43 toevalligheden.
En die gebeurtenissen hangen samen met precies datgene waar je om geeft. Beurzen vallen uit als ze zwaar belast worden, en die belasting is hoog wanneer de prijs beweegt. Ik deelde elk uur van het jaar in op basis van de gerealiseerde volatiliteit en keek waar de ontbrekende minuten vielen: 61% ervan viel in het hoogste deciel. De onvoorwaardelijke kans dat een minuut ontbreekt is 0.008%. Binnen een uur uit het hoogste volatiliteitsdeciel is dat ongeveer 0.05% — zes keer zo hoog, en bovendien in clusters.
De volledigheidsmaatstaf op je dashboard voor datakwaliteit meet dus het verkeerde. 99.992% klinkt als een dataset waar je niet meer over hoeft na te denken. In werkelijkheid beschrijft dat een reeks die volledig is in de uren waarin je strategie niets doet, maar gaten vertoont in de uren waarin ze volop actief is. Een momentumstrategie die afgaat bij een volatiliteitsexpansie heeft een flink grotere kans om een gat tegen te komen dan het totaalcijfer suggereert, en dat gebeurt midden in een transactie.
Wat forward-fill een paar regels later aanricht
Dit is de fout waardoor ik dit stuk ben gaan schrijven. Neem de reeks van 19 minuten. Standaard opschoning: herindexeer op het volledige minutenraster, vul OHLC vooruit met de laatste slotkoers en stel volume in op nul. De reeks is nu doorlopend en je indicatoren draaien zonder dat er ergens een NaN te zien is.
Die 19 bars hebben high == low == close. De true range is bij elke bar nul. Een ATR(14) die over dat venster wordt berekend, vanaf een waarde van ongeveer 240 USDT vóór de uitval, zakt naar ongeveer 34 wanneer de beurs weer online komt — de 5 echte bars die in het venster overblijven dragen het hele gemiddelde. Voer die waarde vervolgens in een positiegroottebepaling op basis van volatiliteit, van het gebruikelijke type size = risk_budget / ATR, en de positiegrootte wordt 7 keer zo groot.
De volgende echte bar is de transactie bij de hervatting, en dat is geen rustige bar. In ons geval opende die 1.8% hoger dan de laatste slotkoers vóór de uitval. De backtest nam zonder morren een positie van 7x in bij een koersgat van 1.8%, op basis van een fill die niet had kunnen bestaan, tegen een prijs die niemand noteerde. Die ene synthetische transactie was in de equity curve meer waard dan een maand aan legitieme P&L, maar dan de verkeerde kant op — en hij ontstond volledig door één regel voor het opschonen van data, geschreven om het dataframe netjes te maken.
De rijen verwijderen in plaats van ze op te vullen is ook geen oplossing; het is dezelfde bug met een ander jasje. Verwijder je ze, dan geven je terugblikken met gehele indexwaarden een verkeerd beeld: een "20-bar-EMA" beslaat nu 39 minuten op de klok over de uitval heen, het rendement van bar tot bar over de grens is de volledige sprong van 1.8%, behandeld als één minuut beweging, en elke volatiliteitsschatting per bar leest die als een gebeurtenis van 60 sigma. Niets waarschuwt je. De index loopt nog steeds monotoon op.
Bij resampling verdwijnt het probleem uit beeld
Het meeste onderzoek draait niet op 1-minuutbars, maar op geaggregeerde data, en aggregatie maskeert het probleem. Resample naar 5 minuten en een gat van 19 minuten wordt vier bars, waarvan de eerste en laatste gedeeltelijk zijn. Pandas berekent een OHLC die er heel redelijk uitziet op basis van 2 overgebleven minuten en geeft die hetzelfde label als een bar die uit 5 minuten is opgebouwd. Niets in de uitvoer maakt het verschil zichtbaar.
De eenvoudigste oplossing die ik ken: neem een kolom bars_in_window mee in elke resampling en gooi die nooit weg. Eén geheel getal per rij, en je kunt elke vervolgvraag beantwoorden over de betrouwbaarheid van een bar. We nemen ook seconds_since_last_real_print mee: dezelfde informatie, maar in een vorm waarop de uitvoeringslaag kan handelen.
Ons beleid, voor zover je het zo kunt noemen
Dit passen onze agents nu toe, in deze volgorde:
- Herindexeer nooit stilzwijgend. De loader maakt een manifest van hiaten met begin, einde, lengte en de categorie waarin het geval volgens de loader valt. Als een reeks ontbrekende minuten korter is dan 3 bars en het volume in de omliggende bars laag is, gaat het om minuten zonder transacties. Alles wat langer duurt tijdens actieve uren behandelen we als een uitval totdat het tegendeel is bewezen.
- Haal de data opnieuw op voordat je conclusies trekt. De helft van onze vroege hiaten kwam door bugs in de paginering. Een tweede download via een ander endpoint of een andere dataleverancier lost categorie vier op en verkleint het probleem voordat we een oordeel hoeven te vellen.
- Gebruik een verouderingsfilter, geen opvulling van hiaten. De strategie krijgt een invoer
data_ageen een harde regel: geen nieuwe posities als de laatste echte transactie ouder is dan N bars; open posities worden bij hervatting alleen gesloten met een market order waarvan de prijs expliciet is aangepast voor het risico van een koersgat. Fills die niet hadden kunnen plaatsvinden zijn erger dan transacties die niet zijn uitgevoerd. - Indicatoren krijgen NaN, geen fictieve data. Vooruitgevulde prijzen bereiken de featurelaag nooit. Als ATR niet kan worden berekend, is de waarde ongedefinieerd, en ongedefinieerd betekent geen positie. Een duidelijke fout is beter dan stilzwijgend een 7x zo grote positie.
- Rapporteer prestaties uitgesplitst naar hiaten. Elke backtest die we opleveren toont de P&L naast het totaalcijfer, met transacties rond uitval uitgesloten. Als die transacties het resultaat dragen, is het resultaat een data-artefact.
Een snelle controle die je vandaag kunt uitvoeren: groepeer ontbrekende minuten in opeenvolgende reeksen en bekijk vervolgens welk deel van de transacties in je backtest binnen 30 minuten vóór of na het begin of einde van zo'n reeks wordt geopend of gesloten. Is dat minder dan 1%, dan sturen de hiaten waarschijnlijk niets aan. Is het 5% of meer, dan vertelt de equity curve deels een verhaal over uitval van de beurs.
Ik ben de vorm van de ontbrekende data meer gaan vertrouwen als signaal dan de omvang ervan. Een dataset met duizenden verspreide gaten tijdens rustige uren is meestal prima. Een dataset met een handvol compacte clusters vertelt je dat er iets bezwijkt onder belasting — en juist daar, waar iets bezwijkt onder belasting, speelt je strategie zich af.
← Alle artikelen
