Strasmore Research
חקירות Matt Connorמאת Matt Connor

איך לבנות גרפי OHLCV מנתוני עסקאות גולמיים

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

גרפי OHLCV נבנים על ידי חלוקת רצף העסקאות (trade tape) לפרקי זמן קבועים, ולאחר מכן צמצום כל העסקאות בתוך פרק זמן נתון לחמישה נתונים: המחיר הראשון, הגבוה ביותר, הנמוך ביותר, האחרון, ומספר המניות שהחליפו ידיים. גרף של דקה אחת הנושא חותמת זמן 09:31 מכסה עסקאות החל מ-09:31:00.000 ועד, אך לא כולל, 09:32:00, ולא שום דבר מעבר לכך. כל הקושי בעבודה זו טמון באותיות הקטנות: אילו עסקאות (prints) כשירות להיכלל, כיצד נראית דקה ללא עסקאות, מתי גרף נכתב מחדש לאחר שכבר השתמשת בו, ומדוע ספקים שונים מפרסמים מחירי שיא שונים לאותה דקה בדיוק.

מה באמת מכיל גרף OHLCV

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

  • המרווח הוא חצי-פתוח, ונכתב כ-[t, t+1min). עסקה ב-09:32:00.000 בדיוק שייכת לגרף של 09:32, לעולם לא לגרף של 09:31.
  • חותמת הזמן מציינת את תחילת המרווח. ישנם ספקים המציינים את סופו, מה שגורם לשתי הסדרות להיות מוסטות בגרף אחד זו מזו.
  • Open (פתיחה) הוא המחיר של העסקה הכשירה הראשונה במרווח, לפי סדר חותמת הזמן של הבורסה. Close (סגירה) הוא המחיר של האחרונה שבהן.
  • High (גבוה) ו-Low (נמוך) הם המקסימום והמינימום מבין העסקאות הכשירות בלבד, לעולם לא מתוך ציטוטי מחיר (quotes). הגבוה של גרף הוא מחיר שבו התבצעה עסקה בפועל.
  • Volume (נפח) הוא סכום גדלי העסקאות הכשירות, וספירת עסקאות נפרדת, במקומות שבהם הספק מספק זאת, סופרת עסקאות ולא מניות.
  • במקרה של חותמות זמן זהות, ההכרעה מתבצעת לפי מספר הרצף (sequence number) ב-tape, מה ששומר על מחירי הפתיחה והסגירה דטרמיניסטיים בתוך דקה עמוסה.

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

שאילתהנרות דקה של AAPL שנבנו מנתוני מסחר בודדים, 10 ביוני 2026
ה-SQL המדויק מאחורי כל מספר
SELECT
    formatDateTime(toStartOfMinute(toTimeZone(sip_timestamp, 'America/New_York')), '%H:%i') AS et_time,
    round(toFloat64(argMin(price, (sip_timestamp, sequence_number))), 2) AS bar_open,
    round(toFloat64(max(price)), 2)                                      AS bar_high,
    round(toFloat64(min(price)), 2)                                      AS bar_low,
    round(toFloat64(argMax(price, (sip_timestamp, sequence_number))), 2) AS bar_close,
    round(toFloat64(sum(size)) / 1e6, 2)                                 AS volume_millions
FROM global_markets.stocks_trades
WHERE ticker = 'AAPL'
  AND sip_timestamp >= '2026-06-10 13:30:00'
  AND sip_timestamp <  '2026-06-10 13:45:00'
GROUP BY et_time
ORDER BY et_time
Run this yourself

גרף הפתיחה, הנושא חותמת זמן של 09:30 ET, נפתח ב-$290.74 ונסגר ב-$290.87, בטווח שבין $290.1 ל-$290.98 על נפח של 1.5 מיליון מניות. החלון מכיל 15 גרפים. כל אחד מחמשת המספרים הללו הוא צמצום של קבוצת עסקאות, וקבוצה זו היא החלק שעליו הספקים חלוקים.

אילו עסקאות כשירות לגרף?

