בניית מרווחי סמך בשיטת Bootstrapping לבדיקות Backtest
עקומת הון אחת היא דגימה בודדת בלבד. מרווח סמך בשיטת Bootstrapping עבור מדד Sharpe מסייע להבין את מהימנות האסטרטגיה. כך בונים ומפרשים את התוצאות בבדיקה לאחור.
מרווח סמך בבדיקה לאחור (Backtest)
מרווח סמך בבדיקה לאחור עונה על שאלה אחת: כמה מעקומת ההון נובע מהאסטרטגיה, וכמה ממקטע ההיסטוריה הספציפי שבו היא פעלה. שימוש בשיטת Bootstrapping בבדיקה לאחור בונה את המרווח הזה באמצעות דגימה מחדש של סדרת התשואות פעמים רבות, חישוב מחדש של המדד הסטטיסטי בכל דגימה, ובחינת האחוזונים של התוצאות המתקבלות. מדד Sharpe של 1.4 שנמדד לאורך שנה אחת של תצפיות יומיות נושא מרווח סמך של תשעים וחמישה אחוזים, הרחב מספיק כדי להכיל את האפס; הלוחות להלן מודדים מדוע זה המצב.
מדוע עקומת הון אחת היא דגימה בודדת
בדיקת Backtest מספקת מספר אחד לכל מדד סטטיסטי: יחס Sharpe אחד, תשואה שנתית אחת, Drawdown מקסימלי אחד, ושיעור הצלחה אחד. אף אחד מאלו אינו הערך האמיתי של האסטרטגיה. כל אחד מהם הוא אומדן המבוסס על רצף סופי של ימי מסחר, וכל תקופה אחרת באותו אורך הייתה מניבה תוצאות שונות.
הפאנל להלן מוציא את האסטרטגיה מהמשוואה לחלוטין. הוא מודד את הפוזיציה הפשוטה ביותר האפשרית: החזקת SPY, שנה קלנדרית בכל פעם, על בסיס תשואות מחיר Close-to-close.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2011-12-01'
AND date < '2026-01-01'
)
SELECT
toString(toYear(date)) AS year,
count() AS obs_count,
round(avg(ret) * 252 * 100, 2) AS ann_return_pct,
round(stddevSamp(ret) * sqrt(252) * 100, 2) AS ann_vol_pct,
round(avg(ret) / stddevSamp(ret) * sqrt(252), 2) AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
AND ret IS NOT NULL
GROUP BY year
ORDER BY yearכל שורה מכסה כ-250 ימי מסחר, קרוב לשנת מסחר סטנדרטית. דבר בפוזיציה לא השתנה לאורך אף אחת מהן. ב-2012 יחס ה-Sharpe השנתי נמדד כ-1.06; ב-2025 הוא נמדד כ-0.88, עם 14 שנים המוצגות בתרשים. שנה אחת של החזקת מדד רחב מזיזה את המספר המרכזי בשיעור שרוב הקוראים היו מסווגים כרעש, ובדיקת Backtest לאסטרטגיה באותו אורך יורשת לפחות את אותה רמת תנודתיות. מוסכמת השנתיות שמאחורי העמודה מפורטת ב-מדריך יחס Sharpe, והמכניקה של תשואות תקופתיות ב-כיצד נמדדות תשואות חודשיות.
מהו רוחב הטווח סביב מדד Sharpe בבדיקה לאחור (backtest)?
טעות התקן של אומדן Sharpe יורדת בקירוב לפי השורש הריבועי של מספר התצפיות. במקום להסתמך על נוסחה זו, יש למדוד את המרווח (spread) באופן ישיר. חלקו עשרים שנות מסחר לחלונות זמן ללא חפיפה באורך קבוע, חשבו את מדד ה-Sharpe השנתי בתוך כל חלון, ובחנו את המרחק בין התוצאות שהתקבלו.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
blocks AS
(
SELECT
w,
intDiv(i, w) AS blk,
count() AS n,
avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252) AS sharpe
FROM indexed
CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
GROUP BY w, blk
HAVING n = w
)
SELECT
concat(toString(w), ' sessions') AS horizon,
count() AS block_count,
round(quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_p05,
round(quantileDeterministic(0.50)(sharpe, blk), 2) AS sharpe_p50,
round(quantileDeterministic(0.95)(sharpe, blk), 2) AS sharpe_p95,
round(quantileDeterministic(0.95)(sharpe, blk)
- quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY wב-21 sessions לכל חלון, האחוזון החמישי עד ה-95 של מדד ה-Sharpe הנמדד נע בין -3.37 ל-6.97, מרווח של 10.34 נקודות Sharpe על פני 238 חלונות. הרחיבו כל חלון ל-504 sessions והמרווח יורד ל-1.67 נקודות, הנמדדות כעת על פני 8 חלונות בלבד. שני דברים משתנים בו-זמנית. האומדן הופך למדויק יותר ככל שגודל המדגם גדל, ומספר הדגימות הבלתי-תלויות שההיסטוריה יכולה לספק מצטמצם. מתח זה הוא לב העניין כולו.
כיצד לבצע Bootstrap לרווח סמך של Backtest
התהליך קצר מספיק כדי לפרטו במלואו.
- התחילו עם סדרת התשואות הממומשות של האסטרטגיה, נתון אחד לכל תקופה, בסך הכל N נתונים.
- דגמו N תשואות מתוך הרשימה באקראי, עם החזרה. חלקן יופיעו פעמיים, וחלקן לא יופיעו כלל.
- חשבו מחדש את המדד הסטטיסטי על המדגם החוזר.
- חזרו על הפעולה כמה אלפי פעמים, ושמרו כל ערך שהתקבל.
- מַיינו את הערכים שנשמרו וקראו את האחוזון ה-2.5 והאחוזון ה-97.5 עבור רווח סמך של 95 אחוז.
קבעו Seed למחולל המספרים האקראיים לפני שלב 2, ותעדו את ה-Seed לצד המרווח שפורסם. Bootstrap הוא אומדן Monte Carlo: שתי הרצות ללא Seed יניבו תוצאות שונות בספרות האחרונות, וסוקר שאינו יכול להריץ מחדש את המרווח שלכם לא יוכל לאמת אותו. זוהי אותה משמעת המתוארת ב-הגדרת Backtest הניתנת לשחזור.
תשואה ממוצעת ומדד Sharpe עוברים את שלב 2 בצורה נקייה, שכן שניהם קוראים את רשימת התשואות כקבוצה. Maximum drawdown אינו פועל כך. ה-Drawdown קורא את הנתיב לפי סדר כרונולוגי. מדגם חוזר שמשנה את סדר התשואות מניב Drawdown מקסימלי ששום סידור של רצף המסחר האמיתי לא הפיק. עם זאת, עדיין כדאי לבצע Bootstrap לנתון זה, בתנאי שהפלט יסומן כפי שהוא: התפלגות ה-Drawdown על פני היסטוריות שעורבבו מחדש, ולא תחזית של הרצף הבא. ההגדרה נמצאת ב-maximum drawdown.
מדוע שיטת ה-IID bootstrap שגויה עבור תשואות שוק
שלב 2 מניח שכל תשואה היא בלתי תלויה ובעלת התפלגות זהה, הנחת ה-IID. תשואות יומיות מפרות הנחה זו באופן שמשנה את רוחב המרווח. תשואות בעלות סימן נושאות זיכרון של יום אחד בלבד, וגם הוא חלש. לעומת זאת, עוצמת התנודות מתקבצת: מהלכים גדולים מופיעים לצד מהלכים גדולים, וימים שקטים מגיעים ברצפים.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
ticker,
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
AND date >= '2015-11-01'
AND date < '2026-01-01'
),
lagged AS
(
SELECT
ticker,
date,
ret,
lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
FROM daily
WHERE ret IS NOT NULL
AND abs(ret) < 0.35
)
SELECT
ticker,
count() AS obs_count,
round(corr(ret, ret_prev), 3) AS return_autocorr,
round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESCעבור SPY, האוטוקורלציה בפיגור של אחד של תשואות יומיות מוחלטות נמדדה כ-0.366 לאורך 2514 ימי מסחר, לעומת -0.133 עבור התשואות בעלות הסימן באותם ימים. השוו בין שני העמודים עבור כל שם בתרשים. ערבוב (shuffling) של סדרת תשואות משמיד את ההתקבצות הזו, וביצוע IID bootstrap על נתונים אלו מציג מרווח צר יותר ממה שהמדגם תומך בו. הטעות פועלת בכיוון הכי פחות מועיל: היא מציגה את האסטרטגיה באור מחמיא מדי.
שיטת ה-moving block bootstrap ובחירת אורך הבלוק
התיקון הוא לבצע דגימה מחדש של בלוקים רציפים במקום של תשואות בודדות. בוחרים אורך בלוק L, דוגמים באקראי בלוקים של L תשואות עוקבות עם החזרה, ומחברים אותם זה לזה עד לקבלת סדרה סינתטית באורך N. התלות בתוך הבלוק נשמרת בשלמותה: התשואות נותרות בסדרן המקורי. רק החיבורים בין הבלוקים הם מלאכותיים.
אורך הבלוק יוצר פשרה בין שתי טעויות, ואין הגדרה שפוטרת משתיהן. בלוקים קצרים מתנהגים כמו IID bootstrap וממעיטים בערך המרווח, מה שמהווה הטיות. בלוקים ארוכים משמרים תלות רבה יותר אך מותירים פחות בלוקים נפרדים לדגימה, כך שכל דגימה מחדש חוזרת על נתחים גדולים מאותה היסטוריה והמרווח עצמו הופך לרועש, מה שמהווה שונות. כללי אצבע שפורסמו קובעים את אורך הבלוק לפי N בחזקת שליש. יש להתייחס אליהם כנקודות מוצא בלבד.
אבחון זול יותר הוא לבדוק כיצד השונות משתנה בהתאם לאופק הזמן. תחת הנחת אי-תלות, השונות של סכום תשואות ל-k ימים שווה ל-k כפול השונות היומית, והיחס בין השניים עומד על אחד.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
base AS
(
SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
SELECT
k,
intDiv(i, k) AS blk,
count() AS n,
sum(ret) AS block_ret
FROM indexed
CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
GROUP BY k, blk
HAVING n = k
)
SELECT
concat(toString(k), ' sessions') AS block_length,
count() AS block_count,
round(varSamp(block_ret) / (k * any(var_1d)), 3) AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY kב-2 sessions היחס שנמדד היה 0.844; ב-63 sessions הוא עמד על 0.584, בחישוב על פני 78 בלוקים שאינם חופפים. ערכים הקרובים לאחד מצביעים על כך שהסכום משתנה באותו אופן שבו היו משתנות דגימות בלתי תלויות באותו אופק זמן. ערכים הרחוקים מאחד מסמנים את אופקי הזמן שבהם התלות עדיין משפיעה, וזהו הטווח שאורך הבלוק חייב לכסות. יש לבחור את הבלוק הקצר ביותר שמכסה טווח זה, ולאחר מכן לבחון לכמה ירד מספר הבלוקים הזמינים.
דגימה מחדש לפי קבוצות (Bucketed resampling) והספירה לצד כל מרווח
דגימה מחדש בתוך קבוצות, לפי משטר תנודתיות או לפי חודש קלנדרי, שומרת על ההתניות שהפכו את השאלה למעניינת מלכתחילה. אם הטענה היא שאסטרטגיה משיגה את מדד ה-Sharpe שלה במשטרים של תנודתיות גבוהה, מרווח שנבנה על ידי דגימה מימים של תנודתיות גבוהה בלבד הוא זה שבוחן את הטענה. המחיר הוא אריתמטי. פיצול של מאתיים וחמישים תצפיות לארבע קבוצות מותיר כשישים תצפיות בכל קבוצה, ו-bootstrap של שישים תצפיות מניב מרווח רחב בערך פי שניים בהשוואה לגרסה של המדגם המלא.
לכן, יש לציין את מספר התצפיות בכל קבוצה לצד כל מרווח, בכל פעם. עמודת ה-block_count בלוחות שלעיל היא ביטוי מוחשי להרגל זה: קריאה של אחוזון חמישי מתוך תשעה חלונות היא אובייקט שונה מקריאה מתוך מאתיים, גם כאשר שניהם מודפסים בדיוק של שתי ספרות אחרי הנקודה העשרונית.
מה ששיטת ה-bootstrap אינה יכולה לתקן
שיטת ה-bootstrap מכמתת דבר אחד בלבד: את רעש הדגימה בסטטיסטיקה המחושבת מתוך הנתונים שבידיך. היא אינה מספקת מידע על השאלה האם הנתונים הללו היו אי פעם ברי-השגה.
Backtest המזוהם ב-look-ahead bias מניב סדרת תשואות שמעולם לא הייתה ניתנת למסחר, וביצוע bootstrap עליה מחזיר טווח ביטחון צר ומטעה סביב בדיה. יקום מניות המורכב מהרכב המדד הנוכחי נושא בחובו survivorship bias, וכל דגימה מחדש של יקום זה יורשת את ההטיה הזו. פער שלישי הוא אפקט הברירה: הרץ מאתיים וריאציות, שמור את הטובה ביותר, ומרווח ה-bootstrap שלה יתאר את רעש הדגימה של אותה וריאציה בלבד, תוך התעלמות ממאה ותשעים ותשע הדגימות האחרות ששימשו לבחירתה. טווחי ביטחון מהימנים הם שימושיים, אך הם אינם מהווים תחליף לנתוני out-of-sample.
הערות על נתונים ומתודולוגיה
- התשואות הן תשואות מחיר מבוססות שער סגירה ליום מסחר. דיבידנדים אינם כלולים, מה שמוביל להערכת חסר של רמת כל תשואה ומדד Sharpe בשיעור של בערך תשואת הדיבידנד. הפיזור שבו עוסק פוסט זה כמעט ואינו מושפע מכך.
- חלונות זמן ובלוקים אינם חופפים, כך שכל סטטיסטיקה נמדדת משתמשת בחלק נפרד של ההיסטוריה. חלונות חופפים היו מנפחים באופן מלאכותי את מספר הדגימות הנראה לעין.
- קוונטילים משתמשים באומד דטרמיניסטי, כך שהרצה חוזרת של פאנל מחזירה את אותו אחוזון ולא קירוב חדש.
- פאנל האוטוקורלציה משתמש בשש מניות גדולות ללא פיצול מניות (share split) בתוך חלון הזמן, ומסנן כל יום שבו נרשמה תנועה של מעל שלושים וחמישה אחוזים, כדי למנוע השפעה של עיוותי התאמת מחיר על המתאם.
שאלות נפוצות
מה מלמד אותי מרווח סמך מסוג bootstrap על בדיקה לאחור (backtest)?
הוא מספק את טווח הערכים שהסטטיסטיקה עשויה לקבל באופן סביר לו היה אותו תהליך נדגם מחדש לאורך אותו מספר תקופות. מרווח של תשעים וחמישה אחוזים המכיל את האפס מעיד על כך שהמדגם קצר מדי מכדי להבחין בין האסטרטגיה לבין היעדר יתרון כלל.
כמה דגימות מחדש (resamples) בשיטת bootstrap הן מספיקות?
עבור מרווח של תשעים וחמישה אחוזים, כמה אלפי דגימות מחדש הן בדרך כלל יציבות, ועשרת אלפים היא ברירת מחדל נפוצה שעלותה זניחה. קוונטילים עמוקים יותר בזנבות דורשים יותר: האחוזון הראשון מתוך אלף דגימות נגזר מתוך כעשרה ערכים בלבד.
מהו אורך הבלוק שבו יש להשתמש ב-moving block bootstrap?
אין אורך נכון אוניברסלי. כללי אצבע קושרים אותו לגודל המדגם בחזקת שליש, ובדיקה מעשית היא האופק שבו השונות מפסיקה להשתנות באופן ליניארי, דבר הנמדד בלוח יחס השונות לעיל. פרסם את האורך שבו השתמשת לצד המרווח.
האם ניתן לבצע bootstrap ל-maximum drawdown?
כן, עם סייג הנוגע לסדר. ה-drawdown תלוי ברצף התשואות, לכן דגימה מחדש המבוצעת על ידי ערבוב מדווחת על ה-drawdown של היסטוריה מסודרת מחדש. שיטת block bootstrap שומרת על רצפים קצרים כפי שהם והיא הכלי העדיף עבור סטטיסטיקות של מסלול.
האם bootstrap מתקן התאמת-יתר (overfitting)?
לא. הוא מודד רעש דגימה בתוך סדרת תשואות אחת. הטיות של מבט קדימה (look-ahead bias) ואפקט הברירה הנובע מבדיקת גרסאות רבות נמצאים מחוץ לטווח הראייה שלו.
כל לוח כאן נושא את ה-SQL שיצר אותו. שנה את ה-ticker או את אורך החלון והרץ זאת בעצמך בטרמינל Strasmore.