Strasmore Research
تعلَّم Matt Connorبقلم Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: بيانات اللحظة الزمنية للاختبارات الرجعية

تمنع بيانات اللحظة الزمنية انحياز التطلع إلى المستقبل في الاختبارات الرجعية. تعرّف إلى إصدارات h5i-db لكل عملية كتابة وتأثير تأخر الإيداعات على السجل القديم.

بيانات اللحظة الزمنية هي سجل لما كانت مجموعة البيانات تحتويه في تاريخ سابق. وفي الاختبار التاريخي، تميّز هذه البيانات بين نتيجة يمكنك الدفاع عنها وأخرى قرأت أرقام الغد من دون أن تلاحظ ذلك. تُعد h5i-db قاعدة بيانات حديثة مفتوحة المصدر للسلاسل الزمنية، كُتبت بلغة Rust وتوفّر واجهة برمجة تطبيقات Python. تخزّن كل عملية كتابة باعتبارها إصداراً مرقّماً، وتتيح لك تثبيت أي عملية قراءة على إصدار أقدم. فيما يلي تسرّب المعلومات الذي يمنعه هذا التثبيت، وقد قيس باستخدام بيانات إيداعات حقيقية، يليه سيناريو يمكنك تشغيله على حاسوب محمول.

ما هي بيانات اللحظة الزمنية في الاختبار الرجعي؟

تحمل كل معلومة سوقية طابعين زمنيين. يحدد وقت الحدث موعد وقوع الأمر. أما وقت الوصول فيحدد متى أصبحت المعلومة متاحة لأي طرف خارج الشركة التي أبلغت عنها. يصف تقرير الحيازات الفصلية المراكز المحتفظ بها في اليوم الأخير من الربع، لكنه يصل إلى الجمهور بعد أسابيع. لذلك، يحصل النموذج الذي يربط البيانات باستخدام وقت الحدث وحده على معلومات لم تكن متاحة لأي أحد حينها.

يمكن قياس الفاصل الزمني بين الموعدين. يقدّم مديرو المؤسسات نموذج 13F بعد إغلاق كل ربع، ويقيس الرسم أدناه عدد الأيام بين الربع الذي يصفه الإيداع واليوم الذي قُدّم فيه.

الاستعلامالمدة التي تستغرقها إفصاحات حيازات 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 في المتوسط بعد 34.1 يوماً من الفترة التي تغطيها. ويكرر الرسم هذا القياس عبر 16 أرباع. وقد يعدّل المدير التقرير بعد وقت طويل من تقديمه، لذلك يظل السجل الذي يصف تاريخاً سابقاً عرضة للتغيير حتى بعد انقضاء ذلك التاريخ.

التحيز الاستشرافي مشكلة تخزين

يتعامل دليلنا حول التحيز الاستشرافي في الاختبار الخلفي مع تسرّب البيانات باعتباره مسألة انضباط: أضف إبطاءً زمنياً إلى كل سمة، والتزم بتواريخ النشر. يستمر هذا الانضباط إلى أن ينساه أحد، ويكون الإخفاق صامتاً. فالاختبار الخلفي الذي يتضمن بيانات متسرّبة يعرض نسبة Sharpe أفضل من دون ظهور أي خطأ.

ينقل التخزين وفق نقطة زمنية الضمان إلى طبقة أدنى. عندما يأتي الإطار المُسلَّم إلى الاستراتيجية من قراءة مثبتة على إصدار محدد، لا يمكن أن يظهر فيه صف كُتب بعد ذلك الإصدار، مهما فعلت شيفرة الاستراتيجية لاحقاً. ويتحول التحقق من مراجعة للشيفرة إلى خاصية ملازمة لعملية القراءة.

تُظهر توزيعات الأرباح الفجوة الزمنية من الجانب الآخر. يُعلَن التوزيع النقدي أولاً، ثم يحل تاريخ 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
Run this yourself

في الشهر الذي يبدأ في 2026-07-01، صدرت الإعلانات قبل تاريخ ex-dividend بمتوسط 88.2 يوماً، ويغطي هذا الجدول 24 شهراً من القياس نفسه. وعند قراءة جدول حديث لتوزيعات الأرباح في تاريخ محاكاة يقع داخل هذه الفجوة، تكون الدفعة موجودة فيه بالفعل، قبل أسابيع من صدور الإعلان.

