Strasmore Research
تعلیمی مواد Matt Connorاز Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: Backtest کے لیے Point-in-Time Data

Point-in-time data storage layer پر backtest look-ahead bias روکتا ہے۔ h5i-db ہر write کو version کرتا ہے، جبکہ filing lag stale history کے خطرے کو ظاہر کرتا ہے۔

Point-in-time data اس بات کا ریکارڈ ہوتا ہے کہ کسی dataset میں ماضی کی ایک مخصوص تاریخ کو کیا معلومات موجود تھیں۔ Backtest میں یہی فرق واضح کرتا ہے کہ کون سا نتیجہ قابلِ دفاع ہے اور کون سا خاموشی سے اگلے دن کے اعدادوشمار پڑھ چکا ہے۔

h5i-db ایک نیا open-source time-series database ہے، جو Rust میں لکھا گیا ہے اور Python API فراہم کرتا ہے۔ یہ ہر write کو ایک numbered version کے طور پر محفوظ کرتا ہے اور ہر read کو کسی سابقہ version پر pin کرنے کی سہولت دیتا ہے۔ ذیل میں حقیقی filing data پر پیمائش کی گئی وہ leakage دی گئی ہے جسے یہ pinning روکتی ہے۔ اس کے بعد ایک ایسا scenario ہے جسے آپ laptop پر چلا سکتے ہیں۔

Backtest میں point-in-time data کیا ہوتا ہے؟

ہر market fact کے ساتھ دو timestamps ہوتے ہیں۔ Event time وہ وقت ہے جب واقعہ پیش آیا۔ Arrival time وہ وقت ہے جب رپورٹ کرنے والی firm سے باہر کسی بھی فرد کے لیے یہ fact قابلِ علم ہوا۔ سہ ماہی holdings report میں سہ ماہی کے آخری دن موجود positions کی تفصیل ہوتی ہے، لیکن یہ report کئی ہفتے بعد public ہوتی ہے۔ اس لیے event time کی بنیاد پر ہی data join کرنے والے model کو ایسی information مل جاتی ہے جو اس وقت کسی کے پاس موجود نہیں تھی۔

اس فرق کو ناپا جا سکتا ہے۔ Institutional managers ہر سہ ماہی کے اختتام کے بعد Form 13F file کرتے ہیں۔ ذیل کا panel اس سہ ماہی کے درمیان دنوں کی تعداد ناپتا ہے جس کی filing میں وضاحت کی گئی ہے اور اس دن کے درمیان جب filing جمع کرائی گئی۔

استفسار کریں13F holdings filings عوام تک پہنچنے میں کتنا وقت لیتیں
ہر عدد کے پیچھے موجود درست 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 اس مدت کے اختتام کے اوسطاً 34.1 دن بعد موصول ہوئیں۔ یہ panel 16 سہ ماہیوں میں اسی پیمائش کو دہراتا ہے۔ Manager filing کے کافی عرصے بعد report میں amendment بھی کر سکتا ہے۔ اس لیے کسی گزرے ہوئے date کی وضاحت کرنے والا record اس date کے گزر جانے کے بعد بھی بدلتا رہتا ہے۔

Look-ahead bias اسٹوریج کا مسئلہ ہے

ہماری backtesting میں look-ahead bias کی رہنمائی leakage کو ایک نظم و ضبط کے مسئلے کے طور پر دیکھتی ہے: ہر feature میں lag شامل کریں اور publication dates کا احترام کریں۔ یہ نظم و ضبط اس وقت تک قائم رہتا ہے جب تک کوئی اسے بھول نہ جائے، اور ناکامی خاموشی سے ہوتی ہے۔ leaked backtest بہتر Sharpe ratio دکھاتا ہے، مگر کوئی error ظاہر نہیں کرتا۔

Point-in-time storage اس ضمانت کو ایک نچلی layer تک منتقل کر دیتا ہے۔ جب strategy کو دیا جانے والا data frame کسی version پر pinned read سے آتا ہے، تو اس version کے بعد لکھی گئی row اس میں شامل نہیں ہو سکتی، خواہ strategy code اس کے بعد کچھ بھی کرے۔ یوں جانچ code review کا معاملہ نہیں رہتی بلکہ read کی ایک بنیادی خصوصیت بن جاتی ہے۔

