h5i-db: ব্যাকটেস্টিংয়ের জন্য পয়েন্ট-ইন-টাইম ডেটা
h5i-db কীভাবে প্রতিটি রাইট অপারেশন সংস্করণ করে ব্যাকটেস্টের লুক-অহেড বায়াস দূর করে তা জানুন। ফাইলিং ল্যাগ ডেটা এবং স্টেল হিস্ট্রি বিশ্লেষণের মাধ্যমে আপনার মডেলের নির্ভুলতা নিশ্চিত করুন।
পয়েন্ট-ইন-টাইম ডেটা এবং ব্যাকটেস্টিংয়ের নির্ভরযোগ্যতা
পয়েন্ট-ইন-টাইম ডেটা হলো অতীতে কোনো নির্দিষ্ট তারিখে একটি ডেটাসেটে কী তথ্য ছিল তার রেকর্ড। ব্যাকটেস্টিংয়ের ক্ষেত্রে এটি এমন একটি ফলাফলকে আলাদা করে যা আপনি প্রমাণ করতে পারবেন, সেই ফলাফল থেকে যা গোপনে ভবিষ্যতের সংখ্যাগুলো ব্যবহার করেছে। h5i-db হলো একটি নতুন ওপেন-সোর্স টাইম-সিরিজ ডেটাবেস, যা Rust-এ লেখা এবং এর একটি Python API রয়েছে। এটি প্রতিটি রাইট অপারেশনকে একটি নম্বরযুক্ত সংস্করণ হিসেবে সংরক্ষণ করে এবং যেকোনো রিড অপারেশনকে অতীতের কোনো নির্দিষ্ট সংস্করণ দেখার সুযোগ দেয়। নিচে সেই ডেটা লিকেজ বা তথ্য ফাঁস দেখানো হলো যা এই পিনিং পদ্ধতি রোধ করে। এটি বাস্তব ফাইলিং ডেটার ওপর পরিমাপ করা হয়েছে এবং এর পরে একটি দৃশ্যপট দেওয়া হলো যা আপনি আপনার ল্যাপটপে চালিয়ে দেখতে পারেন।
ব্যাকটেস্টে পয়েন্ট-ইন-টাইম ডেটা কী?
প্রতিটি বাজার তথ্যের দুটি টাইমস্ট্যাম্প থাকে। ইভেন্ট টাইম হলো সেই সময় যখন ঘটনাটি ঘটেছিল। অ্যারাইভাল টাইম হলো সেই সময় যখন এটি রিপোর্ট করা প্রতিষ্ঠানের বাইরের কারো কাছে জানার উপযোগী হয়। একটি ত্রৈমাসিক হোল্ডিংস রিপোর্ট একটি ত্রৈমাসিকের শেষ দিনে থাকা পজিশনগুলোর বর্ণনা দেয় এবং এটি জনসাধারণের কাছে পৌঁছায় কয়েক সপ্তাহ পরে। তাই, শুধুমাত্র ইভেন্ট টাইমের ভিত্তিতে তৈরি কোনো মডেল এমন তথ্য ব্যবহার করে যা সেই সময়ে কারো কাছেই ছিল না।
এই সময়ের ব্যবধান পরিমাপযোগ্য। প্রাতিষ্ঠানিক ম্যানেজাররা প্রতিটি ত্রৈমাসিক শেষ হওয়ার পর Form 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_end2026-06-30 তারিখে শেষ হওয়া ত্রৈমাসিকের জন্য, 10688 ফাইলিংগুলো তাদের কভার করা সময়ের গড় 34.1 দিন পরে পৌঁছেছে। প্যানেলটি 16 ত্রৈমাসিক জুড়ে সেই পরিমাপটি পুনরাবৃত্তি করে। একজন ম্যানেজার ফাইল করার অনেক পরেও একটি রিপোর্ট সংশোধন করতে পারেন, তাই একটি অতীত তারিখের বর্ণনা দেওয়া রেকর্ডটি সেই তারিখ পার হয়ে যাওয়ার পরেও পরিবর্তিত হতে থাকে।
লুক-অহেড বায়াস একটি স্টোরেজ সমস্যা
ব্যাকটেস্টিং-এ লুক-অহেড বায়াস সংক্রান্ত আমাদের নির্দেশিকা লিকেজকে একটি শৃঙ্খলার বিষয় হিসেবে দেখে: প্রতিটি ফিচারের ক্ষেত্রে ল্যাগ ব্যবহার করুন এবং প্রকাশের তারিখ মেনে চলুন। কেউ ভুলে না যাওয়া পর্যন্ত এই শৃঙ্খলা বজায় থাকে, আর এর ব্যর্থতা হয় নিঃশব্দ। একটি লিকেজযুক্ত ব্যাকটেস্ট উন্নত Sharpe ratio প্রদর্শন করে, অথচ কোনো ত্রুটি ধরা পড়ে না।
পয়েন্ট-ইন-টাইম স্টোরেজ এই নিশ্চয়তাকে এক ধাপ নিচে নিয়ে আসে। যখন কোনো কৌশলের জন্য ব্যবহৃত ফ্রেমটি একটি নির্দিষ্ট ভার্সনের রিড থেকে আসে, তখন সেই ভার্সনের পরে লেখা কোনো রো সেখানে উপস্থিত হতে পারে না, কৌশলটি পরবর্তীতে যাই করুক না কেন। ফলে এটি আর কোড রিভিউয়ের ওপর নির্ভর করে না, বরং রিড-এর একটি বৈশিষ্ট্যে পরিণত হয়।
লভ্যাংশ বা ডিভিডেন্ড টাইমিং গ্যাপকে অন্য দিক থেকে তুলে ধরে। একটি নগদ লভ্যাংশ প্রথমে ঘোষিত হয় এবং পরবর্তীতে 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 month2026-07-01 মাসে শুরু হওয়া সময়ে, ঘোষণাগুলো ex-dividend date-এর গড়ে 88.2 দিন আগে এসেছে এবং প্যানেলটি একই পরিমাপের 24 মাসের তথ্য কভার করে। সেই গ্যাপের ভেতরে কোনো সিমুলেটেড তারিখে একটি আধুনিক ডিভিডেন্ড টেবিল পড়লে দেখা যায়, ঘোষণার কয়েক সপ্তাহ আগেই পেমেন্টের তথ্য সেখানে বিদ্যমান।
সংরক্ষিত ইতিহাসের পুনর্লিখন
দেরিতে আসা একটি ব্যর্থতার ধরন। পুনর্লিখন হলো অন্যটি। কর্পোরেট অ্যাকশনগুলো ইতিমধ্যে সম্পন্ন হওয়া ট্রেডের দাম পরিবর্তন করে দেয়: 4-ফর-1 স্প্লিটের পর, অ্যাডজাস্ট করা সিরিজের প্রতিটি পূর্ববর্তী দামকে চার দিয়ে ভাগ করা হয় এবং আজ ডাউনলোড করা সিরিজটি সেই টেপের সাথে আর মেলে না যা একজন ট্রেডার দেখেছিলেন। স্প্লিট-অ্যাডজাস্ট করা মূল্যের ইতিহাস নিয়ে আমাদের নোটটি এই গাণিতিক হিসাব ব্যাখ্যা করে। এখানে যা গুরুত্বপূর্ণ তা হলো এর পৌনঃপুনিকতা।
প্রতিটি সংখ্যার পেছনের সঠিক 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_date2026-04-01 সালে শুরু হওয়া ত্রৈমাসিকে, 131 টি ফরোয়ার্ড স্প্লিট এবং 303 টি রিভার্স স্প্লিট কার্যকর হয়েছে। প্রতিটি ঘটনা এমন একটি মূল্যের ইতিহাসকে নতুন করে সাজায় যা হয়তো কোনো রিসার্চ পাইপলাইনে আগে থেকেই ক্যাশ করা ছিল। ভার্সনড স্টোরেজ এই পুনর্লিখনকে আটকাতে পারে না। এটি নতুন অবস্থাকে একটি নতুন ভার্সন হিসেবে রেকর্ড করে এবং পুরনোটিকে পাঠযোগ্য রাখে, যা একটি বাসি ফলাফলকে পুনরুৎপাদনযোগ্য ফলাফলে পরিণত করে।
সংবাদের টাইমস্ট্যাম্পগুলো ছোট আকারে একই ফাঁদ তৈরি করে।
প্রতিটি সংখ্যার পেছনের সঠিক 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নিউ ইয়র্কের দিনের পুরো 24 ঘণ্টা জুড়ে সারাক্ষণ শিরোনাম প্রকাশিত হয়। গত 90 দিনে 09:00 ঘণ্টার সময় 790 টি নিবন্ধ প্রকাশিত হয়েছে এবং 20:00 ঘণ্টার সময় 404 টি নিবন্ধ প্রকাশিত হয়েছে। দিনের 4:00 টার ক্লোজিং প্রাইসের সাথে সন্ধ্যার কোনো শিরোনামের টাইমস্ট্যাম্প যুক্ত করা হলে, ক্লোজিং ট্রেড করা কোনো কৌশলকে কয়েক ঘণ্টার বাড়তি সুবিধা (hindsight) দিয়ে দেওয়া হয়।
একটি পয়েন্ট-ইন-টাইম দৃশ্যপট যা আপনি চালাতে পারেন
এখানে সবকিছু পাইথন প্যাকেজেই থাকে। এই প্রজেক্টে একটি রাস্ট কমান্ড লাইন টুলও রয়েছে, যা একটি আলাদা ইনস্টলেশন এবং এই নির্দেশিকায় তার প্রয়োজন নেই। নমুনা ডেটা স্থানীয়ভাবে তৈরি করা হয়, কোনো ডাউনলোডের প্রয়োজন হয় না।
- পিন করা রিলিজটি ইনস্টল করুন:
pip install 'h5i-db==0.1.6', যা 4 আগস্ট 2026 তারিখে প্রকাশিত হয়েছে। এটি পাইথন 3.9 বা তার নতুন সংস্করণ দাবি করে এবংpyarrow>=14নিয়ে আসে। প্রি-বিল্ট হুইলগুলো x86-64 এবং arm64 আর্কিটেকচারের লিনাক্স, অ্যাপল সিলিকন ম্যাকওএস এবং x86-64 আর্কিটেকচারের উইন্ডোজ সমর্থন করে। import pyarrow as paএবংimport pyarrow.parquet as pqএর পরে ডেটা একবার বর্ণনা করুন:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())])।pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet')ব্যবহার করে একটি স্থানীয় ফাইলে দুটি কাল্পনিক সারি লিখুন, যেখানেd1এবংd2হলো টাইমজোন-সচেতন তারিখ ও সময়।- ডেটাবেস এবং টেবিল তৈরি করুন, টাইম কলামটির নাম দিন:
db = h5i_db.Database('pit.db', create=True)তারপরdb.create_table('prices', schema, time_column='ts')। - একটি কী-এর অধীনে ফাইলটি ইনজেস্ট করুন:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1')। এই কলটি তার করা কমিটটি রিটার্ন করে। - সেই একই লাইনটি আবার চালান। প্রজেক্টের ডকুমেন্টেশন অনুযায়ী, একই কী বহনকারী একটি পুনরাবৃত্তি আগের তৈরি করা কমিটটি খুঁজে পায় এবং সারিগুলো দ্বিতীয়বার না লিখে
"segments_added": 0এর মাধ্যমে তা রিটার্ন করে। রিট্রাই-এর উভয় পাশেdb.versions('prices')প্রিন্ট করুন এবং দেখুন ভার্সন লিস্টটি অপরিবর্তিত থাকে। idempotency_key='load-day2'এর অধীনে দ্বিতীয় দিনের ডেটা ইনজেস্ট করুন, তারপর উভয় দিন জুড়ে কোয়েরি করুন:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas()।- দ্বিতীয় দিন যুক্ত হওয়ার আগের অবস্থায় টেবিলটি পড়ুন:
db.read('prices', version=1)। একই কাজের জন্য এই পদ্ধতিটিas_of=এবংsnapshot=আর্গুমেন্ট গ্রহণ করে।
6 নম্বর ধাপটি মনোযোগ দিয়ে বোঝার মতো। একটি ডুপ্লিকেটেড অ্যাপেন্ড কোনো এরর তৈরি করে না। এটি সেই মুহূর্ত থেকে টেবিলটিকে ভুল অবস্থায় রেখে দেয় এবং পরবর্তী প্রতিটি রান নীরবে সেই ক্ষতি বহন করে। 8 নম্বর ধাপটি হলো মূল প্রাপ্তি: মার্চ মাসে গণনা করা একটি সংখ্যা আগস্ট মাসে একই পিন করা ভার্সন থেকে পুনরায় গণনা করা সম্ভব, যা আমাদের reproducible backtest-এর নিবন্ধে ফ্রেমওয়ার্ক লেভেলে যুক্তি হিসেবে তুলে ধরা হয়েছে।
প্রকল্পটির দাবি এবং আমাদের যাচাইকরণ
README-এর শুরুতে একটি বেঞ্চমার্ক উল্লেখ করা হয়েছে:
20 মিলিয়ন সারির OHLCV+VWAP রোলআপের ক্ষেত্রে DuckDB এবং Polars-এর তুলনায় 4.5 গুণেরও বেশি দ্রুত
এই পরিসংখ্যানটি প্রকল্পটির নিজস্ব পরিমাপ, যা আগস্ট 2026-এ সংগৃহীত h5i-db README থেকে উদ্ধৃত। আমরা এটি পরীক্ষা করিনি এবং উপরের কোনো তথ্যই এর ওপর নির্ভরশীল নয়।
এখানে গতির চেয়ে পরিপক্কতা বেশি গুরুত্বপূর্ণ। এই প্রতিবেদনটি লেখার সময় রিপোজিটরিটির 29টি স্টার ছিল এবং এটি Apache-2.0 লাইসেন্সের অধীনে 0.1.6 সংস্করণে রয়েছে। এই সংমিশ্রণটি নির্দেশ করে যে এর রক্ষণাবেক্ষণকারীর সংখ্যা কম এবং এর API পয়েন্ট রিলিজের মধ্যে পরিবর্তিত হতে পারে। লোডের অধীনে ইঞ্জিনটি চলার কোনো দীর্ঘস্থায়ী পাবলিক রেকর্ড নেই। নির্দিষ্ট সংস্করণটি পিন করে রাখা এবং ডেটাবেসে ব্যবহৃত parquet ফাইলগুলো সংরক্ষণ করা একটি নিরাপদ পথ তৈরি করে, যদি কোনো রিলিজের কারণে কার্যপদ্ধতি পরিবর্তিত হয়। আপনার নিজের কাছে কাঁচা ডেটার কপি রাখা সেই একই অভ্যাস, যা কোনো ভেন্ডর আপনার অজান্তে ডেটা পরিবর্তন করলে আপনাকে সুরক্ষা দেয়; এই বিষয়টি শেয়ার বাজারের ডেটাতে সারভাইভারশিপ বায়াস-এর মূল আলোচনার সাথে সামঞ্জস্যপূর্ণ।
সচরাচর জিজ্ঞাসিত প্রশ্নাবলী
পয়েন্ট-ইন-টাইম ডেটা কী?
পয়েন্ট-ইন-টাইম ডেটা হলো এমন একটি ডেটাসেট যা প্রতিটি তথ্য জানার যোগ্য হওয়ার সময়কাল বা টাইমস্ট্যাম্পসহ সংরক্ষিত থাকে। এর ফলে একটি কুয়েরি অতীতের যেকোনো তারিখে কী তথ্য দৃশ্যমান ছিল তা পুনর্গঠন করতে পারে। একটি সাধারণ "লেটেস্ট ভ্যালু" বা সর্বশেষ মানের টেবিল এটি করতে পারে না, কারণ সেটি আজকের সংশোধিত সংখ্যা দিয়ে অতীতের ডেটাকে মুছে ফেলে।
ভার্সনড স্টোরেজ কি লুক-অহেড বায়াস দূর করে?
না। ভার্সনিং এক ধরনের ডেটা লিক বা তথ্য ফাঁস রোধ করে, যেখানে কোনো সিমুলেটেড সিদ্ধান্তের সময়ের পরে লেখা মানগুলো পড়া হয়। ফিচার তৈরির সময় অন্যান্য উপায়েও তথ্য ফাঁস হতে পারে, যেমন কোনো নমুনার সম্পূর্ণ ইতিহাসের ওপর ভিত্তি করে হিসাব করা পরিসংখ্যান দিয়ে সেটিকে স্কেল করা।
ইনজেস্ট বা ডেটা গ্রহণের সময় আইডেমপোটেন্সি কি (idempotency key) কী কাজ করে?
এটি একটি রাইট বা লিখন প্রক্রিয়াকে চিহ্নিত করে, যাতে পুনরায় চেষ্টা করা হলে সেটিকে একই লিখন হিসেবে শনাক্ত করা যায়। এর ফলে কোনো ক্র্যাশের পর লোডার পুনরায় কাজ শুরু করতে পারে এবং একই সারি দুবার যুক্ত করে না। এই ধরনের ত্রুটি টেবিলকে নীরবে ভুল তথ্যে পূর্ণ করে ফেলে।
h5i-db কি প্রোডাকশনের জন্য প্রস্তুত?
এটি সংস্করণ 0.1.6 এবং এই নিবন্ধটি লেখার সময় GitHub-এ এর 29টি স্টার রয়েছে, যা Apache-2.0 লাইসেন্সের অধীনে। এই আকারের প্রাথমিক সফটওয়্যারে API পরিবর্তনের সম্ভাবনা থাকে এবং এর পাবলিক ট্র্যাক রেকর্ডও সীমিত। তাই একটি নির্দিষ্ট ভার্সন পিন করে রাখা এবং সোর্স ফাইলের নিজস্ব কপি সংরক্ষণ করাই মূল্যায়ন প্রক্রিয়াকে অপরিবর্তনীয় রাখার উপায়।
উপরের প্রতিটি প্যানেলের সাথে সেই SQL কোড দেওয়া আছে যা থেকে ডেটা তৈরি হয়েছে, তাই একটি প্যানেল খুলে দেখুন কীভাবে সংখ্যাটি গণনা করা হয়েছে। একই প্রশ্নগুলো Strasmore টার্মিনালে সাধারণ ইংরেজিতেও করা যেতে পারে।