כל עסקה ב-consolidated tape נושאת קודי תנאי (condition codes), מספרים שלמים קטנים המתארים את סוג העסקה, וקודים אלו קובעים את הכשירות. Odd lot, עסקה קטנה מ-100 מניות (ה-round lot הסטנדרטי), מדווחת כמו כל עסקה אחרת, אך לפי כללי ה-tape היא אינה מעדכנת את המחיר האחרון או את הגבוה והנמוך היומיים. עסקאות המסומנות כמחיר נגזר, כאלו שבוצעו מחוץ לרצף, במחיר ממוצע, או במחיר ייחוס קודם, נושאות הגבלות משלהן. רובן עדיין נספרות בנפח המסחר בעודן מנועות מלקבוע את ערכי הקיצון, כך שנפח המניות של גרף יכול לכלול עסקאות שמעולם לא הורשו לקבוע את מחיר השיא שלו.

Odd lots אינם טעות עיגול ב-tape המודרני. הפאנל להלן סופר אותם דקה אחר דקה לאורך אותו חלון זמן.

שאילתהנתח Odd lot מתוך העסקאות והווליום, לפי דקה
ה-SQL המדויק מאחורי כל מספר
SELECT
    formatDateTime(toStartOfMinute(toTimeZone(sip_timestamp, 'America/New_York')), '%H:%i') AS et_time,
    round(100 * countIf(has(conditions, 37)) / count(), 1)                            AS odd_lot_print_pct,
    round(100 * toFloat64(sumIf(size, has(conditions, 37))) / toFloat64(sum(size)), 1) AS odd_lot_volume_pct,
    count()                                                                           AS print_count
FROM global_markets.stocks_trades
WHERE ticker = 'AAPL'
  AND sip_timestamp >= '2026-06-10 13:30:00'
  AND sip_timestamp <  '2026-06-10 13:45:00'
GROUP BY et_time
ORDER BY et_time
Run this yourself

בדקה ה-09:30, 79.5% מהעסקאות היו odd lots, והן נשאו 3.3% מהמניות: נתח גדול מספירת העסקאות, אך נתח דק מהנפח. כל דקה שבה אחת מהן נסחרת בערך קיצון מניבה שני גרפים שונים בהתאם לכלל המיושם, וכלל זה מתועד ולא ניתן לניחוש. המדריך שלנו ל-קודי תנאי עסקה מפרט מה משמעות כל קוד ואילו עסקאות ה-tape מחריג מקביעת הגבוה, הנמוך והאחרון.

בניית הגרפים ב-Python, פעמיים

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

import time

# A hand-typed tape: (epoch_seconds, price, size, condition_codes).
# The rows are deliberately out of timestamp order, the way a live feed can arrive.
TICKS = [
    (1781098200.041, 190.12, 300, []),
    (1781098207.512, 190.18, 200, []),
    (1781098238.204, 190.09, 400, []),
    (1781098221.887, 190.44,   5, [37]),   # odd lot
    (1781098247.310, 190.30, 250, []),
    (1781098259.902, 190.16, 150, []),
    (1781098263.115, 190.21, 500, []),
    (1781098272.640, 189.98,   5, [22]),   # prior reference price
    (1781098290.077, 190.05, 700, []),
    (1781098318.455, 190.11, 300, []),
    (1781098385.220, 190.02, 400, []),
    (1781098424.918, 190.09, 600, []),
]

# Codes that many tape rules bar from setting high, low, or last.
RESTRICTED = {2, 10, 12, 22, 33, 37}