Dividends timing کے فرق کو دوسری سمت سے نمایاں کرتے ہیں۔ Cash dividend پہلے declare ہوتا ہے اور بعد میں ex-dividend ہوتا ہے۔ آج load کی گئی table ہر payment کے لیے دونوں dates رکھتی ہے، ان payments کے لیے بھی جن کا اعلان simulated date پر ابھی نہیں ہوا تھا۔

استفسار کریںماہ کے لحاظ سے dividend declaration اور 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 میں اسی measurement کے 24 ماہ شامل ہیں۔ اس وقفے کے اندر کسی simulated date پر modern dividend table پڑھیں تو payment پہلے ہی اس میں موجود ہوگا، یعنی announcement سے کئی ہفتے پہلے۔

محفوظہ تاریخ دوبارہ لکھی جاتی ہے

دیر سے دستیاب ہونا ایک خرابی ہے۔ Restatement دوسری خرابی ہے۔ Corporate actions پہلے ہی درج ہو چکی قیمتوں کو دوبارہ ترتیب دیتے ہیں: four-for-one split کے بعد adjusted series میں ہر سابقہ قیمت کو four سے تقسیم کر دیا جاتا ہے، اور آج download کی گئی series اس tape سے مختلف ہوتی ہے جسے trader نے دیکھا تھا۔ split-adjusted price history پر ہمارا نوٹ اس حساب کی وضاحت کرتا ہے۔ یہاں اہم بات اس کا وقوع ہے۔

استفسار کریںہر quarter میں مؤثر ہونے والے stock splits، 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 ایسی price history کو دوبارہ مرتب کرتا ہے جسے research pipeline پہلے ہی cache کر چکی ہو سکتی ہے۔ Versioned storage اس restatement کو نہیں روکتا۔ یہ نئی حالت کو ایک نئے version کے طور پر محفوظ کرتا ہے اور پرانا version بھی قابلِ مطالعہ رکھتا ہے۔ اسی عمل سے stale result قابلِ تکرار نتیجے میں تبدیل ہوتا ہے۔

News timestamps میں بھی یہی مسئلہ مختصر شکل میں موجود ہے۔

استفسار کریںNew York کے گھڑی کے وقت کے مطابق market headlines کی اشاعت
ہر عدد کے پیچھے موجود درست 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

Headlines دن رات جاری ہوتے ہیں، New York کے دن کے تمام 24 گھنٹوں میں۔ گزشتہ 90 دنوں کے دوران 09:00 بجے کے گھنٹے میں 790 articles شائع ہوئے، جبکہ 20:00 بجے کے گھنٹے میں 404 articles شائع ہوئے۔ شام کی headline کو اسی دن کے 4:00 p.m. close کے وقت سے منسلک کرنے سے close پر trade کرنے والی strategy کو کئی گھنٹوں کی hindsight مل جاتی ہے۔

ایک مخصوص وقت کی scenario جسے آپ چلا سکتے ہیں

یہاں تمام کام Python package پر ہوگا۔ اس project کے ساتھ ایک Rust command line tool بھی آتا ہے، مگر وہ الگ سے install ہوتا ہے اور اس walkthrough کے لیے ضروری نہیں۔ Sample data مقامی طور پر generate کیا جاتا ہے؛ کسی download کی ضرورت نہیں۔

  1. Pinned release install کریں: pip install 'h5i-db==0.1.6'، جو 4 August 2026 کو publish ہوا تھا۔ اس کے لیے Python 3.9 یا اس کے بعد کا version درکار ہے، اور یہ pyarrow>=14 بھی فراہم کرتا ہے۔ 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') کے ذریعے ایک local 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. اسی exact line کو دوبارہ چلائیں۔ Project کی documentation کے مطابق، وہی key دوبارہ استعمال کرنے والی retry پہلے سے بنائے گئے commit کو تلاش کرتی ہے اور "segments_added": 0 کے ساتھ اسے واپس کرتی ہے، rows کو دوسری مرتبہ write نہیں کرتی۔ 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. Table کو اس حالت میں پڑھیں جو دوسرے دن کا data شامل ہونے سے پہلے تھی: db.read('prices', version=1)۔ اسی کام کے لیے یہی method as_of= اور snapshot= arguments بھی قبول کرتا ہے۔

