שנה קלנדרית של נרות בני 1 דקה אמורה לכלול 525,600 שורות. המשיכה שלנו לשנת 2025 של חוזי BTCUSDT מתמשכים החזירה 525,557 — חסרו 43 דקות, שיעור שלמות של 99.992%. חוזה מתמשך של אלט עם שווי שוק בינוני באותה זירה, באותה משיכה ובאותו מסלול קוד, החזיר 1,247 שורות פחות: 99.76%. ולסדרת נתוני דקות של מניות אמריקאיות באותה שנה היו בערך 427,000 שורות פחות מאשר לסדרת הקריפטו, וזה בכלל לא פער — פשוט שוק שנסגר.
שלושה מהמספרים האלה משעממים. ה-43 הם אלה ששווים אלף מילים.
ארבעה דברים שונים שנראים כמו חור ב-dataframe שלכם
לפני שנחזור ל-43, קצת על הסיווג — כי רוב הקוד לטיפול בפערים שגוי כבר בשלב הזה. כששורה חסרה בקובץ ה-parquet המקומי שלכם, אין לכם דרך לדעת מה מבין הדברים הבאים גרם לכך, והתגובה הנכונה שונה בכל מקרה.
| סוג | מה קרה בפועל | איך זה מופיע | התגובה הנכונה |
|---|---|---|---|
| לא היו עסקאות | השוק היה פתוח; איש לא חצה את המרווח באותה דקה | נר בנפח אפס (כל ערכי OHLC שווים, trades=0) בחלק מהנקודות, שורה חסרה באחרות | משאירים אותו. זה מידע אמיתי: איש לא רצה לסחור. |
| הזירה נפלה | מנוע ההתאמה הושבת, מתוכנן או לא | שורה חסרה, לפעמים רצף של כמה שורות | מסמנים כמידע מיושן. לא סוחרים לאורך הפער. |
| המסחר במכשיר הושעה | חריגה מרצועת LULD, המתנה לחדשות, הודעה על מחיקה מהמסחר | שורה חסרה ואחריה עסקה פומבית במכרז הפתיחה מחדש | מסמנים כמידע מיושן ומתייחסים לחידוש המסחר כאל קפיצה. |
| זו אשמתכם | עימוד שנחסם עקב מגבלת קצב, ניסיון חוזר שהשמיט עמוד, באג בלולאה בגבול אזור זמן | שורה חסרה, אי אפשר להבדיל בינה לבין המקרים לעיל | מזהים ומושכים מחדש. זה המקרה שאפשר לתקן. |
הזירות חלוקות בדעתן לגבי השורה הראשונה בטבלה, ודווקא כאן טמון המוקש. נקודת הקצה של Coinbase לנרות משמיטה לגמרי מקטעים ריקים, כך שההיסטוריה של צמד לא נזיל מלאה בחורים של ממש. נקודת הקצה klines של Binance בדרך כלל תחזיר נר סינתטי עם נפח 0 ועם open=high=low=close, כולם מקובעים לעסקה הקודמת. אותה עובדה בסיסית, שתי צורות שונות; טוען נתונים שמגדיר מחדש את האינדקס לרשת דקות מלאה ממיר צורה אחת לאחרת בלי להודיע לכם. בדקו מה הזירה שלכם עושה לפני שאתם כותבים קוד למילוי פערים, לא אחר כך.
כמעט כל 1,247 הדקות החסרות באלט השתייכו לקטגוריה הראשונה: ספר פקודות דליל ב-04:00 UTC ביום ראשון, בלי עסקאות. מעצבן, קל להבין, ולרוב לא מזיק כי ממילא האסטרטגיה לא הייתה סוחרת אז. וזו בדיוק הסיבה שהפסקתי להתעמק בזה וחזרתי ל-43.
43 הדקות לא היו מפוזרות
אילו החוסרים היו אחידים, 43 דקות בשנה היו מופיעות כ-43 מקרים בודדים, אחד כל שמונה ימים וחצי, כל אחד טעות עיגול. זה לא מה שקרה. הם הגיעו בשישה רצפים: אחד של 19 דקות רצופות, אחד של 11, שניים של 4 ועוד שני זוגות. שישה אירועים, לא 43 תקלות מקריות.
והאירועים תלויים במה שחשוב לכם. זירות נופלות כשהעומס עליהן גבוה, והעומס גבוה כשהמחיר זז. חילקתי את כל שעות השנה לפי תנודתיות ממומשת ובדקתי היכן הופיעו הדקות החסרות: 61% מהן היו בעשירון העליון. ההסתברות הבלתי מותנית שדקה נתונה תחסר היא 0.008%. בתוך שעה מהעשירון העליון של התנודתיות, היא בערך 0.05% — פי 6, ובנוסף גם מרוכזת ברצפים.
לכן מדד השלמות בלוח האיכות של הנתונים שלכם מודד את הדבר הלא נכון. 99.992% נשמע כמו מערך נתונים שאפשר להפסיק לחשוב עליו. בפועל, הנתון מתאר סדרה שלמה בשעות שבהן האסטרטגיה לא עושה כלום, ומלאת חורים בשעות שבהן היא עושה הכול. למערכת מומנטום שנכנסת לפעולה כשהתנודתיות מתרחבת יש סיכוי גבוה בהרבה להיתקל בפער מכפי שהמספר הכללי מרמז, והמפגש קורה באמצע עסקה.
מה שמילוי קדימה עושה שלוש שורות אחר כך
הנה הכשל שבגללו כתבתי את זה. ניקח את הרצף של 19 הדקות. ניקוי מקובל: מגדירים מחדש את האינדקס לרשת הדקות המלאה, ממלאים קדימה את OHLC לפי הסגירה האחרונה, וקובעים את הנפח ל-0. עכשיו הסדרה רציפה והאינדיקטורים שלכם מחושבים בלי NaN באופק.
ב-19 הנרות האלה high == low == close. הטווח האמיתי הוא אפס בכל אחד מהם. ATR(14) שמחושב בחלון הזה, על בסיס קריאה של כ-240 USDT לפני ההשבתה, יורד לכ-34 עד שהזירה חוזרת — חמשת הנרות האמיתיים ששרדו בחלון מחזיקים עכשיו לבדם את כל הממוצע. עכשיו מזינים את הנתון הזה למנגנון לקביעת גודל פוזיציה לפי תנודתיות, מהסוג הרגיל size = risk_budget / ATR. גודל הפוזיציה גדל פי 7.
הנר האמיתי הבא הוא עסקת החידוש, והוא לא נר שקט. במקרה שלנו הוא נפתח במרחק של 1.8% מהסגירה האחרונה לפני ההשבתה. הבדיקה לאחור לקחה בשמחה פוזיציה גדולה פי 7 לפני פער של 1.8%, בעסקה שלא יכלה להתבצע ובמחיר שאיש לא ציטט. העסקה הסינתטית היחידה הזאת הייתה שווה יותר מחודש של רווח והפסד לגיטימיים בעקומת ההון, והפעם בכיוון הלא נכון — והיא נולדה כולה משורת ניקוי נתונים שנכתבה כדי לסדר את ה-dataframe.
גם מחיקת השורות במקום מילוין אינה פתרון; זה אותו באג בתחפושת אחרת. מוחקים אותן, והחלונות הנסוגים מבוססי האינדקס השלם משקרים: עכשיו "EMA ל-20 נרות" משתרע על פני 39 דקות שעון במהלך ההשבתה, התשואה בין נרות מעבר לגבול היא הקפיצה כולה של 1.8% כאילו התרחשה בדקה אחת, וכל אומדן תנודתיות לנר מתייחס אליה כאירוע של 60 סיגמא. אין שום אזהרה. האינדקס עדיין מונוטוני.
דגימה מחדש היא המקום שבו הבעיה נעלמת מהעין
רוב המחקר אינו עובד עם נרות בני 1 דקה, אלא עם נתונים מצרפיים, והצבירה מטשטשת את הבעיה. בדגימה מחדש ל-5 דקות, חור של 19 דקות הופך ל-4 נרות, כשהראשון והאחרון חלקיים. Pandas יחשב OHLC שנראה סביר לחלוטין על סמך שתי דקות שנותרו, וייתן לו תווית זהה לזו של נר שנבנה מחמש דקות. בפלט אין שום דבר שמבדיל ביניהם.
הפתרון הזול ביותר שאני מכיר: לשמור עמודת bars_in_window לאורך כל פעולת דגימה מחדש ולעולם לא למחוק אותה. מספר שלם אחד לכל שורה, וכך אפשר לענות על כל שאלה בהמשך לגבי אמינותו של נר. אנחנו גם שומרים את seconds_since_last_real_print, שמכיל את אותו המידע בצורה ששכבת הביצוע יכולה לפעול לפיה.
המדיניות, אם אפשר לקרוא לה כך
כך הסוכנים שלנו פועלים עכשיו, לפי הסדר:
- לעולם לא מגדירים מחדש את האינדקס בשקט. הטוען מפיק מניפסט פערים — התחלה, סיום, אורך ולאיזו מארבע הקטגוריות הוא סבור שהפער שייך. אם רצף הדקות החסרות קצר מ-3 נרות והנפח בנרות שסביבו דליל, מדובר בדקות ללא מסחר. כל רצף ארוך יותר בשעות פעילות נחשב להשבתה עד שיוכח אחרת.
- מושכים מחדש לפני שמפרשים. מחצית מהפערים המוקדמים שלנו נבעו מבאגים בעימוד. משיכה שנייה מנקודת קצה אחרת או מספק נתונים אחר פותרת את בעיית הקטגוריה הרביעית ומצמצמת את הבעיה עוד לפני שצריך להפעיל שיקול דעת.
- חוסמים לפי מיושנות, לא ממלאים פערים. האסטרטגיה מקבלת קלט
data_ageוכלל קשיח: אין כניסות חדשות כשהעסקה האמיתית האחרונה ישנה מ-N נרות, ופוזיציות פתוחות נסגרות בחידוש המסחר רק בהוראת שוק שמחירה כולל הפחתה מפורשת בגין סיכון הפער. עסקאות שלא יכלו להתבצע גרועות מעסקאות שלא בוצעו. - האינדיקטורים מקבלים NaN, לא נתונים בדויים. מחירים שמולאו קדימה לא מגיעים לשכבת התכונות. אם אי אפשר לחשב ATR, הערך שלו אינו מוגדר, והמשמעות היא שאין פוזיציה. כשל ברור עדיף על פי 7 בשקט.
- מדווחים ביצועים מותנים בפערים. בכל בדיקה לאחור שאנחנו מפרסמים מופיעים הרווח וההפסד לצד התוצאה הכללית גם לאחר נטרול עסקאות הסמוכות להשבתה. אם העסקאות האלה הן שמניבות את התוצאה, התוצאה היא תוצר לוואי של הנתונים.
בדיקה מהירה שאפשר להריץ כבר היום: קבצו את הדקות החסרות לרצפים עוקבים, ואז בדקו איזה חלק מהעסקאות בבדיקה לאחור שלכם נפתח או נסגר בתוך 30 דקות מגבול של רצף. אם מדובר בפחות מ-1%, כנראה שהפערים לא משפיעים על הרבה. אם מדובר ב-5% או יותר, עקומת ההון מספרת בחלקה סיפור על השבתת הזירה.
הסימן שלמדתי לסמוך עליו הוא הצורה של החוסרים, לא הכמות שלהם. מערך נתונים עם אלפי חורים מפוזרים בשעות מתות בדרך כלל בסדר. מערך נתונים עם כמה צברים צפופים אומר שמשהו קורס תחת עומס, ושם — תחת עומס — האסטרטגיה שלכם פועלת.
← כל הפוסטים