def build_bars(ticks, excluded=frozenset()):
    bars = {}
    for stamp, price, size, conditions in sorted(ticks):
        if any(code in excluded for code in conditions):
            continue
        minute = int(stamp // 60) * 60      # the half-open interval [minute, minute + 60)
        bar = bars.get(minute)
        if bar is None:
            bars[minute] = {"open": price, "high": price, "low": price,
                            "close": price, "volume": size}
        else:
            bar["high"] = max(bar["high"], price)
            bar["low"] = min(bar["low"], price)
            bar["close"] = price
            bar["volume"] += size
    return [(minute, bars[minute]) for minute in sorted(bars)]

def show(caption, bars):
    print(caption)
    for minute, bar in bars:
        label = time.strftime("%H:%M", time.gmtime(minute))
        print("  {}  O {:.2f}  H {:.2f}  L {:.2f}  C {:.2f}  V {}".format(
            label, bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"]))

show("every print counted:", build_bars(TICKS))
show("restricted prints skipped:", build_bars(TICKS, RESTRICTED))

# every print counted:
#   13:30  O 190.12  H 190.44  L 190.09  C 190.16  V 1305
#   13:31  O 190.21  H 190.21  L 189.98  C 190.11  V 1505
#   13:33  O 190.02  H 190.09  L 190.02  C 190.09  V 1000
# restricted prints skipped:
#   13:30  O 190.12  H 190.30  L 190.09  C 190.16  V 1300
#   13:31  O 190.21  H 190.21  L 190.05  C 190.11  V 1500
#   13:33  O 190.02  H 190.09  L 190.02  C 190.09  V 1000

שתים-עשרה עסקאות מצטמצמות לשלושה גרפים. הדקה 13:32 אינה מופיעה כלל, שכן לא התבצע בה מסחר, והתוויות מודפסות ב-UTC, כך שהגרף הראשון הוא גרף ה-09:30 בניו יורק. דילוג על העסקאות המוגבלות מותיר את מחירי הפתיחה והסגירה ללא שינוי, אך מזיז שני ערכי קיצון: הגבוה של הגרף הראשון יורד מ-190.44 ל-190.30, והנמוך של הגרף השני עולה מ-189.98 ל-190.05. מחירים אלו מומצאים לצורך הדוגמה, אך המנגנון אינו כזה. עסקה אחת של חמש מניות ברגע הלא נכון היא כל ההבדל בין מחירי השיא של שני ספקים.

מה קורה בדקה ללא עסקאות?

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

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

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

שאילתהמספר דקות המסחר (מתוך 390) שבהן נסחר כל סימול בארה'ב
ה-SQL המדויק מאחורי כל מספר
WITH per_symbol AS
(
    SELECT
        ticker,
        count() AS populated_minutes
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE window_start >= '2026-06-10 00:00:00'
      AND window_start <  '2026-06-11 06:00:00'
      AND toDate(toTimeZone(window_start, 'America/New_York')) = '2026-06-10'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY ticker
)
SELECT
    multiIf(
        populated_minutes <=  39, '1-39',
        populated_minutes <=  78, '40-78',
        populated_minutes <= 117, '79-117',
        populated_minutes <= 156, '118-156',
        populated_minutes <= 195, '157-195',
        populated_minutes <= 234, '196-234',
        populated_minutes <= 273, '235-273',
        populated_minutes <= 312, '274-312',
        populated_minutes <= 351, '313-351',
                                  '352-390') AS minutes_traded_bucket,
    count()                                  AS stock_count
FROM per_symbol
GROUP BY minutes_traded_bucket
ORDER BY min(populated_minutes)
Run this yourself

4677 ניירות ערך נופלים בטווח ה-1-39 ו-2020 בטווח ה-352-390. סדרת דקה אחת עבור כל נייר שאינו בטווח העליון היא במידה רבה שאלה של אופן הטיפול בפערים.

עצירות מסחר, מכירות פומביות, ויום המסחר

מרווחי זמן מסוימים יכולים להכיל הרבה יותר מסחר מאשר שכניהם. המכירה הפומבית של סוף היום (closing auction) היא המקרה שהקוראים מצפים לו: עסקה בודדת וגדולה מאוד שמרווח אחד צריך לספוג, לצד גרפים שנבנו ממסחר רציף רגיל. היכן שעסקה זו נוחתת בסדרת דקות היא מוסכמה של הספק ולא חוק, וניתן לבדוק זאת. הפאנל להלן עוקב אחר נפח המסחר של SPY דקה אחר דקה עד לסגירת אותו יום מסחר.

שאילתהווליום SPY לפי דקה לקראת הנעילה, 10 ביוני 2026
ה-SQL המדויק מאחורי כל מספר
WITH minute_volume AS
(
    SELECT
        toStartOfMinute(toTimeZone(window_start, 'America/New_York')) AS et_minute,
        sum(volume)                                                   AS shares
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2026-06-10 19:45:00'
      AND window_start <  '2026-06-10 20:10:00'
    GROUP BY et_minute
)
SELECT
    formatDateTime(et_minute, '%H:%i')                                            AS et_time,
    round(toFloat64(shares) / 1e6, 2)                                             AS volume_millions,
    round(100 * toFloat64(shares) / (SELECT toFloat64(sum(shares)) FROM minute_volume), 1) AS share_of_window_pct
FROM minute_volume
ORDER BY et_minute
Run this yourself

לאורך ה-25 דקות באותו חלון זמן, הגרף ה-16:00 מכיל 4.8% מהמניות, 0.49 מיליון מהן, קרוב למספר שהיה מתקבל מחלוקה שווה, בעוד שהגרף ה-15:45 שפותח את החלון נושא 0.19 מיליון. אין זינוק בסדר גודל של מכירה פומבית ב-16:00 בסדרת דקות זו, וזהו הלקח המעשי: עסקת הסגירה אינה נמצאת אוטומטית בגרף שנקרא על שם פעמון הסגירה. ספקים נבדלים בשאלה האם היא נוחתת בדקה הרציפה האחרונה, בגרף הנושא חותמת זמן של הפעמון, או מחוץ לסדרת הדקות לחלוטין, ונפח גרף ממוצע סביב הסגירה אומר דבר שונה תחת כל אחת ממוסכמות אלו.

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

ימי מסחר הם הגבול השלישי. עסקאות לפני ואחרי שעות המסחר קיימות ב-tape ובגרפי דקות, ושעות מסחר רגילות הן פילטר שמישהו מפעיל, לא תכונה של הנתונים. גבוה יומי שנלקח על פני כל שעות המסחר וגבוה שנלקח בין 09:30 ל-16:00 ET הם מספרים שונים בימים רבים. המדריך שלנו ל-מסחר לפני ואחרי שעות המסחר מכסה מה נסחר בחלונות אלו.

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

תיקונים כותבים מחדש גרפים שכבר השתמשת בהם

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

עבור backtest, הפער הזה משמעותי. מחקר המורץ על היסטוריה נקייה, מתוקנת ומסוננת לפי תנאים של היום, קורא tape שאף מערכת חיה לא ראתה, וזו אחת הצורות השקטות של look-ahead bias ב-backtesting. שמירת מה שהספק שידר בשידור חי, ולאחר מכן בנייה מחדש של אותו חלון זמן מאוחר יותר והשוואת השניים, מודדת את הפער במקום להניח שהוא אינו קיים.

שאלות נפוצות

מה מייצג OHLCV?

Open (פתיחה), High (גבוה), Low (נמוך), Close (סגירה), ו-Volume (נפח). עבור מרווח נתון, אלו הם מחיר העסקה הראשון, מחירי העסקה הגבוה והנמוך ביותר, מחיר העסקה האחרון, וסך המניות שנסחרו.

האם גרף של דקה אחת כולל עסקה ב-09:31:00 בדיוק?

כן, בגרף של 09:31. גרפים סטנדרטיים משתמשים במרווח חצי-פתוח [09:31:00, 09:32:00), כך שהקצה השמאלי שייך לגרף זה והקצה הימני שייך לגרף הבא.

מדוע הגרפים שבניתי מחדש אינם תואמים לאלו של ספק הנתונים שלי?

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

מה קורה לדקה ללא עסקאות?

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

האם גרפי OHLCV כוללים מסחר לפני ואחרי שעות המסחר?

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

הערות נתונים והפניה לקודי תנאי

פאנל ההפניה להלן מתאים קודי תנאי לפי שם; פאנל ה-odd lot לעיל מסמן עסקאות לפי קוד ה-odd lot עצמו.

שאילתהקודי תנאי מסחר המגבילים את ה-high, ה-low וה-last
ה-SQL המדויק מאחורי כל מספר
SELECT
    id        AS condition_id,
    any(name) AS condition_name,
    any(type) AS condition_type
FROM global_markets.stocks_condition_codes
WHERE asset_class = 'stocks'
  AND (positionCaseInsensitive(name, 'odd lot') > 0
       OR positionCaseInsensitive(name, 'out of sequence') > 0
       OR positionCaseInsensitive(name, 'derivatively') > 0
       OR positionCaseInsensitive(name, 'average price') > 0
       OR positionCaseInsensitive(name, 'prior reference') > 0)
GROUP BY id
ORDER BY id
Run this yourself

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


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