Strasmore Research
למידה Matt Connorמאת Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: ניהול נתוני Point-in-time לביצוע Backtest

שימוש בנתוני Point-in-time מונע הטיית מבט לעתיד בבדיקות לאחור. גלו כיצד h5i-db מתעד גרסאות לכל כתיבה וכיצד השהיית דיווחים משפיעה על דיוק ההיסטוריה הפיננסית שלכם.

2026-07-01
נתוני Point-in-time הם תיעוד של מצב מאגר נתונים בתאריך עבר, ובמסגרת backtest הם מפרידים בין תוצאה שניתן להגן עליה לבין כזו ש"קראה" בשקט את נתוני המחר. h5i-db הוא מסד נתונים מסוג time-series צעיר בקוד פתוח, הכתוב ב-Rust עם ממשק API ל-Python, השומר כל כתיבה כגרסה ממוספרת ומאפשר לכל קריאה להצמיד (pin) גרסה מוקדמת יותר. להלן דליפת הנתונים שפעולת ההצמדה מונעת, כפי שנמדדה על נתוני דיווחים אמיתיים, ולאחריה תרחיש שניתן להריץ על מחשב נייד.

חשיבות ה-Point-in-time בבדיקות לאחור

בניית מודל פיננסי ללא נתוני Point-in-time מובילה כמעט תמיד להטיית הישרדות או לשימוש במידע עתידי. כאשר מנתחים נתונים כגון EPS או יחסי P/E, הבעיה אינה רק בנתון עצמו, אלא במועד שבו הוא הפך לזמין לציבור. אם המודל משתמש בדו"ח כספי שפורסם ב-15 במאי כדי לקבל החלטת השקעה ב-1 במאי, התוצאות יהיו מעוותות.

הטיה זו מתרחשת לעיתים קרובות בגלל עדכונים רטרואקטיביים בבסיסי נתונים. חברות עשויות לתקן דיווחים קודמים, או שספקי נתונים עשויים לעדכן היסטורית את ה-GDP או את הרכב המדדים. ללא שמירה על גרסאות, המערכת תמיד תציג את הנתון המעודכן ביותר, מה שיוצר אשליה של ביצועים עודפים שאינם ניתנים לשחזור במציאות.

מנגנון ה-versioning ב-h5i-db מאפשר למשתמש לבצע שאילתות על בסיס "מה ידענו ומתי". במקום לשמור רק את הערך האחרון, המערכת שומרת את כל רצף העדכונים. הדבר מאפשר ל-backtest לפעול כאילו הוא נמצא בנקודת זמן ספציפית בעבר, תוך התעלמות מכל מידע שנוסף למאגר לאחר מכן.

השוואת ביצועים עם ובלי הצמדת נתונים

תרחיש לדוגמה: הרצת סימולציה על מחשב נייד

כדי להמחיש את ההבדל, ניתן להשתמש ב-h5i-db כדי להשוות אסטרטגיית מסחר המבוססת על נתוני דיווחים. בתרחיש הראשון, המערכת ניגשת לנתונים "הנקיים" (המעודכנים), מה שמוביל לרווחים תיאורטיים גבוהים באופן לא סביר. בתרחיש השני, המערכת משתמשת ב-pinning כדי לגשת לנתונים כפי שהיו ביום הדיווח המקורי. הפער בתוצאות הוא המדד המדויק ל"דליפת המידע" שהתרחשה במודל הראשון.

מהו נתון "נקודת זמן" (point-in-time) בבדיקה לאחור (backtest)?

לכל עובדת שוק יש שני חותמות זמן. זמן האירוע הוא הרגע שבו הדבר התרחש. זמן ההגעה הוא הרגע שבו המידע הפך לנגיש לכל גורם מחוץ לפירמה שדיווחה עליו. דוח אחזקות רבעוני מתאר פוזיציות שהוחזקו ביום האחרון של הרבעון ומגיע לציבור שבועות לאחר מכן, לכן מודל המבצע הצלבה לפי זמן האירוע בלבד מקבל מידע שאף אחד לא החזיק באותו רגע.

