يُنتج الاختبار الخلفي رقماً، والأرقام مُقنِعة. هذه هي المشكلة برمّتها. عندما تكون المحاكاة خاطئة بشكل خفي، فإنها لا تنهار ولا تُحذّرك. بل تُسلّمك نتيجة أفضل قليلاً من الواقع وتتركك تتصرف بناءً عليها.
محاكاة جني ربح ثابت أمر سهل: فالسعر إما بلغ مستواك أو لم يبلغه. أما محاكاة دخول أو خروج معتمد على مؤشر فهنا تكمن الصعوبة، لأن المحاكاة عندئذ عليها أن تُجيب عن سؤال أشد تعقيداً: ما الذي كان البوت يعرفه فعلاً في تلك اللحظة؟
فيما يلي ثلاث طرق أخطأنا فيها في الإجابة عن هذا السؤال، وما علّمنا إياه كل خطأ منها.
أهم النقاط
- الخطأ الكلاسيكي هو قراءة مؤشر على إطار زمني أعلى قبل أن تُغلق شمعته. مؤشر RSI على إطار 4 ساعات إذا استُشير عند فتح الشمعة يحمل أربع ساعات من حركة السعر لم تحدث بعد.
- الحل حسابي وليس تخمينياً: القيمة القادمة من إطار زمني T لا تصبح قابلة للاستخدام بشكل مشروع إلا بعد انقضاء T، لذا أزِح البحث إلى الوراء بمقدار (إطار المُشغِّل ناقص إطار المحاكاة).
- المحركات الحية تعاني المشكلة نفسها في ثوب مختلف. المؤشر المحسوب على شمعة لا تزال تتشكل يُعيد رسم نفسه، أي أن الشرط قد يكون صحيحاً ثم غير صحيح على الشمعة ذاتها.
- المُحاكي الذي يتجاهل بصمت ما لا يستطيع إعادة تشغيله يكون أكثر تساهلاً من البوت الحي، والفجوة تصب دائماً في مصلحة الاختبار الخلفي.
- التصرف النزيه هو إظهار الفجوة في النتيجة بدل إخفائها. الاختبار الخلفي الذي يُخبرك أين يكذب أثمن من الذي يبدو نظيفاً.
1. خطأ الاستباق الذي يختبئ في استراتيجيات الأُطر الزمنية المتعددة
هذا هو الخطأ الأسهل كتابةً والأصعب ملاحظةً.
لنفترض أن المحاكاة تتقدم شمعة واحدة مدتها 5 دقائق في كل خطوة، وأن الاستراتيجية تتضمن شرطاً على مؤشر RSI بإطار 4 ساعات. عند كل خطوة مدتها 5 دقائق يحتاج المحرك إلى قيمة لذلك المؤشر. التنفيذ الساذج يبحث عن شمعة الـ 4 ساعات التي تحتوي اللحظة الحالية ويقرأ قيمتها.
هذه آلة زمن.
شمعة الـ 4 ساعات التي فُتحت عند 12:00 ليس لها قيمة RSI نهائية حتى تُغلق عند 16:00. فإذا كانت المحاكاة واقفة عند 12:05 وقرأت تلك الشمعة، فهي تقرأ رقماً محسوباً من أربع ساعات من حركة السعر لم تحدث بعد من وجهة نظر 12:05. تبدو الاستراتيجية وكأنها تدخل قبيل حركات لم يكن بإمكانها توقعها، ويبدو منحنى رأس المال ممتازاً.
الحل ليس تخميناً. بل ينبع من تدوين اللحظة التي تصبح فيها القيمة معلومة فعلاً. الشمعة التي تُفتح عند t على إطار زمني قدره triggerMs لا تصبح معلومة إلا عند t + triggerMs. وتتخذ المحاكاة قرارها عند إغلاق الشمعة الحالية، أي عند candleOpenTime + simulationMs. إذن تصبح القيمة قابلة للاستخدام بشكل مشروع تماماً عندما:
t + triggerMs <= candleOpenTime + simulationMs
t <= candleOpenTime - (triggerMs - simulationMs)
وهذا يعني أن البحث الصحيح هو البحث نفسه الذي كان لديك أصلاً، لكن مُطبَّقاً على طابع زمني مُزاح إلى الوراء بمقدار triggerMs - simulationMs.
هناك خاصيتان في هذا التعبير تستحقان الإشارة، لأنهما ما يجعل تطبيقه آمناً في كل مكان:
- شروط الإطار الزمني نفسه تحصل على إزاحة قدرها صفر. المؤشر بإطار 5 دقائق المقروء عند إغلاق شمعة 5 دقائق هو قيمته الخاصة المبنية على الإغلاق، وقد كانت صحيحة دائماً. الأُطر الأعلى فعلاً هي وحدها التي تتحرك.
- الإزاحة مُشتقّة لا مُهيّأة. فهي تأتي من الشمعة نفسها وليس من إعداد يتعيّن على أحدهم تذكّر ضبطه.
الدرس العام هو درس أطبّقه على أي محاكاة لا على التداول وحده: حين لا تكون متأكداً هل لديك انحياز استباق أم لا، دوّن الطابع الزمني الذي يصبح عنده كل مُدخل معلوماً وقارنه بالطابع الزمني الذي تستخدمه عنده. الانحياز هو ما يتبقى حين تتخطى هذه الخطوة وتعتمد على الحدس بدلاً منها، والحدس سيّئ في هذا الأمر لأن قراءة «شمعة الـ 4 ساعات الحالية» تبدو معقولة تماماً.
2. الخطأ نفسه في الكود الحي يرتدي ثوباً مختلفاً
قد تظن أن هذه المشكلة تخص الاختبار الخلفي وحده. ليس الأمر كذلك. المحرك الحي يواجهها أيضاً، وهناك يُسمّى إعادة الرسم.
في أي لحظة، تكون الشمعة الأحدث لا تزال تتشكل. المؤشر المحسوب على نافذة تنتهي عند تلك الشمعة سيتغير مع تغيّر الشمعة. قد يكون الشرط صحيحاً عند 12:01، وخاطئاً عند 12:03، وصحيحاً مجدداً عند 12:04، كل ذلك ضمن الشمعة ذاتها، دون أن يفعل السوق شيئاً غير اعتيادي. فإذا تصرف البوت بناءً على أولها، فقد تصرف بناءً على قيمة لم تعد موجودة.
الوضع الافتراضي لدينا هو إسقاط الشمعة التي لا تزال تتشكل والحساب على الشموع المؤكدة فقط، حتى لا تُعيد الإشارة رسم نفسها أبداً. وهذا أيضاً ما يجعل الحي والاختبار الخلفي متوافقين، لأن المحاكاة بحكم بنائها تعمل على شموع مُغلقة.
لكن «عدم إعادة الرسم أبداً» ليس مجانياً. فانتظار التأكيد يعني التصرف متأخراً، وفي بعض الاستراتيجيات يكلّف هذا التأخر أكثر مما يتجنبه من ضوضاء. لذا يُتاح هذا كخيار لكل شرط على حدة: التقييم على الشمعة المؤكدة، أو التقييم بشكل حي مع تقبّل أن القيمة قد تتحرك من تحتك.
نقطة التصميم هي الجزء الذي أتمسك به: حين يكون التنازل تنازلاً حقيقياً، فلا تدفنه في وضع افتراضي وتتظاهر بأنه حقيقة مطلقة. اجعل الخيار الآمن هو الافتراضي، ثم اترك المستخدم يختار الآخر عن دراية. أما ما يجب ألا تفعله فهو ترك الوضعين يختلفان بين الحي والاختبار الخلفي، لأن المستخدم عندئذ يقارن بين بوتين مختلفين.
3. ما لا يستطيع المُحاكي إعادة تشغيله أخطر مما يخطئ فيه
الأولان خطأان في الصحة. أما هذا فمشكلة تصميم، وهو في رأيي الأكثر بخساً لحقه من التقدير.
بعض الشروط لا يمكن إعادة تشغيلها. إشارة webhook خارجية هي أوضح مثال: لا يوجد سجل تاريخي لما كان سيرسله نظام خارجي عند 14:20 في يوم ما من مارس الماضي. المحاكاة ببساطة لا تستطيع معرفة ذلك.
فماذا ينبغي أن تفعل إذن؟ الجواب المُغري هو تجاهل ذلك الشرط وتقييم البقية. هذا ما يجعل النتيجة تبدو مكتملة. وهو أيضاً موضع الضرر، والآلية تستحق الشرح صراحةً:
إذا كانت الاستراتيجية تتطلب A و B، ولم يكن بالإمكان محاكاة B، فإن تجاهل B يترك A يقرر وحده. البوت المُحاكى يدخل في ظروف كان البوت الحي سيبقى فيها خارجاً. المحاكاة أكثر تساهلاً بشكل منهجي من الشيء الحقيقي.
لاحظ الاتجاه. الخطأ ليس عشوائياً. إسقاط شرط ضمن منطق «و» لا يمكن أن يُنتج إلا مزيداً من الصفقات مقارنةً بالواقع، لا أقل، فيشير الانحياز إلى نتيجة أجمل مظهراً في كل مرة. هذا اللاتماثل هو ما يجعله خطيراً: الضوضاء العشوائية تتلاشى بالمعدل على مدى عمليات تشغيل كثيرة، أما الانحياز أحادي الاتجاه فلا يتلاشى.
اخترنا الاستمرار في تشغيل المحاكاة بدلاً من رفضها، لأن نتيجة جزئية بفجوة معلومة تظل مفيدة. لكن يجب عندئذ أن تكون الفجوة ظاهرة في المُخرَج لا مدفونة في سجل. تحمل النتيجة تحذيراً صريحاً، بلغة المستخدم نفسها، باسم يقول ما حدث: «لم تتم محاكاة بعض شروط الدخول». وهناك تحذير مماثل للخروج.
هذا القرار هو الجزء الذي سأدافع عنه بأشد ما يكون في هذا النظام. المُحاكي الذي لا يستطيع أن يفشل بصوت عالٍ سيفشل بصمت، والفشل الصامت في شيء يُنتج أرقاماً هو أسوأ أنماط الفشل على الإطلاق، لأن الناس يتصرفون بناءً على الأرقام ولا يتصرفون بناءً على آثار الأخطاء البرمجية.
ما الذي نأخذه من هذا
إذا كنت تبني أي شيء يُعيد تشغيل التاريخ لإنتاج رقم:
- كل مُدخل يحتاج إلى طابع زمني «معلوم عند». إذا لم تستطع تحديده، فلن تستطيع استبعاد انحياز الاستباق.
- افحص اتجاه أخطائك لا حجمها فحسب. الخطأ الذي يشير دائماً في الاتجاه نفسه يتراكم؛ والذي يشير عشوائياً يتلاشى بالمعدل. التفاؤل أحادي الاتجاه هو بصمة انحياز الاستباق.
- اجعل المُحاكي والنظام الحي يتشاركان منطق القرار حيثما استطعت. تنفيذان لـ «هل هذا الشرط صحيح» سينحرفان عن بعضهما، وستكتشف ذلك من مستخدم لا من اختبار.
- أبلِغ عمّا لم تستطع فعله. مصداقية النتيجة تأتي من حدودها المعلنة لا من نظافتها.
الخلاصة غير المريحة هي أن الاختبار الخلفي ادعاء بشأن واقع مضاد، والوقائع المضادة يسهل الخطأ فيها بهدوء. هدف الهندسة ليس مُحاكياً لا يكذب أبداً. بل هو مُحاكٍ يُخبرك أين يكذب.
