Ein Kalenderjahr mit 1-Minuten-Balken sollte 525,600 Zeilen umfassen. Unser Abruf der BTCUSDT-Perps für 2025 ergab 525,557 – 43 Minuten zu wenig, eine Vollständigkeitsrate von 99.992%. Bei einem Altcoin-Perp mit mittlerer Marktkapitalisierung auf derselben Börse, mit demselben Abruf und demselben Codepfad fehlten 1,247 Minuten: 99.76%. Und eine Minutenreihe für US-Aktien im selben Jahr hatte ungefähr 427,000 Zeilen weniger als die Kryptoreihe. Das ist aber keine Lücke, sondern einfach ein Markt, der schließt.
Drei dieser Zahlen sind unspektakulär. Die 43 sind die Zahl, über die es sich lohnt, tausend Worte zu verlieren.
Vier verschiedene Ursachen, die in deinem DataFrame alle wie ein Loch aussehen
Bevor wir zu den 43 kommen, erst die Einordnung. Denn die meisten Verfahren zur Behandlung von Lücken sind schon auf dieser Ebene falsch. Fehlt eine Zeile in deinem lokalen Parquet, weißt du nicht, welche dieser Ursachen dahintersteckt. Und für jede ist eine andere Reaktion richtig.
| Art | Was tatsächlich passiert ist | Wie es sich zeigt | Richtige Reaktion |
|---|---|---|---|
| Keine Trades | Der Markt war geöffnet, aber in dieser Minute hat niemand den Spread überbrückt | Balken ohne Volumen (OHLC alle gleich, trades=0) bei manchen Endpoints; bei anderen fehlt die Zeile | Beibehalten. Das ist echte Information: Niemand wollte handeln. |
| Börse ausgefallen | Matching Engine offline, geplant oder ungeplant | Zeile fehlt, manchmal eine ganze Reihe davon | Als veraltet markieren. Währenddessen nicht handeln. |
| Handelsunterbrechung des Instruments | LULD-Bandüberschreitung, ausstehende Nachrichten oder Delisting-Mitteilung | Zeile fehlt, dann ein Kurs bei der Wiedereröffnung | Als veraltet markieren und die Wiedereröffnung als Diskontinuität behandeln. |
| Dein Fehler | Ratenbegrenzte Pagination, ein Retry, bei dem eine Seite verloren ging, oder ein Fehler an der Zeitzonengrenze in der Schleife | Zeile fehlt, nicht von den obigen Fällen zu unterscheiden | Erkennen und erneut abrufen. Diesen Fall kannst du beheben. |
Bei der ersten Tabellenzeile unterscheiden sich die Börsen, und genau das wird zum Problem. Der Candles-Endpoint von Coinbase lässt leere Intervalle komplett aus. Deshalb ist der Verlauf eines illiquiden Handelspaars voller echter Lücken. Binances klines liefern in der Regel einen synthetischen Balken mit Volumen 0 und open=high=low=close, festgesetzt auf den vorherigen Trade. Derselbe Sachverhalt, zwei unterschiedliche Darstellungen. Ein Loader, der auf ein vollständiges Minutengitter umindiziert, wandelt die eine in die andere um, ohne es kenntlich zu machen. Finde zuerst heraus, wie sich deine Börse verhält, und schreibe dann den Lückenfüller.
Bei den 1,247 fehlenden Minuten des Altcoins handelte es sich fast ausschließlich um die erste Kategorie: ein dünnes Orderbuch um 04:00 UTC an einem Sonntag, niemand handelte. Ärgerlich, leicht einzuordnen und weitgehend harmlos, weil die Strategie zu dieser Zeit ohnehin nicht gehandelt hätte. Genau deshalb hörte ich auf, mir diese Zahl anzusehen, und wandte mich wieder den 43 zu.
Die 43 Minuten waren nicht verstreut
Wären die Ausfälle gleichmäßig verteilt, wären die 43 Minuten über das Jahr hinweg 43 vereinzelte Lücken: alle achteinhalb Tage eine, jeweils ein Rundungsfehler. So sah es bei uns nicht aus. Die Lücken traten in 6 Intervallen auf: eines mit 19 aufeinanderfolgenden Minuten, eines mit 11, zwei mit jeweils 4 und ein paar Zweiergruppen. 6 Ereignisse, nicht 43 Zufälle.
Und diese Ereignisse hängen von dem ab, was dich interessiert. Börsen fallen unter Last aus, und Last entsteht, wenn sich der Preis bewegt. Ich ordnete jede Stunde des Jahres nach der realisierten Volatilität und prüfte, in welchen Stunden die fehlenden Minuten lagen: 61% davon entfielen auf das oberste Dezil. Die unbedingte Wahrscheinlichkeit, dass eine Minute fehlt, beträgt 0.008%. Liegt sie innerhalb einer Stunde im obersten Volatilitätsdezil, beträgt sie ungefähr 0.05% – sechsmal so hoch und zusätzlich in Gruppen gehäuft.
Die Vollständigkeitskennzahl auf deinem Dashboard zur Datenqualität misst also das Falsche. 99.992% klingt nach einem Datensatz, um den du dir keine Gedanken mehr machen musst. Tatsächlich beschreibt die Zahl eine Reihe, die in den Stunden vollständig ist, in denen deine Strategie nichts tut, und Lücken hat, wenn sie alles tut. Ein Momentum-System, das bei zunehmender Volatilität auslöst, trifft mit deutlich höherer Wahrscheinlichkeit auf eine Lücke, als die Kennzahl vermuten lässt – und zwar mitten im Trade.
Was Forward-Fill drei Zeilen später anrichtet
Dieser Fehler hat mich zu diesem Text gebracht. Nehmen wir das Intervall mit 19 Minuten. Übliche Datenhygiene: auf das vollständige Minutengitter umindizieren, OHLC vom letzten Schlusskurs per Forward-Fill ergänzen und das Volumen auf null setzen. Jetzt ist die Reihe durchgehend, und deine Indikatoren laufen, ohne dass irgendwo ein NaN auftaucht.
Bei diesen 19 Balken gilt high == low == close. Die True Range ist bei jedem einzelnen gleich null. Ein ATR(14), der für dieses Intervall berechnet wird und von einem Wert kurz vor dem Ausfall von etwa 240 USDT kommt, sinkt bis zur Rückkehr der Börse auf ungefähr 34 – die 5 echten Balken, die im Intervall übrig sind, tragen den gesamten Durchschnitt. Gib diesen Wert nun in einen volatilitätsbasierten Positionsgrößenrechner ein, den üblichen vom Typ size = risk_budget / ATR. Die Positionsgröße steigt auf das Siebenfache.
Der nächste echte Balken ist der Kurs bei Wiedereröffnung, und der ist alles andere als ruhig. In unserem Fall eröffnete er 1.8% entfernt vom letzten Schlusskurs vor dem Ausfall. Der Backtest eröffnete prompt eine Position zum 7-Fachen, mitten in einer Lücke von 1.8% – zu einem Kurs, zu dem kein Fill möglich gewesen wäre und den niemand gestellt hatte. Dieser eine synthetische Trade war in der Equity-Kurve mehr wert als ein Monat legitimer P&L – in die falsche Richtung. Entstanden ist er allein durch eine Zeile zur Datenbereinigung, die den DataFrame ordentlich aussehen lassen sollte.
Die Zeilen zu löschen, statt sie aufzufüllen, ist auch keine Lösung. Es ist derselbe Fehler mit anderer Verkleidung. Lösche sie, und Lookbacks mit ganzzahligem Index führen in die Irre: Ein „20-Balken-EMA“ erstreckt sich nun über 39 Minuten Echtzeit hinweg über den Ausfall, die Rendite von Balken zu Balken über die Grenze hinweg behandelt den gesamten Sprung von 1.8% als Bewegung innerhalb einer Minute, und jede Volatilitätsschätzung pro Balken liest ihn als Ereignis mit 60 Sigma. Nichts weist dich darauf hin. Der Index ist weiterhin monoton.
Beim Resampling wird das Problem unsichtbar
Die meiste Forschung läuft nicht auf 1-Minuten-Balken, sondern auf aggregierten Daten. Durch die Aggregation wird das Problem glattgebügelt. Beim Resampling auf 5 Minuten wird aus einer Lücke von 19 Minuten eine Folge von 4 Balken, von denen der erste und der letzte unvollständig sind. Pandas berechnet aus 2 verbliebenen Minuten ein OHLC, das vollkommen plausibel aussieht, und kennzeichnet es genauso wie einen Balken aus 5 Minuten. Die Ausgabe lässt keinen Unterschied erkennen.
Die einfachste Lösung, die ich kenne: Führe eine Spalte bars_in_window durch jedes Resampling mit und wirf sie nie weg. Ein Integer pro Zeile reicht, damit sich jede nachgelagerte Frage zur Verlässlichkeit eines Balkens beantworten lässt. Außerdem führen wir seconds_since_last_real_print mit. Das sind dieselben Informationen in einer Form, mit der die Execution-Ebene etwas anfangen kann.
Unsere Richtlinie, soweit es eine gibt
So gehen unsere Agenten inzwischen vor, der Reihe nach:
- Niemals stillschweigend umindizieren. Der Loader gibt ein Lückenmanifest aus: Anfang, Ende, Länge und welche der 4 Kategorien vermutlich zutrifft. Fehlt eine Folge von weniger als 3 Balken und ist das Volumen in den angrenzenden Balken gering, handelt es sich um Minuten ohne Trades. Alles, was während aktiver Handelszeiten länger dauert, behandeln wir als Ausfall, bis das Gegenteil bewiesen ist.
- Vor der Analyse erneut abrufen. Die Hälfte unserer frühen Lücken ging auf Pagination-Fehler zurück. Ein zweiter Abruf über einen anderen Endpoint oder von einem anderen Anbieter klärt Kategorie vier und verkleinert das Problem, bevor wir etwas beurteilen müssen.
- Veraltungsschranke statt Lückenfüller. Die Strategie erhält einen
data_age-Eingabewert und eine klare Regel: keine neuen Positionen, wenn der letzte echte Kurs älter als N Balken ist. Offene Positionen werden bei der Wiedereröffnung nur per Market Order geschlossen, deren Preis einen ausdrücklich einkalkulierten Abschlag für das Lückenrisiko enthält. Unmögliche Fills sind schlimmer als Trades, die nicht stattgefunden haben. - Indikatoren bekommen NaN, keine Fiktion. Forward-gefüllte Preise gelangen nie in die Feature-Ebene. Lässt sich der ATR nicht berechnen, ist er nicht definiert – und nicht definiert bedeutet, dass wir keine Position halten. Ein deutlich sichtbarer Fehler ist besser als ein unbemerkter Faktor 7.
- Performance abhängig von Lücken ausweisen. Jeder Backtest, den wir veröffentlichen, zeigt neben dem Gesamtergebnis auch den P&L ohne Trades in zeitlicher Nähe zu Ausfällen. Tragen diese Trades das Ergebnis, ist es ein Artefakt der Daten.
Ein kurzes Audit, das du heute durchführen kannst: Fasse deine fehlenden Minuten zu aufeinanderfolgenden Intervallen zusammen. Prüfe dann, welcher Anteil der Trades in deinem Backtest innerhalb von 30 Minuten vor oder nach einer Intervallgrenze eröffnet oder geschlossen wird. Liegt der Anteil unter 1%, treiben die Lücken vermutlich nichts an. Liegt er bei 5% oder höher, erzählt die Equity-Kurve teilweise eine Geschichte über Ausfälle der Börse.
Ich achte inzwischen mehr auf das Muster der fehlenden Daten als auf ihre Anzahl. Ein Datensatz mit Tausenden vereinzelten Lücken in ruhigen Stunden ist meist in Ordnung. Ein Datensatz mit einigen wenigen eng beieinanderliegenden Lücken verrät dir, dass unter Last etwas ausfällt – und deine Strategie ist genau dort aktiv, wo dieses Etwas ausfällt.
← Alle Beiträge


