Strasmore Research
Leren Matt ConnorDoor Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: Point-in-Time data voor backtests

Point-in-time data voorkomt look-ahead bias in backtests op de opslaglaag. Lees hoe h5i-db elke write versieert en wat filing lag zegt over de betrouwbaarheid van historische data.

Point-in-time data en het voorkomen van datalekken

Point-in-time data is een vastlegging van de inhoud van een dataset op een datum in het verleden. Bij een backtest zorgt dit ervoor dat een resultaat verdedigbaar is, in plaats van dat het stilletjes gebruikmaakt van cijfers uit de toekomst. h5i-db is een jonge open-source time-series database, geschreven in Rust met een Python API. Deze database slaat elke schrijfactie op als een genummerde versie en stelt de gebruiker in staat om bij elke leesactie een eerdere versie vast te pinnen. Hieronder volgt de lekkage die het pinnen blokkeert, gemeten aan de hand van werkelijke rapportagedata, gevolgd door een scenario dat u op een laptop kunt uitvoeren.

De impact van datalekken in backtests

Wanneer een systeem bij het berekenen van een historische waarde onbedoeld toegang heeft tot informatie die op dat moment nog niet beschikbaar was, spreken we van 'look-ahead bias'. Dit is een veelvoorkomende fout in kwantitatieve modellen. Door gebruik te maken van point-in-time versies, wordt de database gedwongen om alleen de data te zien die op de betreffende datum bekend was. Dit voorkomt dat het model "in de toekomst kijkt" en zorgt voor een betrouwbaardere evaluatie van de strategie.

Praktijkvoorbeeld: scenario voor uw laptop

Om dit in de praktijk te brengen, kunt u de volgende stappen volgen met h5i-db. Dit scenario demonstreert hoe u een dataset bevriest op een specifiek tijdstip om een zuivere backtest uit te voeren.

  • Installeer de h5i-db bibliotheek via de officiële repository.
  • Importeer de historische dataset met de bijbehorende versienummers.
  • Voer uw query uit door de database te pinnen op een specifieke versie (bijvoorbeeld 2026-07-01).
  • Vergelijk het resultaat met een query zonder pinning om het verschil in uitkomst te observeren.

Door deze methode toe te passen, elimineert u de ruis die ontstaat door het onbedoeld opnemen van latere correcties of updates in uw historische analyse. Dit proces is essentieel voor het valideren van financiële modellen die gebaseerd zijn op tijdreeksen.

Wat is point-in-time data in een backtest?

Elk marktgegeven heeft twee tijdstempels. Event time is het moment waarop de gebeurtenis plaatsvond. Arrival time is het moment waarop het voor buitenstaanders van de rapporterende partij kenbaar werd. Een kwartaalrapportage over beleggingsposities beschrijft de posities op de laatste dag van een kwartaal en bereikt het publiek pas weken later. Een model dat enkel op basis van event time koppelt, krijgt dus informatie die op dat moment voor niemand beschikbaar was.

Dit tijdsverschil is meetbaar. Institutionele beheerders dienen na afloop van elk kwartaal een Form 13F in. Het onderstaande overzicht meet het aantal dagen tussen het kwartaal dat een melding beschrijft en de dag van indiening.

QueryTijdsduur voor publicatie van 13F-houderschapsmeldingen
De exacte SQL achter elk getal
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_end
Run this yourself

Voor het kwartaal eindigend op 2026-06-30 kwamen de 10688-meldingen gemiddeld 34.1 dagen na de betreffende periode binnen. Het overzicht herhaalt die meting over 16 kwartalen. Een beheerder kan een rapport bovendien lang na de indiening nog wijzigen. Hierdoor blijft het dossier over een datum uit het verleden veranderen, zelfs nadat die datum is gepasseerd.

Look-ahead bias is een opslagprobleem

Onze gids over look-ahead bias in backtesting behandelt datalekken als een kwestie van discipline: vertraag elke variabele en respecteer publicatiedata. Deze discipline houdt stand totdat iemand het vergeet, en de fout blijft onopgemerkt. Een backtest met een datalek laat een betere Sharpe ratio zien zonder dat er een foutmelding optreedt.

