Strasmore Research
शिक्षण Matt Connorद्वारे Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db point-in-time डेटाने backtest look-ahead bias टाळा

h5i-db प्रत्येक writeची आवृत्ती साठवून readला जुन्या स्थितीशी pin करते. Filing lagमधील stale historyमुळे backtestमधील look-ahead bias कसा टळतो ते जाणून घ्या.

Point-in-time data म्हणजे एखाद्या datasetमध्ये भूतकाळातील ठरावीक तारखेला असलेली नोंद. Backtestमध्ये यामुळे समर्थनीय निकाल आणि नकळत भविष्यातील आकडे वापरून मिळालेला निकाल यांच्यात फरक करता येतो. h5i-db हा Rustमध्ये लिहिलेला आणि Python API असलेला नवीन open-source time-series database आहे. तो प्रत्येक write numbered version म्हणून साठवतो आणि कोणत्याही readला पूर्वीच्या आवृत्तीशी pin करण्याची सुविधा देतो. खाली, प्रत्यक्ष filing dataवर मोजलेली, या pinningमुळे रोखली जाणारी leakage दिली आहे. त्यानंतर laptopवर चालवता येईल असा एक scenario दिला आहे.

बॅकटेस्टमध्ये point-in-time डेटा म्हणजे काय?

प्रत्येक बाजार-संबंधित तथ्याला दोन timestamp असतात. Event time म्हणजे घटना घडलेली वेळ. Arrival time म्हणजे ती माहिती अहवाल देणाऱ्या संस्थेबाहेरील कोणालाही उपलब्ध होण्याची वेळ. तिमाही holdings reportमध्ये तिमाहीच्या शेवटच्या दिवशी असलेल्या positionsचे वर्णन असते; मात्र तो अहवाल त्यानंतर काही आठवड्यांनी सार्वजनिक होतो. त्यामुळे केवळ event timeवर join करणाऱ्या मॉडेलला त्या वेळी कोणाकडेही उपलब्ध नसलेली माहिती दिली जाते.

हा कालावधी मोजता येतो. Institutional managers प्रत्येक तिमाही संपल्यानंतर Form 13F दाखल करतात. खालील panelमध्ये filing ज्या तिमाहीचे वर्णन करते आणि ती filing ज्या दिवशी दाखल झाली, त्या दोन तारखांमधील दिवस मोजले आहेत.

क्वेरी13F होल्डिंग्ज फाइलिंग सार्वजनिक होण्यास लागणारा कालावधी
प्रत्येक आकड्यामागील अचूक SQL
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

2026-06-30ला संपलेल्या तिमाहीसाठी 10688 filingsमध्ये त्या covered periodनंतर सरासरी 34.1 दिवसांनी माहिती उपलब्ध झाली. हा panel 16 तिमाहींमध्ये हीच मोजणी पुन्हा दाखवतो. Manager filing केल्यानंतर बराच काळ उलटल्यानंतरही reportमध्ये दुरुस्ती करू शकतो. त्यामुळे भूतकाळातील एखाद्या तारखेचे वर्णन करणारा record ती तारीख निघून गेल्यानंतरही बदलत राहतो.

Look-ahead bias ही डेटा-साठवणीची समस्या आहे

Backtestingमधील look-ahead biasवरील आमच्या मार्गदर्शकात data leakageला शिस्तीचा विषय मानले आहे: प्रत्येक featureला lag द्या आणि publication datesचा आदर करा. मात्र कोणी विसरले की ही शिस्त अपयशी ठरते. हे अपयश शांतपणे घडते. Leaked backtestमध्ये Sharpe ratio अधिक चांगला दिसतो आणि कोणतीही error येत नाही.

Point-in-time storage ही हमी आणखी एका स्तरावर लागू करते. Strategyला दिलेला frame एखाद्या versionला pin केलेल्या readमधून आला असेल, तर त्या versionनंतर लिहिलेली row त्यात येऊ शकत नाही, strategy codeने त्यानंतर काहीही केले तरी. त्यामुळे तपासणी code reviewपुरती मर्यादित राहत नाही; ती readच्या गुणधर्माचा भाग बनते.

Dividendsच्या बाबतीत timing gap उलट दिशेने स्पष्ट दिसतो. Cash dividend आधी घोषित केला जातो आणि ex-dividend date नंतर येते. आज load केलेल्या tableमध्ये प्रत्येक paymentसाठी या दोन्ही तारखा असतात. त्यात simulated dateला ज्या paymentsची घोषणा झालेली नव्हती, त्यांच्याही तारखा समाविष्ट असतात.

क्वेरीलाभांश जाहीर केल्यापासून ex-date पर्यंतचे दिवस, महिन्यानुसार
प्रत्येक आकड्यामागील अचूक SQL
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

