h5i-db: Point-in-Time Data para sa Backtests
Pinipigilan ng point-in-time data ang look-ahead bias sa backtest. Alamin kung paano bina-version ng h5i-db ang bawat write at sinusukat ang filing lag sa lumang history.
Ang point-in-time data ay tala ng nilalaman ng isang dataset sa isang nakaraang petsa. Sa backtest, tinutukoy nito kung ang resulta ay mapangangatwiranan o tahimik na gumamit ng mga numero mula sa hinaharap. Ang h5i-db ay isang bagong open-source time-series database na isinulat sa Rust at may Python API. Iniimbak nito ang bawat write bilang may numerong version at nagbibigay-daan na i-pin ng anumang read ang isang mas naunang version. Narito ang leakage na hinaharangan ng pag-pin, batay sa aktuwal na filing data, at kasunod nito ang isang scenario na maaari mong patakbuhin sa laptop.
Ano ang point-in-time data sa isang backtest?
May dalawang timestamp ang bawat market fact. Ang event time ay kung kailan nangyari ang isang bagay. Ang arrival time ay kung kailan ito naging available sa sinumang nasa labas ng firm na nag-ulat nito. Inilalarawan ng quarterly holdings report ang mga position na hawak sa huling araw ng isang quarter, ngunit umaabot ito sa publiko pagkalipas pa ng ilang linggo. Kaya kapag event time lang ang ginamit ng model sa pag-uugnay ng data, nabibigyan ito ng impormasyong wala pa sa sinuman noong panahong iyon.
Nasusukat ang pagitan ng dalawang timestamp. Naghahain ang institutional managers ng Form 13F pagkatapos ng pagtatapos ng bawat quarter, at sinusukat ng panel sa ibaba ang bilang ng mga araw mula sa quarter na inilalarawan ng filing hanggang sa araw ng paghahain nito.
Ang eksaktong SQL sa likod ng bawat numero
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_endPara sa quarter na nagtapos noong 2026-06-30, dumating ang mga filing ng 10688, sa average, makalipas ang 34.1 araw mula sa panahong saklaw ng mga ito. Inuulit ng panel ang pagsukat na ito sa loob ng 16 quarters. Maaari ring amyendahan ng isang manager ang report matagal na matapos itong maihain. Dahil dito, patuloy na nagbabago ang record na naglalarawan sa isang nakaraang petsa kahit lumipas na ang petsang iyon.
Ang look-ahead bias ay problema sa storage
Itinuturing ng aming gabay tungkol sa look-ahead bias sa backtesting ang leakage bilang usapin ng disiplina: i-lag ang bawat feature at sundin ang mga petsa ng publication. Gumagana ang disiplina hanggang sa may makalimot, at tahimik ang pagkakamali. Ang backtest na may leakage ay maaaring magpakita ng mas mataas na Sharpe ratio nang walang anumang error.
Inililipat ng point-in-time storage ang garantiya sa mas mababang layer. Kapag ang frame na ipinasa sa strategy ay nagmula sa read na naka-pin sa isang version, hindi maaaring lumitaw rito ang row na isinulat pagkatapos ng version na iyon, anuman ang gawin ng strategy code. Hindi na code review ang pangunahing pagsusuri; nagiging property na ito ng read.
Ipinapakita ng dividends ang agwat sa timing mula sa kabilang panig. Idinedeklara muna ang cash dividend at nagiging ex-dividend sa mas huling petsa, habang ang table na nilo-load ngayon ay naglalaman ng parehong petsa para sa bawat payment, kabilang ang mga hindi pa naaanunsyo sa simulated date.
Ang eksaktong SQL sa likod ng bawat numero
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 monthSa buwan na nagsisimula sa 2026-07-01, ang mga declaration ay dumating nang average na 88.2 araw bago ang ex-dividend date, at saklaw ng panel ang 24 buwan ng parehong measurement. Kapag nagbasa ka ng modern dividend table sa simulated date na nasa loob ng agwat na ito, naroon na ang payment ilang linggo bago pa umiiral ang announcement.
Muling sinusulat ang nakaimbak na history
Ang late arrival ay isang failure mode. Ang restatement ang isa pa. Binabago ng corporate actions ang mga presyong na-print na: pagkatapos ng 4-for-1 split, hahatiin sa apat ang bawat naunang presyo sa adjusted series, kaya hindi na tugma ang series na dina-download ngayon sa tape na minamanmanan ng trader. Tinalakay sa aming note tungkol sa split-adjusted price history ang pagkukuwenta. Ang mahalaga rito ay ang dalas.
Ang eksaktong SQL sa likod ng bawat numero
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_dateSa quarter na nagsimula noong 2026-04-01, naging epektibo ang 131 forward splits at 303 reverse splits. Bawat isa ay nagre-restate ng price history na maaaring naka-cache na sa research pipeline. Hindi pinipigilan ng versioned storage ang restatement. Itinatala nito ang bagong state bilang bagong version at nananatiling nababasa ang luma, kaya nagiging reproducible ang isang stale result.
May katulad na bitag, sa mas maliit na anyo, ang mga timestamp ng balita.
Ang eksaktong SQL sa likod ng bawat numero
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_hourLumilitaw ang headlines sa buong maghapon, sa lahat ng 24 oras ng New York day. Ang 09:00 hour ay may 790 articles sa nakalipas na 90 days, habang ang 20:00 hour ay may 404. Kapag inilagay ang timestamp ng evening headline sa 4:00 p.m. close ng araw na iyon, binibigyan nito ng ilang oras na hindsight ang isang strategy na nagte-trade sa close.
Isang point-in-time scenario na maaari mong patakbuhin
Python package lamang ang kailangan sa lahat ng hakbang dito. May kasama ring Rust command-line tool ang project, pero hiwalay itong ini-install at hindi kailangan sa walkthrough na ito. Lokal na ginagawa ang sample data at walang dina-download.
- I-install ang pinned release:
pip install 'h5i-db==0.1.6', na inilathala noong 4 August 2026. Kailangan nito ng Python 3.9 o mas bago at kasama nito angpyarrow>=14. May prebuilt wheels para sa Linux sa x86-64 at arm64, gayundin sa Apple silicon macOS at Windows sa x86-64. - Ilarawan ang data nang isang beses, pagkatapos ng
import pyarrow as paatimport pyarrow.parquet as pq:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]). - Sumulat ng dalawang imbentong row sa isang lokal na file gamit ang
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), kung saan angd1atd2ay timezone-aware datetimes. - Gawin ang database at table, at pangalanan ang time column:
db = h5i_db.Database('pit.db', create=True), pagkatapos aydb.create_table('prices', schema, time_column='ts'). - I-ingest ang file gamit ang isang key:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Ibinabalik ng call ang commit na ginawa nito. - Patakbuhin muli ang eksaktong linyang iyon. Ayon sa dokumentasyon ng project, kapag inulit ito gamit ang parehong key, hahanapin nito ang commit na nagawa na nito at ibabalik iyon gamit ang
"segments_added": 0sa halip na isulat muli ang mga row. I-print angdb.versions('prices')sa magkabilang panig ng retry at tingnan kung hindi nagbabago ang version list. - I-ingest ang ikalawang araw gamit ang
idempotency_key='load-day2', pagkatapos ay mag-query sa parehong araw:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Basahin ang table ayon sa kalagayan nito bago maidagdag ang ikalawang araw:
db.read('prices', version=1). Tumatanggap ang parehong method ngas_of=atsnapshot=arguments para sa parehong gawain.
Ang Step 6 ang dapat pagtuunan ng pansin. Walang error na ibinibigay kapag na-duplicate ang append. Nagiging mali ang table mula sa puntong iyon, at tahimik na namamana ng bawat susunod na run ang pinsala. Ang Step 8 ang pakinabang: maaaring muling kalkulahin sa August ang isang numerong kinalkula noong March gamit ang parehong pinned version. Ito ang property na ipinaglalaban sa antas ng framework ng aming write-up tungkol sa isang reproducible backtest.
Mga sinasabi ng proyekto, at ang aming sinuri
Nangunguna sa README ang isang benchmark:
higit sa 4.5× na mas mabilis kaysa DuckDB at Polars sa OHLCV+VWAP rollups sa 20M rows
Sariling sukat ito ng proyekto, na sinipi mula sa h5i-db README batay sa bersyong nakuha noong August 2026. Hindi namin ito pinatakbo, at walang bahagi ng nasa itaas ang nakadepende rito.
Mas mahalaga rito ang maturity kaysa bilis. May 29 stars ang repository noong isinulat ito, at nasa version 0.1.6 ito sa ilalim ng Apache-2.0 licence. Ibig sabihin, maliit ang pool ng mga maintainer at maaari pang magbago ang API sa pagitan ng mga point release. Wala pang mahabang pampublikong rekord ng engine na tumatakbo sa ilalim ng load. Kapag naka-pin ang eksaktong version at itinatago ang mga parquet file na ginamit sa database, mayroon kang fallback kung magbago ang behavior ng isang release. Ang pagtatago ng sarili mong raw copy ay kaparehong kasanayang nagpoprotekta laban sa vendor na binabago ang historical data nang hindi mo namamalayan—ang temang tinatalakay sa survivorship bias sa stock data.
Mga FAQ
Ano ang point-in-time data?
Ang point-in-time data ay dataset na naka-store kasama ang timestamp kung kailan naging available ang bawat fact. Dahil dito, maaaring i-reconstruct ng query kung ano ang nakikita sa anumang petsa sa nakaraan. Hindi ito kayang gawin ng simpleng table na may “latest value” dahil pinapalitan nito ang mga dating value ng mga na-correct na numero ngayon.
Inaalis ba ng versioned storage ang look-ahead bias?
Hindi. Inaayos ng versioning ang isang uri ng leakage: kapag nakakabasa ang isang run ng mga value na naisulat pagkatapos ng simulated decision time. Maaari pa ring magkaroon ng leakage sa ibang paraan sa feature construction. Halimbawa, maaaring i-scale ang sample gamit ang statistics na kinalkula mula sa buong history nito.
Ano ang ginagawa ng idempotency key habang nag-i-ingest?
Nilalagyan nito ng label ang isang write upang makilala ang retry bilang kaparehong write. Dahil dito, maaaring patakbuhing muli ng loader ang proseso pagkatapos ng crash nang hindi nadodoble ang pagdagdag ng parehong rows. Ang ganitong failure ang maaaring mag-iwan sa isang table na mali nang hindi agad napapansin.
Handa na ba ang h5i-db para sa production?
Version 0.1.6 ito at may 29 stars sa GitHub sa oras ng pagsulat, sa ilalim ng Apache-2.0. Ang early software na ganito ang laki ay maaaring magkaroon ng madalas na pagbabago sa API at limitado pang public track record. Ang pag-pin ng version at pagkakaroon ng sarili mong kopya ng source files ang tumutulong upang maibalik sa dating setup ang evaluation.
Kasama sa bawat panel sa itaas ang SQL na gumawa nito. Buksan ang isang panel upang makita kung paano kinalkula ang numero. Maaari ring itanong sa plain English ang parehong mga tanong sa Strasmore terminal.