إعادة كتابة السجل التاريخي

التأخر في الوصول أحد أوجه الخلل. وإعادة البيان وجه الخلل الآخر. تعيد إجراءات الشركات كتابة الأسعار التي سبق تسجيلها: فبعد تجزئة للسهم بنسبة four-for-one، يُقسَم كل سعر سابق في السلسلة المعدَّلة على four، ولا تعود السلسلة التي تُنزَّل اليوم مطابقةً للشريط السعري الذي راقبه المتداول. يشرح بحثنا عن تاريخ الأسعار المعدَّل بالتجزئة العمليات الحسابية بالتفصيل. لكن العامل المهم هنا هو التواتر.

الاستعلامتجزئات الأسهم السارية كل ربع سنة، عادية وعكسية
استعلام 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 تجزئةً أمامية و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
Run this yourself

تُنشر العناوين على مدار الساعة، خلال جميع ساعات يوم نيويورك البالغ عددها 24 ساعة. وشهدت ساعة 09:00 790 مقالاً خلال آخر 90 يوماً، في حين شهدت ساعة 20:00 404 مقالاً. إن إسناد عنوان مسائي إلى إغلاق الساعة 4:00 مساءً في ذلك اليوم يمنح استراتيجية تتداول عند الإغلاق عدة ساعات من المعرفة اللاحقة.

سيناريو لحظي يمكنك تشغيله

يبقى كل ما يرد هنا ضمن حزمة Python. يوفّر المشروع أيضاً أداة سطر أوامر مكتوبة بلغة Rust، لكن هذا التثبيت المنفصل لا تحتاج إليه في هذا الشرح. تُنشأ البيانات النموذجية محلياً، من دون تنزيل.

  1. ثبّت الإصدار المحدد: pip install 'h5i-db==0.1.6'، المنشور في 4 أغسطس 2026. يتطلب Python 3.9 أو أحدث، ويتضمن pyarrow>=14. وتتوفر حزم wheels مُسبقة البناء لنظام Linux على x86-64 وarm64، وكذلك لنظام macOS على Apple silicon ولنظام Windows على x86-64.
  2. عرّف البيانات مرة واحدة، بعد import pyarrow as pa وimport pyarrow.parquet as pq: 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')، حيث إن d1 وd2 يمثلان وقتاً مع مراعاة المنطقة الزمنية.
  4. أنشئ قاعدة البيانات والجدول، وسمِّ عمود الوقت: db = h5i_db.Database('pit.db', create=True) ثم db.create_table('prices', schema, time_column='ts').
  5. أدخل الملف تحت مفتاح: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). ويعيد الاستدعاء عملية الإيداع التي نفذها.
  6. شغّل السطر نفسه مرة أخرى. يوضح المشروع أن التكرار الذي يحمل المفتاح نفسه يعثر على عملية الإيداع التي سبق أن أنشأها ويعيدها باستخدام "segments_added": 0 بدلاً من كتابة الصفوف مرة ثانية. اطبع db.versions('prices') قبل إعادة المحاولة وبعدها، وراقب بقاء قائمة الإصدارات من دون تغيير.
  7. أدخل بيانات يوم ثانٍ باستخدام idempotency_key='load-day2'، ثم نفّذ استعلاماً يشمل اليومين: db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas().
  8. اقرأ الجدول كما كان قبل إضافة بيانات اليوم الثاني: db.read('prices', version=1). وتقبل الطريقة نفسها الوسيطتين as_of= وsnapshot= لتنفيذ المهمة نفسها.

تستحق الخطوة السادسة التوقف عندها. فالإضافة المكررة لا تثير أي خطأ. بل تجعل الجدول غير صحيح منذ تلك اللحظة، وتنتقل المشكلة بصمت إلى كل عملية تشغيل لاحقة. أما الخطوة الثامنة فهي الفائدة الأساسية: يمكنك إعادة حساب رقم حُسب في مارس خلال أغسطس، باستخدام الإصدار المحدد نفسه. وهذه هي الخاصية التي يدافع عنها شرحنا لـ الاختبار التاريخي القابل لإعادة الإنتاج على مستوى الإطار البرمجي.