Point-in-time opslag verplaatst de garantie naar een lager niveau. Wanneer het databestand dat aan een strategie wordt verstrekt, afkomstig is van een read die is vastgepind op een specifieke versie, kan een rij die na die versie is geschreven daar niet in verschijnen, ongeacht wat de strategiecode daarna doet. De controle is dan geen kwestie meer van code review, maar een eigenschap van de read zelf.

Dividenden tonen de timingkloof vanuit het andere perspectief. Een contant dividend wordt eerst aangekondigd en gaat pas later ex-dividend. Een tabel die vandaag wordt geladen, bevat beide data voor elke betaling, inclusief betalingen die op de gesimuleerde datum nog niet waren aangekondigd.

QueryAantal dagen tussen dividenddeclaratie en ex-dividenddatum, per maand
De exacte SQL achter elk getal
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 month
Run this yourself

In de maand die begon op 2026-07-01, werden aankondigingen gemiddeld 88.2 dagen vóór de ex-dividenddatum gedaan, en het panel beslaat 24 maanden van dezelfde meting. Als u een moderne dividendtabel leest op een gesimuleerde datum binnen die periode, staat de betaling er al in, weken voordat de aankondiging bestond.

Historische data worden herschreven

Een late aankomst is één type fout. Herberekening is de andere. Corporate actions herschrijven koersen die al zijn uitgevoerd: na een 4-voor-1 split wordt elke eerdere koers in een gecorrigeerde reeks door vier gedeeld, waardoor de reeks die vandaag wordt gedownload niet langer overeenkomt met de tape die een trader destijds zag. Onze notitie over split-gecorrigeerde koershistorie behandelt de rekenkunde hierachter. Wat hier van belang is, is de frequentie.

QueryAantal stock splits per kwartaal, forward en reverse
De exacte SQL achter elk getal
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_date
Run this yourself

In het kwartaal dat begon in 2026-04-01, werden 131 forward splits en 303 reverse splits van kracht. Elk daarvan herschrijft een koershistorie die een onderzoekspijplijn mogelijk al in de cache heeft staan. Versiebeheer in opslag voorkomt de herberekening niet. Het legt de nieuwe status vast als een nieuwe versie en houdt de oude leesbaar, wat een verouderd resultaat verandert in een reproduceerbaar resultaat.

Nieuwstijdstempels bevatten in het klein dezelfde valkuil.

QueryPublicatietijdstip van marktkoppen, per uur (New York-tijd)
De exacte SQL achter elk getal
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_hour
Run this yourself

Koppen verschijnen de klok rond, gedurende alle 24 uren van de handelsdag in New York. Het uur van 09:00 uur bevatte 790 artikelen over de afgelopen negentig dagen, en het uur van 20:00 uur bevatte 404. Het toewijzen van een avondkop aan de slotkoers van 16:00 uur van die dag geeft een strategie die op de slotkoers handelt, enkele uren aan achterafkennis.

Een point-in-time scenario dat u kunt uitvoeren

Alles in dit document blijft beperkt tot het Python-pakket. Het project levert ook een Rust-commandlinetool, een afzonderlijke installatie die voor deze handleiding niet nodig is. De voorbeelddata wordt lokaal gegenereerd, zonder downloads.

  1. Installeer de vastgelegde release: pip install 'h5i-db==0.1.6', gepubliceerd op vier augustus tweeduizendzesentwintig. Deze vereist Python 3.9 of nieuwer en bevat pyarrow>=14. Vooraf gebouwde wheels ondersteunen Linux op x86-64 en arm64, evenals Apple silicon macOS en Windows op x86-64.
  2. Beschrijf de data eenmalig, na import pyarrow as pa en import pyarrow.parquet as pq: schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]).
  3. Schrijf twee verzonnen rijen naar een lokaal bestand met pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), waarbij d1 en d2 timezone-aware datetimes zijn.
  4. Maak de database en de tabel aan en benoem de tijdkolom: db = h5i_db.Database('pit.db', create=True) en vervolgens db.create_table('prices', schema, time_column='ts').
  5. Ingest het bestand onder een key: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). De aanroep retourneert de commit die is aangemaakt.
  6. Voer diezelfde regel opnieuw uit. Het project documenteert dat een herhaling met dezelfde key de reeds geproduceerde commit vindt en deze retourneert met "segments_added": 0 in plaats van de rijen een tweede keer te schrijven. Print db.versions('prices') aan beide kanten van de retry en zie dat de versielijst ongewijzigd blijft.
  7. Ingest een tweede dag onder idempotency_key='load-day2' en voer vervolgens een query uit over beide: db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas().
  8. Lees de tabel zoals deze was voordat dag twee werd toegevoegd: db.read('prices', version=1). Dezelfde methode gebruikt as_of= en snapshot= argumenten voor dezelfde taak.

