ביצוע Backtest ב-Python ללא מפתח API
למדו כיצד להריץ בדיקה לאחור הניתנת לשחזור מלא ב-Python ללא צורך במפתח API. המדריך מסביר על התקנות מקובעות, נתונים דטרמיניסטיים וניתוח ביקורתי של עקומות הון בשוק ההון.
בדיקה לאחור (Backtest) הניתנת לשחזור
בדיקה לאחור ב-Python נחשבת ניתנת לשחזור כאשר אדם זר יכול להריץ אותה מחדש על מכונה נקייה ולקבל בדיוק את אותם המספרים, ללא צורך בחשבון או במפתח API. רוב המדריכים נכשלים במבחן הזה כבר בשורה הראשונה, שכן הורדה בזמן אמת מספקת לקורא הבא היסטוריית מחירים השונה במעט מזו שקיבל הכותב. מדריך זה מתמקד במנוע קוד פתוח אחד, quantjourney-bt בגרסה 0.12.4, מריץ את הדוגמה המצורפת אליו ללא צורך בפרטי הזדהות, ולאחר מכן משתמש בנתוני שוק אמיתיים כדי להראות מה הרצה נקייה אחת עדיין לא יכולה לומר לכם.
מה הופך בדיקת Backtest לשחזורית?
לשחזוריות יש כאן משמעות צרה וניתנת לבדיקה: אדם שני מריץ פקודה אחת על מכונה נקייה ומקבל את המספרים שלכם עד לרמת הספרה העשרונית האחרונה. שני דברים רגילים משבשים זאת.
הראשון הוא הקוד. ספרייה בגרסת 0.x אינה נושאת הבטחה לתאימות בין גרסאות משנה. שינוי שם, שינוי בערך ברירת מחדל, או סידור מחדש של עמודה – והסקריפט שלכם ימשיך לרוץ בזמן שהוא מדווח בשקט על משהו אחר.
השני הוא הנתונים. מדריך שהשורה הראשונה שלו היא הורדה חיה מעולם לא היה שחזורי. ספקים מעדכנים נתוני עבר, מבצעים התאמות לפיצולי מניות (splits) וממלאים פערים, ואותו סקריפט מדפיס מספרים שונים חודש לאחר מכן. לאחר מעשה, לא ניתן להפריד בין שינוי בקוד לבין שינוי בנתונים.
הדפוס שכדאי להעתיק מהפרויקט הזה הוא הצימוד של גרסת מנוע קבועה עם ערכת נתונים קטנה ומצורפת, שנארזת בתוך החבילה עצמה.
קיבוע ההתקנה: pip install quantjourney-bt==0.12.4
quantjourney-bt הוא ה-backtester של QuantJourney, המופץ תחת רישיון Apache License 2.0 ומחייב Python 3.11 או גרסה חדשה יותר. גרסה 0.12.4 פורסמה ב-21 ביולי 2026, והיא הגרסה אליה מתייחסת כל פקודה להלן, כפי שתועד באוגוסט 2026.
עבוד בתוך סביבה מבודדת. python3 -m venv .venv יוצרת אחת כזו, source .venv/bin/activate נכנסת אליה, ו-python -m pip install -U pip מעדכנת את המתקין בתוכה. לאחר מכן, השתמש בגרסה המדויקת: pip install quantjourney-bt==0.12.4.
הפרויקט מתעד את הצורה הלא-מקובעת, pip install quantjourney-bt. החלק של ==0.12.4 הוא באחריותך, ובגרסת 0.x הוא מצדיק את קיומו. רשום את הקיבוע במקום שבו האדם הבא ימצא אותו: pip freeze > requirements.txt מתעדת כל תלות שנפתרה, כולל אלו שמעולם לא ציינת בשמן.
קיימות שתי תוספות אופציונליות. pip install "quantjourney-bt[wf]" מוסיפה את Optuna עבור דוגמאות ה-walk-forward והאופטימיזציה. pip install "quantjourney-bt[data]" מוסיפה גיבוי של yfinance המשמש עבור מדדי ייחוס (benchmarks).
רישיון Apache-2.0 הוא מתירני. באפשרותך להשתמש בקוד ולשנות אותו למטרות מסחריות, עליך לשמור על קבצי הרישיון וההודעות בכל הפצה מחדש, והתורמים מעניקים זכויות פטנט במפורש.
כיצד להריץ את דוגמת ה-SMA המצורפת ללא מפתח API
המאגר כולל סקריפט הפעלה לצד חמישים אסטרטגיות דוגמה שניתן להריץ, המחולקות למסלול מבוסס משקולות ומסלול מבוסס פקודות, ביניהם חמישה תהליכי עבודה מסוג walk-forward. ./strategy.sh --list מדפיס את הקטלוג. ./strategy.sh example_weights_01_sma_daily --check מייבא אסטרטגיה בודדת ואינו ניגש כלל לנתונים, מה שמהווה את האישור המהיר ביותר לכך שההתקנה תקינה.
הרצת ההדגמה עצמה מסתכמת בשורה אחת: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
הדגל --sample-data הוא לב העניין. הפרויקט מתאר את מערך הנתונים שמאחוריו כך:
מערך הנתונים לדוגמה הוא קטן וניתן לשחזור באופן מכוון. הוא שימושי לבדיקות התקנה, הפקת דוחות וקריאת זרימת המנוע מבלי ליצור חשבון.
מקור: quantjourney-bt README, גרסה 0.12.4, נקרא ב-6 באוגוסט 2026.
ההרצה כותבת ספרייה במקום פלט קונסולה: summary.txt ו-summary.json, קובץ metrics.csv, קובץ equity_curve.csv לצד ה-equity_curve.png שלו, dashboard.html, תיקיית plots/, וקובץ run_metadata.json המתעד כיצד הוגדרה ההרצה. הקובץ האחרון הוא זה שרוב האנשים מדלגים עליו, והוא זה שהופך תוצאה לניתנת לביקורת כעבור שנה.
קרא את המדדים המתקבלים ביושר. מערך הנתונים המצורף הוא קטן ונועד להמחשה בלבד, לכן יחס שארפ (Sharpe ratio) ו-הירידה המקסימלית מהשיא המודפסים ב-summary.txt מתארים קובץ דוגמה. הם אינם מהווים עדות לאסטרטגיה, והתייחסות אליהם כאל תוצאה היא הטעות הראשונה שניתן לעשות.
מה שההרצה כן מבססת הוא בעל ערך: ההתקנה עובדת, וזרימת המנוע המלאה, מהאות ועד למשקולות היעד וערך התיק המשוחזר, מייצרת את התוצרים שלה על המכונה שלך ללא צורך באף אישור. קיים מסלול מאובטח לשירות הנתונים של הפרויקט עצמו עבור בדיקות לאחור (backtests) על היסטוריה אמיתית. מסלול זה מתועד, והמדריך הזה עוצר כאן, בחלק שאינו דורש דבר מאף אחד.
מה עקומת הון אחת בתוך המדגם אינה מספרת לכם
הרצת המדגם משרטטת עקומת הון אחת. להלן מה שעקומה זו אינה יכולה לעשות, כפי שנמדד על נתוני שוק אמיתיים ולא על קובץ הדגמה.
הלוח להלן מיישם את אותו הרעיון שבו משתמשת אסטרטגיית הדוגמה, ממוצע נע של עשרים ימי מסחר החוצה ממוצע של חמישים ימים, על SPY, ומדווח על כל שנה קלנדרית בנפרד משנת 2017 ועד 2025. הפוזיציה בכל יום מסחר נקבעת לפי שער הסגירה של יום המסחר הקודם, כך שהכלל לעולם אינו מבצע פעולות על סמך נתון שטרם היה זמין לו.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
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 d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearכלל אחד ללא שינוי, שנמדד 9 פעמים נפרדות. קראו את שתי עמודות האחוזים לפני שתקראו כל דבר אחר. בשנת 2017 הכלל סיים את השנה ב-16% לעומת 19.4% עבור החזקת SPY באותה תקופה. בשנת 2025 שתי אותן עמודות מציגות 10.4% ו-16.4%. הקוד זהה בשתי השורות. רק חלון הזמן זז.
עמודת ההצלבות מראה עד כמה הראיות הבסיסיות הופכות דלות. 4 שינויי פוזיציה לאורך 2025 אומרים ששנה שלמה של עקומת הון נשענת על קומץ החלטות, וזהו מדגם קטן מאוד כדי להגדירו כתוצאה.
האם אותו כלל מתנהג באותו אופן על נכסים אחרים?
שינוי טווח התאריכים הוא דרך אחת לבחון עקומת תשואה בודדת. שינוי היקום המנייתי הוא הדרך השנייה. הלוח להלן שומר על הפרמטרים קבועים ומריץ את אותו הכלל על חמישה נכסים סחירים לאורך חמש שנים קלנדריות, מ-2021 ועד 2025.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
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, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ ניצב בראש הלוח ב-40.2%, והשורה התחתונה, KO, מסתכמת ב-3.8%. עמודת ה-days_long_pct מדווחת איזה חלק מהתקופה בילתה כל גרסה בהחזקת פוזיציה כלשהי, 67% עבור השורה העליונה. סט פרמטרים אחד, חמישה יקומים, ומרווח רחב מספיק כדי להוכיח שבחירת המנצח בדיעבד אינה מעידה דבר על ההרצה שטרם ביצעת.
אין באמור משום המלצה לסחור ב-crossover. ה-crossover הוא כלי מדידה עבור ה-backtest, וה-backtest הוא מה שאנו מודדים.
היכן מחלחלת הטיית ה-look-ahead לבדיקות לאחור של משקולות
מנוע מבוסס משקולות מתרגם איתות למשקולות יעד, מדמה ביצועים (fills) אל מול אותן משקולות, ולאחר מכן מחשב מחדש את ערך התיק מתוך הפוזיציות שנוצרו. הכשל חבוי בנקודת החיבור שבין האיתות למשקל. אם המשקל של היום נגזר משער הסגירה של היום ואז מניב את התשואה של אותו יום, הבדיקה לאחור ביצעה מסחר על בסיס מידע שלא היה קיים בעת שליחת הפקודה. זוהי הטיית look-ahead, והיא אינה מפעילה שום התרעת שגיאה. היא פשוט גורמת לכל התוצאות להיראות טובות יותר.
הפאנל להלן מריץ את שתי הגרסאות של כלל אחד על אותה היסטוריה של SPY.
ה-SQL המדויק מאחורי כל מספר
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
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 d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearב-2017 הגרסה המבוססת על יום המסחר הקודם הניבה 16%, בעוד שהגרסה המבוססת על אותו יום מסחר הניבה 17.1%, פער של 1.1 נקודות אחוז. ב-2025 המרחק בין השתיים עמד על 1.6 נקודות אחוז. רק אחת מעמודות אלו יכולה להיות מופקת על ידי מכונה שלא ידעה מראש היכן ייסגר יום המסחר, וההפרש ביניהן הוא חשבונאות טהורה, ללא רעיון, ללא מיומנות וללא מסחר מאחוריה.
מנוע זה מצהיר על עמדתו בנוגע לשאלת התזמון, הצהרה ששווה יותר מהבטחה:
עבור ביצועים בפתיחה, החלקת מחיר (slippage) הרגישה לטווח המסחר רואה רק את הנר הקודם שהושלם, וקיבולת הנפח נחזית מתוך תצפיות בפיגור; המנוע אינו משתמש בשער הגבוה, הנמוך, הסגירה או בנפח המסחר המלא של אותו יום.
מקור: quantjourney-bt README, גרסה 0.12.4, נקרא ב-6 באוגוסט 2026.
ניתן לבדוק הנחה מתועדת אל מול המקור שכבר התקנת. הנחה לא מתועדת היא ניחוש בלבד.
מדוע קיימת תוספת ה-walk-forward
דוגמאות ה-walk-forward, מ-WF01 ועד WF05, מגיעות עם התוספת [wf] והתלות שלה ב-Optuna. ב-walk-forward מתאימים פרמטרים על מקטע היסטורי אחד, מודדים אותם על המקטע העוקב, ולאחר מכן מגלגלים את הזוג קדימה וחוזרים על הפעולה. הגרסאות המתגלגלות (rolling) והמתרחבות (expanding) נבדלות בשאלה האם חלון ההתאמה משמיט את הנתונים הישנים ביותר ככל שהוא מתקדם. דוגמה נוספת מוסיפה ניקוי (purge) ועיכוב (embargo) בכל נקודת גבול, ובכך משמיטה את התצפיות הקרובות ביותר לתפר, כדי שמקטע מותאם לא יוכל "לדלוף" למקטע שעליו הוא נמדד.
שום דבר מזה אינו הופך רעיון חלש לרעיון עובד. הוא מחליף מספר בודד בהתפלגות של מספרים שניתן להתווכח עליהם, וזהו כל השדרוג. הצעד שאחריו עדיין אינו כסף חי: מסחר בנייר לפני כסף אמיתי מודד את מה ש-backtest אינו יכול לראות מבנית, החל בשאלה האם הפקודות שלך מתבצעות במחיר הקרוב לזה שהסימולטור הניח, ו-מפסקי זרם לבוטים של מסחר מכסים את מה שהקוד שלך עושה ביום שבו הוא אינו מתפקד. עבור הסטטיסטיקה שמתחת לכל זה, ההערות שלנו על ספר המסחר הכמותי בקוד פתוח צוללות לרמה עמוקה יותר.
שאלות נפוצות
האם ניתן לבצע Backtest לאסטרטגיה ללא מפתח API?
כן. quantjourney-bt מופץ עם ערכת נתונים לדוגמה תחת דגל --sample-data, ואסטרטגיות הדוגמה שלו רצות מול נתונים אלו ללא צורך בחשבון או בפרטי הזדהות. ערכת הנתונים קטנה ונועדה להמחשה בלבד, לכן יש להתייחס להרצה כזו כבדיקת התקנה וצינור נתונים (pipeline) ולא כהוכחה לביצועי אסטרטגיה.
מדוע לקבע (pin) את גרסת חבילת ה-Backtesting של Python?
חבילה בגרסת 0.x אינה נושאת הבטחת תאימות בין גרסאות משנה, ושינוי בערך ברירת מחדל או שינוי שם של מדד לא ילוו בהתראה. קיבוע הגרסה באמצעות pip install quantjourney-bt==0.12.4 ותיעוד הסביבה בקובץ דרישות (requirements file) מבטיחים שתוצאה שהופקה היום תוכל להישחזר בשנה הבאה על אותו מנוע שייצר אותה.
תחת איזה רישיון מופץ quantjourney-bt?
רישיון Apache 2.0. הרישיון מתיר שימוש מסחרי ושינויים, מחייב שמירה על קבצי הרישיון וההודעות בכל הפצה מחדש, וכולל הענקת פטנט מפורשת מהתורמים. גרסה 0.12.4 פורסמה ב-21 ביולי 2026 ומחייבת Python 3.11 או חדש יותר.
האם תוצאת Backtest חזקה מעידה על כך שהאסטרטגיה עובדת?
לא. Backtest הוא מדידה אחת, על חלון זמן אחד, ביקום נתונים אחד. הלוחות לעיל מראים כלל יחיד וקבוע המניב נתונים שנתיים שונים מאוד עבור Ticker אחד, ונתונים שונים מאוד על פני חמישה שמות; זהו הפער שנועד להיחשף באמצעות ולידציית Walk-forward ובדיקות Out-of-sample.
כיצד חושבו הלוחות לעיל
מחירי הסגירה היומיים הם ה-print האחרון של דקת המסחר הרגילה לכל תאריך, לפי שעון ניו יורק בין 09:30 ל-16:00, מה ששומר על דיוק בימי מסחר מקוצרים ללא צורך בקידוד קשיח של אורך הסשן. הממוצע המהיר מכסה עשרים סשנים והממוצע האיטי חמישים, שניהם פשוטים (SMA), ו-49 הסשנים הראשונים בכל סדרה משמשים כחימום (warm-up) שבו לא מוחזקת פוזיציה. הנתונים השנתיים משקללים את תנועת הסגירה-לסגירה של כל סשן עבור הסשנים שבהם הכלל היה בלונג, ועמודת ה-hold משקללת כל סשן באותה שנה לצורך השוואה. חמשת השמות בחתך הרוחב נבחרו בשל היסטוריה רציפה ללא פיצול (split) בתוך חלון הזמן, כך שסדרת הסגירה אינה דורשת התאמה. חלונות הזמן קבועים בעבר, ולכן לוחות אלו מציגים את אותם מספרים בכל הרצה מחדש.
כל לוח כאן נושא את שאילתת ה-SQL המדויקת שמאחוריו, מה שהופך את המספרים בדף זה לניתנים להרצה חוזרת בדיוק כמו ההתקנה המקובעת. כדי למדוד כלל על פני חלון זמן משלך לפני כתיבת קוד Backtest, שאל את השאלה באנגלית פשוטה בטרמינל Strasmore.