ما الذي يدّعيه المشروع، وما الذي تحققنا منه

يستهل ملف README معياراً للأداء:

أسرع بأكثر من 4.5 مرات من DuckDB وPolars في عمليات التجميع المتحركة لبيانات OHLCV+VWAP على 20 مليون صف

هذا الرقم صادر عن قياس أجراه المشروع نفسه، وقد نُقل من ملف README لمشروع h5i-db كما جرى استرجاعه في أغسطس 2026. لم نشغّل الاختبار بأنفسنا، ولا يعتمد أي مما سبق عليه.

تكتسب درجة النضج أهمية أكبر من السرعة في هذه الحالة. كان المستودع يضم 29 نجمة وقت إعداد النص، وكان يعمل بالإصدار 0.1.6 بموجب ترخيص Apache-2.0. ويعني ذلك وجود عدد محدود من القائمين على الصيانة، مع احتمال استمرار تغيّر واجهة API بين الإصدارات الفرعية. ولا يوجد سجل علني طويل يثبت تشغيل المحرك تحت الضغط. يتيح تثبيت الإصدار الدقيق والاحتفاظ بملفات parquet التي زوّدت قاعدة البيانات بالبيانات إمكانية الرجوع إلى حالة سابقة إذا غيّر إصدار جديد سلوك النظام. والاحتفاظ بنسخة خام خاصة بك هو العادة نفسها التي تحميك من أن يعيد أحد مزودي البيانات تشكيل السجل التاريخي من دون علمك، وهي الفكرة التي تتكرر في انحياز البقاء في بيانات الأسهم.

الأسئلة الشائعة

ما المقصود ببيانات نقطة زمنية محددة؟

بيانات نقطة زمنية محددة هي مجموعة بيانات تُخزَّن مع الطوابع الزمنية التي أصبح فيها كل واقع قابلاً للمعرفة، بحيث يمكن للاستعلام إعادة بناء ما كان ظاهراً في أي تاريخ سابق. ولا يستطيع جدول بسيط يعرض «أحدث قيمة» فعل ذلك، لأنه يستبدل الماضي بالأرقام المصححة الحالية.

هل يلغي التخزين المُدار بالإصدارات انحياز النظر إلى المستقبل؟

لا. تعالج إدارة الإصدارات فئة واحدة من تسرب البيانات، وهي الحالة التي يقرأ فيها التشغيل قيماً كُتبت بعد وقت القرار المُحاكى. لكن إنشاء الخصائص قد يؤدي إلى تسرب بطرق أخرى، مثل تقييس عينة باستخدام إحصاءات حُسبت على كامل تاريخها.

ما وظيفة مفتاح idempotency أثناء إدخال البيانات؟

يضع المفتاح علامة على عملية الكتابة بحيث يُتعرَّف إلى إعادة المحاولة على أنها عملية الكتابة نفسها. عندئذ يستطيع برنامج التحميل إعادة التشغيل بعد حدوث عطل من دون إضافة الصفوف نفسها مرتين، وهو الخلل الذي يترك الجدول غير صحيح من دون تنبيه.

هل أصبح h5i-db جاهزاً للاستخدام في بيئة الإنتاج؟

إصداره هو 0.1.6، ولديه 29 نجمة على GitHub وقت كتابة هذا النص، ويخضع لترخيص Apache-2.0. تحمل البرمجيات المبكرة بهذا الحجم تغييرات متكررة في API وسجلاً عاماً محدوداً. لذلك فإن تثبيت الإصدار والاحتفاظ بنسختك الخاصة من الملفات المصدرية هما ما يبقيان عملية التقييم قابلة للعكس.


تتضمن كل لوحة أعلاه استعلام SQL الذي أنتجها، لذا افتح إحداها واقرأ طريقة احتساب الرقم. ويمكن طرح الأسئلة نفسها باللغة الإنجليزية البسيطة على منصة Strasmore الطرفية.