Step 6 پر خاص توجہ دینی چاہیے۔ Duplicate append پر کوئی error نہیں آتا۔ اس لمحے سے table غلط ہو جاتی ہے، اور اس کے بعد چلنے والی ہر run خاموشی سے اسی خرابی کو آگے منتقل کرتی ہے۔ Step 8 اصل فائدہ دکھاتا ہے: مارچ میں calculate کیا گیا number اگست میں اسی pinned version سے دوبارہ calculate کیا جا سکتا ہے۔ یہی وہ خاصیت ہے جس کے حق میں framework level پر ہماری reproducible backtest کی تحریر دلیل دیتی ہے۔

پروجیکٹ کا دعویٰ کیا ہے، اور ہم نے کیا جانچا

README میں ایک benchmark نمایاں طور پر درج ہے:

20M rows پر OHLCV+VWAP rollups میں DuckDB اور Polars سے 4.5× سے زیادہ تیز

یہ figure خود پروجیکٹ کی measurement ہے، جسے اگست 2026 میں حاصل کیے گئے h5i-db README سے نقل کیا گیا ہے۔ ہم نے اسے خود run نہیں کیا، اور اوپر کی کوئی بات اس پر منحصر نہیں ہے۔

یہاں speed سے زیادہ maturity اہم ہے۔ تحریر کے وقت repository پر 29 stars تھے، اور Apache-2.0 licence کے تحت version 0.1.6 موجود تھا۔ اس امتزاج کا مطلب ہے کہ maintainers کی تعداد کم ہے اور point releases کے دوران API میں اب بھی تبدیلی آ سکتی ہے۔ اس engine کے load کے تحت چلنے کا کوئی طویل public record موجود نہیں۔ exact version کو pin کرنا اور database کو feed کرنے والی parquet files محفوظ رکھنا اس صورت میں واپسی کا راستہ فراہم کرتا ہے جب کسی release سے behaviour تبدیل ہو جائے۔ اپنی raw copy محفوظ رکھنا بھی وہی عادت ہے جو کسی vendor کے آپ کے نیچے تاریخ کو ازسرنو ترتیب دینے کے خطرے سے بچاتی ہے؛ یہی موضوع stock data میں survivorship bias میں زیرِ بحث ہے۔

اکثر پوچھے جانے والے سوالات

point-in-time data کیا ہے؟

point-in-time data ایسا dataset ہے جس میں ہر fact کے قابلِ علم ہونے کا timestamp محفوظ ہوتا ہے۔ اس طرح query ماضی کی کسی بھی تاریخ پر دستیاب معلومات کو دوبارہ تشکیل دے سکتی ہے۔ ایک عام “latest value” table ایسا نہیں کر سکتی، کیونکہ وہ ماضی کی values کو آج کے corrected numbers سے overwrite کر دیتی ہے۔

کیا versioned storage look-ahead bias ختم کر دیتی ہے؟

نہیں۔ Versioning leakage کی ایک قسم کو درست کرتی ہے۔ یہ وہ صورت ہے جس میں کوئی run simulated decision time کے بعد لکھی گئی values پڑھ لیتا ہے۔ Feature construction اب بھی دوسرے طریقوں سے leakage پیدا کر سکتی ہے، مثلاً کسی sample کی scaling کے لیے اس کی پوری history پر calculated statistics استعمال کرنا۔

ingest کے دوران idempotency key کیا کرتی ہے؟

یہ کسی write کو label کرتی ہے تاکہ retry کو اسی write کے طور پر شناخت کیا جا سکے۔ اس کے بعد loader crash کے بعد دوبارہ چل سکتا ہے اور وہی rows دوسری مرتبہ append نہیں ہوتیں۔ یہی خرابی کسی table کو خاموشی سے غلط بنا دیتی ہے۔

کیا h5i-db production کے لیے تیار ہے؟

تحریر کے وقت یہ Apache-2.0 کے تحت version 0.1.6 ہے اور GitHub پر اس کے 29 stars ہیں۔ اس سائز کے ابتدائی software میں API churn اور عوامی سطح پر محدود track record کا خطرہ ہوتا ہے۔ Version pin اور source files کی اپنی copy رکھنا evaluation کو واپس قابلِ عمل بناتا ہے۔


اوپر موجود ہر panel کے ساتھ اسے تیار کرنے والی SQL بھی فراہم کی گئی ہے۔ اس لیے کسی panel کو کھولیں اور دیکھیں کہ number کیسے count کیا گیا۔ یہی سوالات Strasmore terminal پر plain English میں بھی پوچھے جا سکتے ہیں۔