2026-07-01पासून सुरू होणाऱ्या महिन्यात declarations या ex-dividend dateच्या सरासरी 88.2 दिवस आधी झाल्या. त्याच मोजमापात panelमध्ये 24 महिन्यांचा कालावधी समाविष्ट आहे. या gapमधील एखाद्या simulated dateला modern dividend table वाचा. घोषणा अस्तित्वात येण्याच्या अनेक आठवडे आधीच payment त्यात नोंदलेले दिसेल.

साठवलेला इतिहास पुन्हा लिहिला जातो

उशिरा येणारा डेटा हा एक अपयशाचा प्रकार आहे. Restatement हा दुसरा. Corporate actions आधीच नोंदल्या गेलेल्या किमतींची पुनर्रचना करतात: 4-for-1 splitनंतर adjusted seriesमधील प्रत्येक आधीची किंमत चारने भागली जाते आणि आज डाउनलोड केलेली series ट्रेडरने पाहिलेल्या tapeशी जुळत नाही. split-adjusted price historyवरील आमच्या टिपणात ही गणना सविस्तर दिली आहे. येथे महत्त्वाचा मुद्दा म्हणजे वारंवारता.

क्वेरीप्रत्येक तिमाहीत लागू होणारे स्टॉक स्प्लिट्स, forward आणि reverse
प्रत्येक आकड्यामागील अचूक SQL
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

2026-04-01पासून सुरू झालेल्या तिमाहीत 131 forward splits आणि 303 reverse splits लागू झाले. प्रत्येक splitमुळे research pipelineने आधीच cache केलेला price history पुन्हा restate होऊ शकतो. Versioned storageमुळे restatement टळत नाही. ते नवीन स्थिती नवीन version म्हणून नोंदवते आणि जुनी स्थिती वाचण्यायोग्य ठेवते. त्यामुळे stale resultचे reproducible resultमध्ये रूपांतर होते.

News timestampsमध्येही हाच सापळा छोट्या प्रमाणात दिसतो.

क्वेरीमार्केट मथळे प्रकाशित होण्याची वेळ, New York घड्याळानुसार
प्रत्येक आकड्यामागील अचूक SQL
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

New Yorkच्या दिवसातील सर्व 24 तासांत Headlines चोवीस तास नोंदवल्या जातात. मागील 90 दिवसांत 09:00 या तासात 790 articles नोंदवले गेले, तर 20:00 या तासात 404 articles नोंदवले गेले. संध्याकाळची headline त्या दिवसाच्या 4:00 p.m. closeवर लावल्यास closeवर व्यवहार करणाऱ्या strategyला अनेक तासांची hindsight मिळते.

ठरावीक वेळेच्या स्थितीवर चालवता येणारी परिस्थिती

येथील सर्व काही Python packageमध्येच राहते. या प्रकल्पासोबत Rust command-line toolदेखील दिले जाते; मात्र या walkthroughसाठी त्याची स्वतंत्र install आवश्यक नाही. नमुना data download न करता स्थानिक पातळीवर तयार केला जातो.

  1. निश्चित केलेली release install करा: pip install 'h5i-db==0.1.6', जी 4 August 2026 रोजी प्रकाशित झाली. यासाठी Python 3.9 किंवा त्यानंतरची आवृत्ती आवश्यक आहे आणि ती pyarrow>=14 सोबत install होते. Prebuilt wheels Linuxच्या x86-64 आणि arm64साठी, तसेच Apple silicon macOS आणि Windowsच्या x86-64साठी उपलब्ध आहेत.
  2. import pyarrow as pa आणि import pyarrow.parquet as pqनंतर dataचे वर्णन एकदाच द्या: schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]).
  3. pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet') वापरून स्थानिक fileमध्ये तयार केलेल्या दोन rows लिहा. यामध्ये d1 आणि d2 timezone-aware datetimes आहेत.
  4. Database आणि table तयार करा आणि time columnचे नाव द्या: db = h5i_db.Database('pit.db', create=True), त्यानंतर db.create_table('prices', schema, time_column='ts').
  5. File एका keyअंतर्गत ingest करा: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). या callमधून तयार केलेला commit परत मिळतो.
  6. तीच ओळ पुन्हा चालवा. समान key असलेली पुनरावृत्ती आधी तयार केलेला commit शोधते आणि rows दुसऱ्यांदा लिहिण्याऐवजी "segments_added": 0सह तोच commit परत करते, असे प्रकल्पाच्या documentationमध्ये नमूद आहे. Retryच्या आधी आणि नंतर db.versions('prices') print करा आणि version listमध्ये कोणताही बदल होत नाही हे पाहा.
  7. idempotency_key='load-day2'अंतर्गत दुसऱ्या दिवसाचा data ingest करा, त्यानंतर दोन्ही दिवसांवरील query चालवा: db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas().
  8. दुसऱ्या दिवसाचा data येण्यापूर्वी table जशी होती तशी वाचा: db.read('prices', version=1). त्याच कामासाठी या methodमध्ये as_of= आणि snapshot= argumentsदेखील देता येतात.

