پایگاه داده h5i-db برای بکتست با دادههای مقطعی
استفاده از دادههای مقطعی در h5i-db مانع از سوگیری نگاه به آینده در بکتست میشود. این پایگاه داده با نسخهگذاری هر تراکنش، دقت تحلیلها را در بررسی تاخیر گزارشهای مالی تضمین میکند.
دادههای مقطعی (Point-in-time)
دادههای مقطعی، سوابقی از وضعیت یک مجموعه داده در تاریخی مشخص در گذشته هستند. در فرآیند بکتست (backtest)، این دادهها نتایج قابلدفاع را از نتایجی که بهطور پنهانی از اعدادِ روزهای آینده استفاده کردهاند، متمایز میکنند. h5i-db یک پایگاه داده سری زمانی متنباز و نوپا است که با زبان Rust نوشته شده و دارای API پایتون است. این پایگاه داده، هر عملیات نوشتن را بهعنوان یک نسخه شمارهگذاریشده ذخیره میکند و به هر عملیات خواندن اجازه میدهد تا نسخهای پیشین را مبنا قرار دهد (pin). در ادامه، نشت دادهای که با استفاده از این قابلیتِ مبناگذاری مسدود میشود، بر اساس دادههای واقعی گزارشهای مالی بررسی شده و سپس سناریویی ارائه میشود که میتوانید آن را روی لپتاپ خود اجرا کنید.
دادههای مقطعی (Point-in-time) در بکتست چیست؟
هر واقعیت بازار دارای دو برچسب زمانی است. «زمان رویداد» لحظهای است که اتفاق رخ داده است. «زمان ورود» لحظهای است که آن رویداد برای هر کسی خارج از نهاد گزارشدهنده، قابلشناسایی شده است. گزارش داراییهای فصلی، موقعیتهای معاملاتی در آخرین روز یک فصل را توصیف میکند، اما هفتهها بعد به دست عموم میرسد؛ بنابراین، مدلی که تنها بر اساس «زمان رویداد» دادهها را ترکیب کند، به اطلاعاتی دسترسی پیدا میکند که در آن مقطع زمانی در اختیار هیچکس نبوده است.
این فاصله قابلاندازهگیری است. مدیران نهادی فرم 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برای فصل منتهی به 2026-06-30، ثبتهای 10688 بهطور میانگین 34.1 روز پس از دورهای که پوشش میدهند، دریافت شدهاند. این پنل، آن اندازهگیری را در طول 16 فصل تکرار میکند. همچنین یک مدیر میتواند گزارش خود را مدتها پس از ثبت اولیه اصلاح کند؛ بنابراین، سوابقی که یک تاریخ گذشته را توصیف میکنند، حتی پس از سپری شدن آن تاریخ نیز همچنان در حال تغییر هستند.
سوگیری نگاه به آینده یک مشکل ذخیرهسازی است
راهنمای ما در خصوص سوگیری نگاه به آینده در بکتست، نشت داده را یک مسئله انضباطی میداند: هر ویژگی (feature) را با تأخیر اعمال کنید و به تاریخهای انتشار پایبند باشید. این انضباط تا زمانی که کسی آن را فراموش نکند برقرار است، اما شکست در آن بیسروصدا رخ میدهد. یک بکتست حاوی نشت داده، نسبت شارپ (Sharpe ratio) بهتری را ثبت میکند، بدون آنکه هیچ خطایی بروز دهد.
ذخیرهسازی «نقطه-در-زمان» (Point-in-time)، این تضمین را به لایهای پایینتر منتقل میکند. هنگامی که دادههای ارائهشده به یک استراتژی از خوانشی ناشی شود که به یک نسخه خاص قفل شده است، سطری که پس از آن نسخه نوشته شده باشد، فارغ از اینکه کد استراتژی در ادامه چه کاری انجام میدهد، نمیتواند در آن ظاهر شود. در این حالت، بررسی صحت دادهها از یک بازبینی کد به ویژگیِ خودِ عملیات خواندن تبدیل میشود.
سود سهام، شکاف زمانی را از زاویهای دیگر نشان میدهد. سود نقدی ابتدا اعلام و سپس در تاریخ ex-dividend اعمال میشود؛ جدولی که امروز بارگذاری میشود، هر دو تاریخ را برای هر پرداخت در خود دارد، از جمله مواردی که در تاریخ مورد شبیهسازی، هنوز اعلام نشده بودند.
کد دقیق 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در ماهی که از 2026-07-01 آغاز میشود، اعلامیهها بهطور میانگین 88.2 روز پیش از تاریخ ex-dividend ثبت شدند و این پنل، 24 ماه از همین اندازهگیری را پوشش میدهد. اگر یک جدول مدرن سود سهام را در یک تاریخ شبیهسازیشده در داخل این شکاف زمانی بخوانید، پرداخت مذکور از قبل در آنجا وجود دارد، یعنی هفتهها پیش از آنکه آن اعلامیه اصلاً وجود خارجی داشته باشد.
بازنویسی تاریخچه ذخیرهشده
ورود دیرهنگام یک حالت شکست است. بازنویسی (Restatement) حالت دیگر. اقدامات شرکتی، قیمتهایی را که قبلاً ثبت (Print) شدهاند، بازنویسی میکنند: پس از یک تقسیم سهام 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_dateدر سهماههای که از 2026-04-01 آغاز شد، 131 تقسیم سهام مستقیم و 303 تقسیم معکوس اجرایی شد. هر یک از این موارد، تاریخچه قیمتی را بازنویسی میکند که ممکن است خط لوله پژوهشی از پیش آن را کش (Cache) کرده باشد. ذخیرهسازی نسخهبندیشده (Versioned storage) مانع از بازنویسی نمیشود. این روش، وضعیت جدید را بهعنوان نسخهای تازه ثبت کرده و نسخه قدیمی را نیز قابلخواندن نگه میدارد؛ این همان چیزی است که یک نتیجه منسوخ را به نتیجهای قابلبازتولید تبدیل میکند.
برچسبهای زمانی اخبار نیز همین دام را در مقیاسی کوچکتر دارند.
کد دقیق 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 ساعت روز کاری نیویورک منتشر میشوند. ساعت 09:00 در 90 روز گذشته شاهد انتشار 790 مقاله بوده و ساعت 20:00 نیز 404 مقاله را به خود اختصاص داده است. الصاق یک سرفصل خبری عصرگاهی به قیمت بسته شدن ساعت 16:00 همان روز، به استراتژیای که در زمان بسته شدن بازار معامله میکند، چندین ساعت «مزیت دیدِ پسنگرانه» (Hindsight) میدهد.
یک سناریوی مقطعی که میتوانید اجرا کنید
تمام موارد در اینجا در بسته پایتون باقی میماند. این پروژه همچنین یک ابزار خط فرمان Rust ارائه میدهد که یک نصب جداگانه است و این راهنما به آن نیازی ندارد. دادههای نمونه بهصورت محلی تولید میشوند و نیازی به دانلود ندارند.
- نسخه ثابتشده را نصب کنید:
pip install 'h5i-db==0.1.6'، که در 4 اوت 2026 منتشر شد. این نسخه به پایتون 3.9 یا جدیدتر نیاز دارد وpyarrow>=14را به همراه میآورد. فایلهای از پیش ساختهشده (Wheels) شامل لینوکس روی x86-64 و arm64، بهعلاوه macOS با تراشه اپل و ویندوز روی 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'). - فایل را تحت یک کلید وارد (Ingest) کنید:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). این فراخوانی، کامیت (commit) انجامشده را بازمیگرداند. - دقیقاً همان خط را دوباره اجرا کنید. مستندات پروژه بیان میکند که تکرار با همان کلید، کامیتی را که قبلاً ایجاد کرده است پیدا میکند و آن را با
"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 نتیجه نهایی است: عددی که در ماه مارس محاسبه شده است، میتواند در ماه اوت با همان نسخه ثابتشده دوباره محاسبه شود؛ این همان ویژگی است که ما در نوشتار خود درباره بکتست قابلتکرار در سطح چارچوب (Framework) بر آن تأکید داریم.
ادعاهای پروژه و آنچه بررسی کردیم
فایل README با یک بنچمارک آغاز میشود:
بیش از 4.5 برابر سریعتر از DuckDB و Polars در عملیات rollup دادههای OHLCV+VWAP روی 20 میلیون ردیف
این رقم، اندازهگیری خودِ پروژه است که از README مربوط به h5i-db در اوت 2026 نقل شده است. ما این آزمایش را اجرا نکردیم و هیچیک از مطالب فوق به آن وابسته نیست.
در اینجا، بلوغ پروژه اهمیت بیشتری نسبت به سرعت دارد. این مخزن در زمان نگارش این مطلب 29 ستاره داشته و با نسخه 0.1.6 تحت مجوز Apache-2.0 ارائه شده است. این ترکیب به معنای محدود بودن تیم نگهداری و APIای است که همچنان ممکن است در نسخههای جزئی تغییر کند. سابقه عمومی طولانیمدتی از عملکرد این موتور تحت بار کاری وجود ندارد. تثبیت نسخه دقیق و نگهداری فایلهای پارکت (parquet) که خوراک پایگاه داده بودهاند، در صورت تغییر رفتار در نسخههای جدید، راه بازگشتی باقی میگذارد. نگهداری یک نسخه خام توسط خودتان، همان عادتی است که از تغییر تاریخچه توسط فروشنده جلوگیری میکند؛ موضوعی که در سوگیری بقا در دادههای سهام به آن پرداخته شده است.
پرسشهای متداول
دادههای مقطع زمانی (Point-in-time) چیست؟
دادههای مقطع زمانی، مجموعهای از دادههاست که با برچسبهای زمانیِ لحظهای که هر واقعیت قابلشناسایی شده، ذخیره میشود؛ بنابراین یک پرسوجو (Query) میتواند بازسازی کند که در هر تاریخ گذشته، چه اطلاعاتی در دسترس بوده است. یک جدول ساده «آخرین مقدار» نمیتواند این کار را انجام دهد، زیرا دادههای گذشته را با اعداد اصلاحشده امروز بازنویسی میکند.
آیا ذخیرهسازی نسخهبندیشده، سوگیری نگاه به آینده (Look-ahead bias) را از بین میبرد؟
خیر. نسخهبندی تنها یک دسته از نشت دادهها را برطرف میکند؛ یعنی حالتی که در آن یک پردازش، مقادیری را میخواند که پس از زمان تصمیمگیریِ شبیهسازیشده ثبت شدهاند. ساخت ویژگیها (Feature construction) همچنان میتواند از روشهای دیگری دچار نشت شود؛ برای نمونه، مقیاسبندی یک نمونه بر اساس آماری که در کل تاریخچه آن محاسبه شده است.
کلید همانی (Idempotency key) در هنگام ورود دادهها چه کاری انجام میدهد؟
این کلید یک عملیات نوشتن را برچسبگذاری میکند تا تلاش مجدد برای ثبت، بهعنوان همان عملیات قبلی شناسایی شود. بدین ترتیب، یک بارگذار (Loader) میتواند پس از خرابی سیستم، بدون افزودن دوباره ردیفهای تکراری، عملیات را از سر بگیرد؛ چرا که تکرار دادهها خطایی است که باعث میشود جدول بهطور پنهانی دچار نقص شود.
آیا h5i-db برای استفاده عملیاتی (Production) آماده است؟
این ابزار در زمان نگارش این متن، نسخه 0.1.6 با 29 ستاره در GitHub است و تحت مجوز Apache-2.0 ارائه میشود. نرمافزارهای نوپا با این مقیاس، تغییرات مداوم در API و سابقه عمومی محدودی دارند؛ بنابراین تثبیت نسخه (Version pin) و داشتن یک کپی شخصی از فایلهای منبع، تنها راهی است که ارزیابی شما را قابلبازگشت نگه میدارد.
هر پنل در بالا به همراه کد SQL تولیدکننده آن ارائه شده است؛ بنابراین یکی را باز کنید و بخوانید که چگونه آن عدد محاسبه شده است. پرسشهای مشابه را میتوان به زبان انگلیسی ساده در پایانه Strasmore مطرح کرد.