המרחק הזה ניתן למדידה. מנהלים מוסדיים מגישים טופס 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 ratio) טוב יותר ללא כל הודעת שגיאה.

אחסון מבוסס נקודת זמן (point-in-time) מעביר את הערבות שכבה אחת למטה. כאשר מסגרת הנתונים המועברת לאסטרטגיה מגיעה מקריאה המקובעת לגרסה מסוימת, שורה שנכתבה לאחר אותה גרסה לא יכולה להופיע בה, ללא קשר למה שקוד האסטרטגיה עושה לאחר מכן. הבדיקה מפסיקה להיות סקירת קוד והופכת למאפיין של פעולת הקריאה.

דיבידנדים מציגים את פער התזמון מהצד השני. דיבידנד במזומן מוכרז תחילה ורק לאחר מכן מגיע לתאריך האקס-דיבידנד, וטבלה הנטענת כיום נושאת את שני התאריכים עבור כל תשלום, כולל כאלו שטרם הוכרזו בתאריך המדמה.

שאילתהימים בין הכרזת דיבידנד ליום האקס (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, הכרזות הגיעו בממוצע 88.2 ימים לפני תאריך האקס-דיבידנד, והפאנל מכסה 24 חודשים של אותה מדידה. קרא טבלת דיבידנדים מודרנית בתאריך מדמה הנמצא בתוך הפער הזה, והתשלום כבר מופיע שם, שבועות לפני שההכרזה הייתה קיימת.

היסטוריה מאוחסנת עוברת עדכון

הגעה מאוחרת היא כשל אחד. עדכון נתונים (Restatement) הוא הכשל השני. פעולות תאגידיות משכתבות מחירים שכבר פורסמו: לאחר ספליט (פיצול מניות) ביחס של ארבע לאחת, כל מחיר קודם בסדרה מותאמת מחולק בארבע, והסדרה שמתקבלת היום אינה תואמת עוד את ה-tape שראה הסוחר בזמן אמת. הרשימה שלנו בנושא היסטוריית מחירים מותאמת לספליט מפרטת את החישובים. מה שחשוב כאן הוא התדירות.

שאילתהפיצולי מניות שנכנסו לתוקף בכל רבעון, רגילים והפוכים
ה-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 ספליטים הפוכים (reverse splits). כל אחד מהם מעדכן היסטוריית מחירים שצינור המחקר (research pipeline) עשוי היה לשמור במטמון. אחסון בגרסאות (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
Run this yourself

כותרות מתפרסמות מסביב לשעון, לאורך כל 24 השעות של יום המסחר בניו יורק. בשעה 09:00 פורסמו 790 מאמרים במהלך תשעים הימים האחרונים, ובשעה 20:00 פורסמו 404. הצמדת כותרת של ערב לשער הנעילה של אותה יום בשעה 16: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 הם אובייקטי datetime מודעי אזור זמן.
  4. צרו את מסד הנתונים ואת הטבלה, ותנו שם לעמודת הזמן: db = h5i_db.Database('pit.db', create=True) ולאחר מכן db.create_table('prices', schema, time_column='ts').
  5. בצעו Ingest לקובץ תחת מפתח: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). הקריאה מחזירה את ה-commit שנוצר.
  6. הריצו את אותה שורה בדיוק שוב. הפרויקט מתעד כי חזרה על הפעולה עם אותו מפתח מאתרת את ה-commit שכבר נוצר ומחזירה אותו עם "segments_added": 0 במקום לכתוב את השורות פעם שנייה. הדפיסו את db.versions('prices') משני צידי הניסיון החוזר וראו כי רשימת הגרסאות נותרת ללא שינוי.
  7. בצעו Ingest ליום שני תחת 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= עבור אותה משימה.

שלב 6 הוא השלב שראוי להתעכב עליו. הוספה כפולה אינה מעלה שגיאה. היא מותירה את הטבלה במצב שגוי מרגע זה ואילך, וכל הרצה לאחר מכן יורשת את הנזק בשקט. שלב 8 הוא התועלת: מספר שחושב במרץ ניתן לחישוב מחדש באוגוסט מאותה גרסה מקובעת, התכונה שאנו טוענים עבורה ברמת התשתית בתיאור שלנו של backtest הניתן לשחזור.

מה הפרויקט טוען, ומה בדקנו

ה-README נפתח בנתון השוואתי:

מהיר פי ארבעה וחצי מ-DuckDB ומ-Polars בביצוע חישובי OHLCV+VWAP על פני עשרים מיליון שורות

נתון זה הוא מדידה עצמית של הפרויקט, כפי שצוטט מתוך ה-README של h5i-db כפי שנצפה באוגוסט 2026. לא הרצנו את הבדיקה בעצמנו, ושום דבר מהאמור לעיל אינו נשען עליה.

במקרה זה, בשלות חשובה יותר ממהירות. נכון למועד כתיבת שורות אלו, המאגר צבר עשרים ותשעה כוכבים והוא נמצא בגרסה 0.1.6 תחת רישיון Apache-2.0. שילוב זה מעיד על מאגר מצומצם של מתחזקים ועל API שעדיין עשוי להשתנות בין גרסאות משנה. לא קיים תיעוד ציבורי ארוך טווח של המנוע תחת עומס. קיבוע הגרסה המדויקת ושמירת קבצי ה-parquet שהזינו את בסיס הנתונים מותירים פתח לחזרה לאחור אם שחרור גרסה ישנה את אופן הפעולה. שמירת עותק גולמי משלכם היא אותו הרגל המגן מפני ספק שמשנה את ההיסטוריה מתחת לרגליכם, נושא שחוזר לאורך הטיית שרידות בנתוני מניות.

שאלות נפוצות

מהו נתון מסוג point-in-time?

נתון מסוג point-in-time הוא מערך נתונים הנשמר עם חותמות זמן המציינות מתי כל עובדה הפכה לידועה, כך ששאילתה יכולה לשחזר את מה שהיה גלוי בכל תאריך בעבר. טבלה פשוטה של "ערך אחרון" אינה יכולה לעשות זאת, שכן היא דורסת את העבר עם המספרים המעודכנים של היום.

האם אחסון מבוסס גרסאות מבטל הטיית look-ahead?

לא. ניהול גרסאות פותר סוג אחד של דליפת מידע, כזה שבו הרצה קוראת ערכים שנכתבו לאחר זמן ההחלטה המדומה. בניית מאפיינים (feature construction) עדיין יכולה לדלוף בדרכים אחרות, כגון נרמול דגימה לפי נתונים סטטיסטיים שחושבו על פני כל ההיסטוריה שלה.

מה עושה מפתח idempotency במהלך תהליך ה-ingest?

הוא מתייג כתיבה כך שפעולת ניסיון חוזר מזוהה כאותה כתיבה בדיוק. טוען נתונים (loader) יכול להריץ את הפעולה מחדש לאחר קריסה מבלי להוסיף את אותן שורות פעמיים, שכן זהו הכשל שמותיר טבלה שגויה באופן שקט.

האם h5i-db מוכן לסביבת ייצור?

זוהי גרסה 0.1.6 עם עשרים ותשעה כוכבים ב-GitHub בעת כתיבת שורות אלו, תחת רישיון Apache-2.0. תוכנה מוקדמת בהיקף כזה נושאת בחובה שינויים תכופים ב-API והיסטוריה ציבורית דלה, וקיבוע גרסה (version pin) לצד עותק מקומי של קבצי המקור הם מה ששומר על הערכה הפיכה.


כל פאנל לעיל מגיע עם ה-SQL שיצר אותו, לכן פתחו אחד וקראו כיצד המספר חושב. את אותן שאלות ניתן לשאול באנגלית פשוטה במסוף Strasmore.