Step 6वर विशेष लक्ष द्या. Duplicated appendमुळे कोणतीही error येत नाही. त्या क्षणापासून table चुकीची राहते आणि त्यानंतरच्या प्रत्येक runमध्ये ही चूक शांतपणे पुढे जाते. Step 8 हा याचा मुख्य फायदा आहे: Marchमध्ये काढलेली संख्या Augustमध्ये त्याच pinned versionवरून पुन्हा काढता येते. Framework स्तरावर आमच्या reproducible backtestविषयक लेखनात मांडलेली हीच आवश्यक गुणवैशिष्ट्य आहे.

प्रकल्पाचे दावे आणि आम्ही केलेली पडताळणी

READMEमध्ये एक बेंचमार्क दिला आहे:

20M rowsवरील OHLCV+VWAP rollupsमध्ये DuckDB आणि Polarsपेक्षा 4.5×हून अधिक वेगवान

हा आकडा प्रकल्पाने स्वतः केलेल्या मोजमापावर आधारित आहे. तो August 2026मध्ये मिळवलेल्या h5i-db READMEमधून उद्धृत केला आहे. आम्ही ही चाचणी चालवलेली नाही. वरीलपैकी कोणतीही बाब या आकड्यावर अवलंबून नाही.

या प्रकरणात वेगापेक्षा प्रकल्पाची परिपक्वता अधिक महत्त्वाची आहे. हा लेख लिहिताना repositoryला 29 stars होते आणि Apache-2.0 licenceअंतर्गत त्याची आवृत्ती 0.1.6 होती. याचा अर्थ maintainerचा गट लहान आहे आणि point releasesदरम्यान APIमध्ये अजूनही बदल होऊ शकतात. हा engine loadखाली चालल्याचा दीर्घ सार्वजनिक इतिहास उपलब्ध नाही. अचूक आवृत्ती pin करून database तयार करण्यासाठी वापरलेल्या parquet files जतन केल्यास releaseमुळे वर्तन बदलल्यास मागे जाण्याचा मार्ग राहतो. स्वतःची raw copy ठेवणे हीच सवय vendorने तुमच्या नकळत ऐतिहासिक नोंदी बदलण्यापासून संरक्षण करते; हीच बाब stock dataमधील survivorship biasमधून स्पष्ट होते.

वारंवार विचारले जाणारे प्रश्न

point-in-time data म्हणजे काय?

point-in-time data हा असा dataset आहे, ज्यामध्ये प्रत्येक तथ्य ज्ञात झाल्याची timestamp नोंदवलेली असते. त्यामुळे कोणत्याही भूतकाळातील तारखेला काय उपलब्ध होते, हे queryद्वारे पुन्हा तयार करता येते. साधी “latest value” table हे करू शकत नाही, कारण ती आजच्या दुरुस्त आकडेवारीने भूतकाळातील मूल्ये बदलून टाकते.

versioned storageमुळे look-ahead bias नाहीसा होतो का?

नाही. Versioningमुळे leakageचा एक प्रकार दूर होतो. म्हणजे simulated decision timeनंतर लिहिलेली मूल्ये एखाद्या runमध्ये वाचली जाण्याची समस्या दूर होते. मात्र feature constructionमध्ये इतर मार्गांनी leakage होऊ शकतो. उदाहरणार्थ, संपूर्ण historyवर मोजलेल्या statisticsच्या आधारे sampleचे scaling करणे.

ingestदरम्यान idempotency key काय करते?

ती एखाद्या writeला label देते, त्यामुळे retry झाल्यावर तोच write पुन्हा झाल्याचे ओळखता येते. त्यानंतर loader crashनंतर पुन्हा चालवता येतो आणि त्याच rows दोनदा append होत नाहीत. अन्यथा tableमधील आकडेवारी नकळत चुकीची राहू शकते.

h5i-db productionसाठी तयार आहे का?

लेखनाच्या वेळी h5i-dbची version 0.1.6 होती आणि GitHubवर त्याला 29 stars होते. ते Apache-2.0 अंतर्गत उपलब्ध आहे. या आकाराच्या सुरुवातीच्या softwareमध्ये APIमध्ये वारंवार बदल होण्याची शक्यता आणि मर्यादित public track record असतो. त्यामुळे version pin करणे आणि source filesची स्वतःची copy ठेवणे, evaluation उलटवता येण्याजोगे ठेवण्याचे प्रमुख मार्ग आहेत.


वरील प्रत्येक panelसोबत तो तयार करण्यासाठी वापरलेले SQL दिलेले आहे. त्यामुळे एखादा panel उघडा आणि आकडा कसा मोजला गेला, ते वाचा. हेच प्रश्न Strasmore terminalवर plain Englishमध्ये विचारता येतात.