h5i-db für Point-in-Time Daten in Backtests nutzen
Point-in-Time Daten verhindern Look-ahead Bias bei Backtests auf Speicherebene. Erfahren Sie, wie h5i-db Schreibvorgänge versioniert und Meldeverzögerungen die Historie verfälschen.
2026-07-01
Point-in-Time-Daten sind eine Aufzeichnung des Bestands eines Datensatzes zu einem vergangenen Zeitpunkt. Bei einem Backtest ermöglichen sie die Unterscheidung zwischen einem validen Ergebnis und einem Resultat, das auf Informationen der Zukunft basiert. h5i-db ist eine junge Open-Source-Zeitreihendatenbank, die in Rust geschrieben ist und über eine Python-API verfügt. Sie speichert jeden Schreibvorgang als nummerierte Version und erlaubt es, bei jedem Lesevorgang einen früheren Stand festzulegen. Nachfolgend ist das Datenleck aufgeführt, das durch diese Fixierung verhindert wird, gemessen anhand realer Meldedaten, gefolgt von einem Szenario, das auf einem Laptop ausgeführt werden kann.
Datenlecks bei Backtests verhindern
Ein Backtest ist nur so verlässlich wie die Trennung zwischen dem Informationsstand zum Zeitpunkt der Entscheidung und den tatsächlichen Marktdaten. Ohne Point-in-Time-Funktionalität greifen Algorithmen oft auf Daten zu, die erst nach dem Handelszeitpunkt veröffentlicht wurden. Dies führt zu einer Verzerrung der Ergebnisse, da der Algorithmus auf Informationen reagiert, die zum damaligen Zeitpunkt noch nicht verfügbar waren.
Die Verwendung von h5i-db ermöglicht es, den Datenbestand exakt auf den Stand eines bestimmten Datums oder einer bestimmten Version zu fixieren. Dies verhindert, dass zukünftige Korrekturen oder verspätete Meldungen in die historische Analyse einfließen.
Hier finden Sie die technische Dokumentation zur Implementierung
Die Architektur von h5i-db stellt sicher, dass jede Änderung versioniert wird. Dies ist entscheidend für die Integrität von quantitativen Modellen, da es die Reproduzierbarkeit von Backtests unter realen Bedingungen gewährleistet.
Was ist Point-in-Time-Daten in einem Backtest?
Jeder Marktfakt besitzt zwei Zeitstempel. Die Ereigniszeit markiert den Zeitpunkt des tatsächlichen Geschehens. Die Ankunftszeit definiert den Moment, ab dem diese Information für Außenstehende außerhalb des berichtenden Unternehmens zugänglich war. Ein Quartalsbericht über Bestände beschreibt Positionen zum letzten Tag eines Quartals, wird jedoch erst Wochen später veröffentlicht. Ein Modell, das ausschließlich auf der Ereigniszeit basiert, greift somit auf Informationen zu, die zu diesem Zeitpunkt niemandem zur Verfügung standen.
Dieser zeitliche Abstand ist messbar. Institutionelle Verwalter reichen das Formular 13F nach Ende jedes Quartals ein. Die untenstehende Grafik misst die Anzahl der Tage zwischen dem Quartalsende, das ein Bericht beschreibt, und dem tatsächlichen Einreichungsdatum.
Das exakte SQL hinter jeder Zahl
WITH per_filing AS
(
SELECT
accession_number,
any(toDate(parseDateTimeBestEffortOrNull(toString(period)))) AS period_end,
any(toDate(filing_date)) AS filed_on
FROM global_markets.stocks_13f_filings
WHERE filing_date >= '2023-01-01'
GROUP BY accession_number
)
SELECT
toString(period_end) AS period_end_date,
countDistinct(accession_number) AS filings_count,
round(avg(dateDiff('day', period_end, filed_on)), 1) AS avg_days_to_public
FROM per_filing
WHERE period_end IS NOT NULL
AND filed_on >= period_end
AND filed_on <= period_end + 400
GROUP BY period_end
HAVING filings_count >= 100
ORDER BY period_endFür das Quartal mit Ende 2026-06-30 gingen 10688 Einreichungen durchschnittlich 34.1 Tage nach dem abgedeckten Zeitraum ein. Die Grafik wiederholt diese Messung über 16 Quartale. Da ein Verwalter einen Bericht auch lange nach der Einreichung korrigieren kann, verändert sich der Datensatz für ein vergangenes Datum kontinuierlich, selbst nachdem dieses Datum bereits verstrichen ist.
Look-ahead Bias ist ein Problem der Datenspeicherung
Unser Leitfaden zu Look-ahead Bias beim Backtesting behandelt Datenlecks als eine Frage der Disziplin: Verzögern Sie jedes Merkmal und berücksichtigen Sie die Veröffentlichungsdaten. Diese Disziplin hält so lange an, bis sie jemand vergisst, und das Versagen bleibt unbemerkt. Ein Backtest mit Datenlecks liefert eine bessere Sharpe Ratio ohne jegliche Fehlermeldung.
Point-in-Time-Speicherung verlagert die Garantie auf eine tiefere Ebene. Wenn der Datensatz, der einer Strategie übergeben wird, aus einem Lesezugriff stammt, der an eine bestimmte Version gebunden ist, kann eine Zeile, die erst nach dieser Version geschrieben wurde, nicht darin erscheinen – ungeachtet dessen, was der Strategiecode anschließend tut. Die Prüfung ist dann kein Code-Review mehr, sondern eine Eigenschaft des Lesezugriffs.
Dividenden verdeutlichen diese zeitliche Lücke von der anderen Seite. Eine Bardividende wird zuerst angekündigt und notiert erst später ex-Dividende. Eine heute geladene Tabelle enthält für jede Zahlung beide Daten, auch für solche, die zum Zeitpunkt der Simulation noch gar nicht angekündigt waren.
Das exakte SQL hinter jeder Zahl
SELECT
toString(toStartOfMonth(ex_dividend_date)) AS month,
round(avg(dateDiff('day', declaration_date, ex_dividend_date)), 1) AS avg_days_announced_ahead,
countDistinct(ticker) AS payers_count
FROM global_markets.stocks_dividends
WHERE ex_dividend_date >= toStartOfMonth(today() - 730)
AND ex_dividend_date < toStartOfMonth(today())
AND declaration_date >= '1990-01-01'
AND declaration_date <= ex_dividend_date
GROUP BY month
ORDER BY monthIn dem Monat, der am 2026-07-01 begann, lagen die Ankündigungen durchschnittlich 88.2 Tage vor dem Ex-Dividend-Date, und das Panel umfasst 24 Monate derselben Messung. Liest man eine moderne Dividendentabelle zu einem simulierten Datum innerhalb dieser Lücke, ist die Zahlung bereits dort hinterlegt – Wochen bevor die Ankündigung überhaupt existierte.
Historische Daten werden nachträglich angepasst
Verspätete Datenübermittlungen sind eine Fehlerquelle. Die nachträgliche Korrektur von Daten eine andere. Kapitalmaßnahmen verändern Kurse, die bereits als ausgeführt gemeldet wurden: Nach einem Aktiensplit im Verhältnis vier zu eins wird jeder frühere Kurs in einer bereinigten Zeitreihe durch vier geteilt. Die heute heruntergeladene Datenreihe stimmt somit nicht mehr mit dem Kursverlauf überein, den ein Händler zum damaligen Zeitpunkt beobachtet hat. Unsere Erläuterung zur splitbereinigten Kurshistorie verdeutlicht die zugrunde liegende Arithmetik. Entscheidend ist hier die Häufigkeit.
Das exakte SQL hinter jeder Zahl
SELECT
toString(toStartOfQuarter(execution_date)) AS quarter_start_date,
countDistinctIf(id, split_to > split_from) AS forward_splits,
countDistinctIf(id, split_to < split_from) AS reverse_splits
FROM global_markets.stocks_splits
WHERE execution_date >= toStartOfQuarter(today() - 1460)
AND execution_date < toStartOfQuarter(today())
AND split_from > 0
AND split_to > 0
GROUP BY quarter_start_date
ORDER BY quarter_start_dateIm Quartal beginnend mit 2026-04-01 wurden 131 Aktiensplits und 303 Aktienzusammenlegungen wirksam. Jede dieser Maßnahmen schreibt eine Kurshistorie um, die in einer Analyse-Pipeline bereits zwischengespeichert sein könnte. Eine versionierte Datenspeicherung verhindert diese nachträgliche Anpassung nicht. Sie zeichnet den neuen Zustand als neue Version auf und hält die alte Version lesbar. Genau das macht aus einem veralteten Ergebnis ein reproduzierbares.
Nachrichten-Zeitstempel bergen dieselbe Gefahr im Kleinen.
Das exakte SQL hinter jeder Zahl
SELECT
formatDateTime(toTimeZone(published_utc, 'America/New_York'), '%H:00') AS et_hour,
countDistinct(id) AS articles
FROM global_markets.stocks_news
WHERE published_utc >= today() - 90
GROUP BY et_hour
ORDER BY et_hourSchlagzeilen werden rund um die Uhr über alle 24 Stunden des New Yorker Handelstages veröffentlicht. In der Stunde um 09:00 Uhr wurden in den vergangenen neunzig Tagen 790 Artikel publiziert, in der Stunde um 20:00 Uhr waren es 404. Wer eine abendliche Schlagzeile mit dem Schlusskurs des Tages um 16:00 Uhr verknüpft, verschafft einer Handelsstrategie, die auf den Schlusskurs setzt, einen unzulässigen Wissensvorsprung von mehreren Stunden.
Ein Point-in-Time-Szenario zur Durchführung
Alles hier verbleibt im Python-Paket. Das Projekt liefert zudem ein Rust-Kommandozeilen-Tool aus, eine separate Installation, die für diese Anleitung nicht erforderlich ist. Die Beispieldaten werden lokal generiert, ohne Download.
- Installieren Sie das fixierte Release:
pip install 'h5i-db==0.1.6', veröffentlicht am vierten August zweitausendsechsundzwanzig. Es erfordert Python 3.9 oder neuer und bringtpyarrow>=14mit. Vorkompilierte Wheels decken Linux auf x86-64 und arm64 ab, sowie Apple Silicon macOS und Windows auf x86-64. - Beschreiben Sie die Daten einmal, nach
import pyarrow as paundimport pyarrow.parquet as pq:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]). - Schreiben Sie zwei erfundene Zeilen mit
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet')in eine lokale Datei, wobeid1undd2zeitzonenabhängige Datums- und Zeitangaben sind. - Erstellen Sie die Datenbank und die Tabelle und benennen Sie die Zeitspalte:
db = h5i_db.Database('pit.db', create=True)und danndb.create_table('prices', schema, time_column='ts'). - Ingestieren Sie die Datei unter einem Schlüssel:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Der Aufruf gibt den erstellten Commit zurück. - Führen Sie genau diese Zeile erneut aus. Das Projekt dokumentiert, dass eine Wiederholung mit demselben Schlüssel den bereits erzeugten Commit findet und diesen mit
"segments_added": 0zurückgibt, anstatt die Zeilen ein zweites Mal zu schreiben. Geben Siedb.versions('prices')vor und nach dem erneuten Versuch aus und beobachten Sie, wie die Versionsliste unverändert bleibt. - Ingestieren Sie einen zweiten Tag unter
idempotency_key='load-day2'und fragen Sie dann beide ab:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Lesen Sie die Tabelle in dem Zustand, in dem sie sich vor dem zweiten Tag befand:
db.read('prices', version=1). Dieselbe Methode akzeptiertas_of=undsnapshot=als Argumente für dieselbe Aufgabe.
Schritt sechs ist derjenige, mit dem man sich befassen sollte. Ein duplizierter Append löst keinen Fehler aus. Er hinterlässt die Tabelle ab diesem Moment in einem fehlerhaften Zustand, und jeder nachfolgende Durchlauf übernimmt den Schaden stillschweigend. Schritt acht ist der entscheidende Vorteil: Eine im März berechnete Zahl kann im August mit derselben fixierten Version neu berechnet werden – die Eigenschaft, für die unser Bericht über einen reproduzierbaren Backtest auf Framework-Ebene plädiert.
Was das Projekt behauptet und was wir überprüft haben
Die README-Datei beginnt mit einem Benchmark:
über 4,5-mal schneller als DuckDB und Polars bei OHLCV+VWAP-Rollups über 20 Millionen Zeilen
Dieser Wert ist eine eigene Messung des Projekts, zitiert aus der h5i-db README mit Stand August 2026. Wir haben diesen Test nicht selbst durchgeführt, und die obigen Ausführungen hängen nicht davon ab.
Reife ist hier wichtiger als Geschwindigkeit. Das Repository verzeichnete zum Zeitpunkt der Erstellung 29 Sterne und befindet sich unter der Apache-2.0-Lizenz in der Version 0,1,6. Diese Kombination deutet auf einen kleinen Kreis von Betreuern hin und auf eine API, die sich zwischen den Punkt-Releases noch ändern kann. Es gibt keine langjährige öffentliche Historie über den Betrieb der Engine unter Last. Das Festschreiben der exakten Version und das Aufbewahren der Parquet-Dateien, die die Datenbank speisen, lässt einen Rückweg offen, falls ein Release das Verhalten ändert. Eine eigene Rohdaten-Kopie vorzuhalten, ist dieselbe Vorsichtsmaßnahme, die vor Anbietern schützt, die im Hintergrund die Historie verändern – ein Thema, das sich durch Survivorship-Bias bei Aktiendaten zieht.
Häufig gestellte Fragen
Was sind Point-in-Time-Daten?
Point-in-Time-Daten sind Datensätze, die mit Zeitstempeln gespeichert werden, die den Zeitpunkt markieren, zu dem ein Faktum bekannt wurde. Eine Abfrage kann so rekonstruieren, welche Informationen an einem beliebigen vergangenen Datum verfügbar waren. Eine einfache Tabelle mit den aktuellsten Werten kann dies nicht, da sie vergangene Daten mit den korrigierten Zahlen von heute überschreibt.
Beseitigt versionierte Speicherung den Look-Ahead-Bias?
Nein. Die Versionierung behebt eine Art von Datenleck, bei der ein Modell Werte liest, die erst nach dem simulierten Entscheidungszeitpunkt geschrieben wurden. Bei der Erstellung von Merkmalen können jedoch weiterhin Lecks auftreten, etwa wenn eine Stichprobe anhand von Statistiken skaliert wird, die über den gesamten Verlauf berechnet wurden.
Welche Funktion hat ein Idempotency-Key beim Ingest?
Er kennzeichnet einen Schreibvorgang, sodass ein erneuter Versuch als derselbe Schreibvorgang erkannt wird. Ein Loader kann nach einem Absturz neu gestartet werden, ohne dieselben Zeilen doppelt anzuhängen. Dies verhindert den Fehler, der eine Tabelle unbemerkt verfälschen würde.
Ist h5i-db produktionsreif?
Es handelt sich um die Version 0,1,6 mit neunundzwanzig Sternen auf GitHub zum Zeitpunkt der Erstellung, lizenziert unter Apache-2,0. Frühe Software dieser Größenordnung unterliegt häufigen API-Änderungen und verfügt über eine geringe öffentliche Erfolgsbilanz. Eine feste Versionierung sowie eine eigene Kopie der Quelldateien stellen sicher, dass eine Evaluierung nachvollziehbar bleibt.
Jedes der oben genannten Panels enthält den SQL-Code, mit dem es erstellt wurde. Öffnen Sie ein Panel, um nachzuvollziehen, wie die Kennzahl berechnet wurde. Dieselben Fragen können in einfacher Sprache über das Strasmore-Terminal gestellt werden.