11 בספטמבר 2026 · שחזוריות

אותו קוד, אותם נתונים, שני ערכי Sharpe שונים: ביקורת אי-הדטרמיניזם שהבדיקה ההיסטורית שלכם צריכה

אותו קוד, אותם נתונים, שני ערכי Sharpe שונים: ביקורת אי-הדטרמיניזם שהבדיקה ההיסטורית שלכם צריכה

אותו commit, אותם קובצי parquet, אותה מכונה, שתי הרצות בהפרש של שעה. Sharpe של 1.34 ו־Sharpe של 1.19. תשואה כוללת של 41.2% ושל 37.8%. מספר העסקאות: 1,418 ו־1,421. ושתי עקומות ההון היו זהות עד הספרה העשרונית המודפסת האחרונה במשך 11,205 נרות רצופים, לפני שנפרדו.

0.15פער Sharpe, קלטים זהים
3 / 1,418עסקאות שונות
11,205נרות זהים עד הפיצול

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

האסטרטגיה: מומנטום רוחבי על 30 חוזי perpetual מסוג USDⓈ-M בעלי נפח המסחר הגבוה ביותר, איזון מחדש כל 4 שעות, לונג בחמשת הנכסים בעלי התשואה הגבוהה ביותר ב־12 השעות האחרונות, שורט בחמשת הנמוכים ביותר, היסטוריה של 18 חודשים, עמלות ומימון מחויבים לכל רגל.

מציאת הנר שבו ההרצות נפרדות

אם מתעדים את ההון בכל נר בדיוק מלא, זה לוקח בערך ארבע דקות. מייצאים את שתי ההרצות ל־CSV של (bar_index, equity, open_positions_hash), טוענים את שתיהן ומוצאים את האינדקס הראשון שבו יש הבדל. כבר כתבנו את הסקריפט הזה בשביל באג אחר, וזו הסיבה היחידה שלא בזבזתי עליו בוקר שלם.

נר 11,206, 2025-03-14T08:00 UTC. ההון בנר הקודם זהה עד 13 ספרות משמעותיות. בנר 11,206 גיבובי הפוזיציות שונים: הרצה A בלונג על SOL, הרצה B בלונג על AVAX. אותו כיוון, אותו ערך נקוב, סימול אחר. כל מה שבא אחר כך הוא תוצאה של זה.

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

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

שוויון, מיון לא יציב והדבר שהתעלמתי ממנו במשך שנתיים

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

שוויונות כאלה אינם אירוע חריג. הם מבניים. בכל מקום שבו פיצ'ר מגיע לרוויה או נחתך, נוצרת זהות מדויקת: נרות ללא נפח מסחר נותנים תשואות אפס בדיוק, ציון z שנחתך נתקע ב־±3.0, המרת ערכים לדירוג עם מעט ערכים מובחנים מייצרת עשרות שוויונות, ומסנן בוליאני נותן ציון 1.0 לכל מה שעובר. במהלך 18 חודשים של נרות בני 4 שעות, היו בהרצה הזו 47 נרות עם שוויון בגבול הבחירה. בשלושה מהם השתנתה סל הנכסים שנבחר. בשאר המקרים היה שוויון בין שני נכסים שכבר נכללו שניהם או נותרו שניהם בחוץ.

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

למה שלוש עסקאות שוות 0.15 ב־Sharpe

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

הסטייה הראשונה כמעט לא עלתה לנו בפני עצמה. רגל ה־AVAX של הרצה B הפסידה 2.1% במהלך תשע שעות; רגל ה־SOL של הרצה A הרוויחה 0.4%. פער ההון אחרי העסקה הזו: 0.21%. זניח. אבל מאז, שתי ההרצות כבר אינן אותה אסטרטגיה. הן מחזיקות פוזיציות מעט שונות בגודלן, ולכן צוברות שיעורי מימון מעט שונים, ושניים משוויונות הגבול המאוחרים נשברו שוב אחרת, כי הציונים שהזינו אותם חושבו כעת מפוזיציות מוחזקות שונות במקצת. אחד מהם קרה ב־2025-03-27, יום לפני מגמה שנמשכה שישה ימים והניבה בערך שליש מה־PnL הכולל של ההרצה. הרצה A הייתה בפנים לאורך כל המהלך; הרצה B נכנסה באיחור של מועד איזון מחדש אחד.

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

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

חמשת המקומות שבהם זה באמת קורה

מקורתסמיןפתרון
‏PYTHONHASHSEED לא מקובע וסדר איטרציה של set/dict שמזין מיוןהכרעה שונה בשוויון בין הרצות; סטייה ראשונה בנר מסויםקבעו את ה־seed; מיינו לפי מפתח משני מפורש (סימול) כדי שההכרעה בשוויון תהיה דטרמיניסטית
מיון לא יציב לפי מפתח שיש בו שוויון (quicksort ברירת מחדל ב־NumPy/pandas)כמו לעיל, ממשיך להשפיע גם אחרי קיבוע ה־seedkind="stable", או הפכו את המפתח לטוטלי
RNG לא מאותחל ב־bootstrap, בערבובים של train/test או בתנודות סינתטיות של ביצועי פקודותסטייה לאורך כל ההרצה, ללא נקודת סטייה ברורהseed מפורש לכל רכיב, מתועד במניפסט ההרצה
צמצום מקביל של מספרי נקודה צפה (סדר הסכימה תלוי במספר ה־threads)הבדלים בסיביות האחרונות, לרוב זניחים עד שהם חוצים סף בהשוואהקבעו את מספר ה־threads בהרצות מחקר; לעולם אל תשוו מספרי נקודה צפה עם == על גבול הכרעה
גרסאות ספריות לא מקובעותניתן לשחזר היום, לא בנובמברגיבוב קובץ הנעילה במניפסט, לצד גיבוב תמונת הנתונים

השורה הרביעית היא זו שפחות משפיעה מכפי שאנשים חוששים, והשורה הראשונה היא זו שמכשילה שוב ושוב.

שחזור זהה עד לרמת הביט הוא כלי, לא מטרה בפני עצמה

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

אבל ברגע שאפשר לשחזר, כדאי גם לשבש זאת בכוונה. הריצו את המערכת 64 פעמים עם 64 seed-ים ובדקו את הפיזור:

רצועת התנודתיות. אותה אסטרטגיה, אותם נתונים, 64 תמורות של הכרעות בשוויון וסדר ביצוע, עם seed. Sharpe ב־p5 הוא 1.12, החציון 1.27, וב־p95 הוא 1.41. רוחב הרצועה: 0.29.

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

ההבנה הזו שינתה את הדרך שבה אנחנו בוחרים. תוצאת סריקה היא דירוג רק אם הפערים בין התצורות גדולים מהתנודתיות של תצורה בודדת, ובאסטרטגיה שתלויה במסלול עם 1,400 עסקאות התנודתיות לרוב גדולה מספיק כדי להפוך את השליש העליון של טבלת המובילים לתיקו. כשזה קורה, בחרו לפי מדד שהרצועה אינה יכולה להסתיר: פחות תחלופה, פחות פרמטרים, הנחת עלויות שתוכלו להגן עליה מול ספקן, או התנהגות טובה יותר בקפל ה־walk-forward שאתם הכי פחות אוהבים. אלה שוברי שוויון אמיתיים. יתרון של 0.15 ב־Sharpe אינו כזה.

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

דטרמיניזםבדיקה היסטוריתהנדסת נתוניםהתאמת יתרpython
← כל הפוסטים