Stap zes is de belangrijkste om bij stil te staan. Een dubbele append veroorzaakt geen foutmelding. Het laat de tabel vanaf dat moment in een onjuiste staat achter en elke daaropvolgende run erft de schade geruisloos. Stap acht is het resultaat: een getal dat in maart is berekend, kan in augustus opnieuw worden berekend op basis van dezelfde vastgelegde versie; dit is de eigenschap waarvoor ons artikel over een reproduceerbare backtest op frameworkniveau pleit.

Wat het project claimt en wat wij hebben gecontroleerd

De README opent met een benchmark:

ruim 4,5 keer sneller dan DuckDB en Polars bij OHLCV+VWAP-rollups over twintig miljoen rijen

Dit cijfer is een eigen meting van het project, geciteerd uit de h5i-db README zoals geraadpleegd in augustus 2026. Wij hebben deze test niet zelf uitgevoerd en niets van het bovenstaande is daarvan afhankelijk.

Volwassenheid is hier belangrijker dan snelheid. De repository had op het moment van schrijven negenentwintig sterren en bevindt zich op versie 0.1.6 onder de Apache-2.0 licentie. Deze combinatie wijst op een kleine groep beheerders en een API die tussen punt-releases nog kan veranderen. Er is geen langdurig publiek trackrecord van de engine onder belasting. Het vastzetten van de exacte versie en het bewaren van de parquet-bestanden die de database hebben gevoed, biedt een uitweg als een release het gedrag verandert. Het aanhouden van een eigen ruwe kopie is dezelfde gewoonte die beschermt tegen een leverancier die de geschiedenis onder uw voeten herschrijft; dit is het thema dat door survivorship bias in koersdata loopt.

Veelgestelde vragen

Wat is point-in-time data?

Point-in-time data is een dataset die wordt opgeslagen met de tijdstempels waarop elk feit bekend werd, zodat een query kan reconstrueren wat op een willekeurige datum in het verleden zichtbaar was. Een standaard tabel met alleen de "meest recente waarde" kan dat niet, omdat deze het verleden overschrijft met de gecorrigeerde cijfers van vandaag.

Elimineert versiebeheer in opslag look-ahead bias?

Nee. Versiebeheer lost één type datalek op, namelijk het type waarbij een run waarden leest die pas na het gesimuleerde beslismoment zijn weggeschreven. Bij het construeren van kenmerken kan nog steeds op andere manieren informatie lekken, bijvoorbeeld door een sample te schalen op basis van statistieken die over de volledige historie zijn berekend.

Wat doet een idempotency key tijdens het inladen?

Deze voorziet een schrijfactie van een label, zodat een herhaalde poging wordt herkend als dezelfde schrijfactie. Een loader kan na een crash opnieuw worden uitgevoerd zonder dezelfde rijen dubbel toe te voegen; dit is de fout die ervoor zorgt dat een tabel stilletjes onjuiste gegevens bevat.

Is h5i-db klaar voor productie?

Het betreft versie 0.1.6 met negenentwintig sterren op GitHub op het moment van schrijven, onder de Apache-2.0 licentie. Vroege software van die omvang brengt wijzigingen in de API en een beperkte publieke staat van dienst met zich mee. Een versie-pin en een eigen kopie van de bronbestanden zorgen ervoor dat een evaluatie reproduceerbaar blijft.


Bij elk bovenstaand paneel wordt de SQL geleverd die het resultaat heeft gegenereerd; open er een en lees hoe het cijfer is berekend. Dezelfde vragen kunnen in begrijpelijk Engels worden gesteld op de Strasmore-terminal.