إثبات غياب التحيز الاستباقي في اختبارات الأداء الخلفي متعددة الأطر الزمنية: عبث بالمستقبل لتثبت أن الماضي لا يستطيع رؤيته
جزء من سلسلة "اختبارات الأداء الخلفي بلا أوهام".
انتهت المقالة السابقة بأداة تشخيصية: اختبار إزاحة الشمعة الواحدة (one-bar shift test). حرّك كل عملية تنفيذ شمعة واحدة إلى الأمام؛ فإذا انهار الأداء، فهذا يعني أنك كنت تتداول في الماضي. هذا الاختبار كافٍ لاستراتيجية ذات إطار زمني واحد، حيث يختبئ التحيز الاستباقي في التنفيذ — فقد اتخذت القرار عند الشمعة i ونفّذت الصفقة عند الشمعة i نفسها.
تملك الاستراتيجيات متعددة الأطر الزمنية موضعًا ثانيًا وأكثر هدوءًا للتسرب، لا يصل إليه اختبار الإزاحة. فعندما يستشير قرار على شمعة مدتها دقيقة واحدة اتجاهًا على إطار الساعة الواحدة، لم يعد التسرب في التنفيذ — بل هو في المؤشر نفسه، المحسوب من شمعة إطار زمني أعلى لم تنتهِ من التكوين بعد. هذه المقالة تتناول ذلك التسرب، وكيفية إثبات — آليًا، لا بمجرد تحديق في الكود — أن محرّكك خالٍ منه.
الادعاء الذي نصل إليه في النهاية قوي: اجتاز محرّكنا متعدد الأطر الزمنية 25 من أصل 25 فحص تطابق وتسرب على 86,400 شمعة من بيانات حقيقية لزوج ETHUSDT بإطار دقيقة واحدة (1m)، بما في ذلك فحص إزاحة المستقبل الذي يعبث بكل شمعة مستقبلية ويؤكد أن ولا قرار واحدًا من الماضي يتحرك. ولأن المحرّك مطابق حرفيًا (bit-identical) للنواة المرجعية لدى البوت الحي، فإن الإثبات يصبح خاصية للنظام الحي نفسه، لا للاختبار الخلفي فقط.
حقل الألغام متعدد الأطر الزمنية

تتخذ استراتيجية متعددة الأطر الزمنية (multi-TF) قرارها على ساعة سريعة بينما تستشير ساعات أبطأ. محرّكنا مثال ملموس على ذلك: قاعدة زخم ثلاثية الأطر الزمنية، حيث يعمل اتجاه الإطار الزمني الأعلى (HTF) واتجاه الإطار الزمني المتوسط (MTF) كـبوابة للدخول، بينما يعمل تقاطع الإطار الزمني الأدنى (LTF) كـمُحفّز لتنفيذه. على الساعة الأساسية بإطار 1m، لا يُطلق أمر الدخول إلا عندما يتفق تقاطع HMA/HMA3 على إطار 1m مع اتجاه إطار الساعة (1h) واتجاه إطار الـ15 دقيقة (15m)؛ ويُطلق أمر الخروج عندما ينعكس اتجاه أحد الأطر الزمنية الأعلى ضد الصفقة المفتوحة، أو عندما يعكس تقاطع LTF اتجاهه.
الخطر هنا بنيوي. فكل واحدة من قيم الأطر الزمنية الأعلى يجب الإجابة عنها في كل شمعة 1m — أي 60 مرة في الساعة — ويجب أن تعتمد كل إجابة حصرًا على ما كان بوسع بوت حي معرفته في تلك اللحظة بالذات. فبمجرد أن تلامس قراءة واحدة فقط من بين نحو 180 قراءة لكل شمعة (ثلاثة أطر زمنية، اتجاهات الدخول والخروج، بوابات الفصل) شمعةً لم تُطبع بعد، يكون لديك تحيز استباقي. مساحة الخطأ المحتملة لخطأ إزاحة بمقدار واحد (off-by-one) هائلة، وخلافًا لتسرب التنفيذ، فإن هذا النوع من التسرب لا يُعلن عن نفسه عبر إزاحة في التنفيذ.
الشمعة قيد التكوين لا تملك إغلاقًا نهائيًا بعد
إليك الفخ بالضبط. لنفترض أن الساعة الآن 10:37 وأنت تتخذ قرارًا عند شمعة 1m التي أُغلقت للتو. تحتاج قاعدتك إلى اتجاه إطار الساعة (1h). شمعة الـ1h التي تغطي الفترة 10:00–11:00 لا تزال قيد التكوين — ولن يُعرف إغلاقها النهائي إلا عند الساعة 10:59:59. فماذا تعرف فعليًا عند الساعة 10:37؟ لا تعرف سوى الإغلاق الجاري لتلك الشمعة حتى تلك اللحظة، وهو إغلاق شمعة 10:37. أما الإغلاق النهائي للشمعة فهو في المستقبل، بعد 23 دقيقة.
يقوم الاختبار الخلفي الساذج متعدد الأطر الزمنية بشيء يبدو بريئًا تمامًا: يعيد أخذ عينات (resample) لكامل سلسلة 1m إلى إطار 1h مرة واحدة، سلفًا، ثم يقرأ لكل شمعة 1m قيمة "إغلاق الـ1h". لكن القيمة التي يقرأها لكل شمعة 1m بين الساعة 10:00 و11:00 هي إغلاق الفترة 10:00–11:00 النهائي — وهو رقم لا وجود له، في الزمن الحقيقي، حتى تنتهي الساعة. فكل قرار داخل تلك الساعة يُسلَّم بصمت إلى ما يصل إلى 59 دقيقة من المستقبل. وهذا ليس تسربًا صغيرًا: فإغلاق الإطار الزمني الأعلى يُعد من أقوى المؤشرات الممكنة للتنبؤ بعوائد 1m القريبة التي أنت على وشك تداولها، فتسريبه أشبه بقراءة ورقة الإجابة النموذجية مسبقًا. هذه هي قناة تسرب المؤشرات من التصنيف، لكن بصورة مضخّمة: فالاطلاع المسبق هنا ليس بمقدار شمعة واحدة، بل قد يصل إلى فترة HTF كاملة.
اختبار إزاحة الشمعة الواحدة من المقالة السابقة لا يلتقط هذا الخلل. فبإمكانك إزاحة التنفيذ شمعة واحدة إلى الأمام وستظل سلسلة الـ1h المعاد أخذ عيناتها ملوّثة — فالتسرب يكمن في طريقة بناء المؤشر، لا في توقيت تنفيذ الصفقة.
قاعدة البوت الحي، مُعاد إنتاجها بدقة: دلالات الشمعة المغلقة

القاعدة الصحيحة هي تلك التي يشغّلها البوت الحي فعلًا. في قاعدة الكود لدينا، هي عبارة عن صنف (class) صغير باسم RunningCandleBuffer، نُقل حرفيًا من محاكي التِك (tick simulator) الحي. يقوم هذا الصنف بتدفق الشموع الأساسية إلى شموع ذات فترة ثابتة، ويحسب عند كل شمعة أساسية مؤشر HTF من مصفوفة محددة جدًا:
all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)
اقرأ هذا حرفيًا. self.closes هي الإغلاقات النهائية للشموع التي أُغلقت بالفعل — إذ تُضاف الشمعة فقط عند عبور حدّ فترة جديدة، والقيمة المخزّنة هي إغلاق آخر شمعة أساسية داخل تلك الفترة (candle_buffer.py:39–44). أما الشمعة قيد التكوين فتُسهم برقم واحد بالضبط، self.current_close، وهو الإغلاق الجاري — أي إغلاق أحدث شمعة أساسية، base_close[i]. وهذه كمية معروفة عند الشمعة i بحكم التعريف. أما الإغلاق النهائي للشمعة قيد التكوين فلا يُستخدم إطلاقًا، لأنه غير موجود بعد.
إذن مؤشر HTF عند الساعة 10:37 يرى [..., close(9:00 candle), close(10:37 so far)]. وعندما تُطبع شمعة 10:38، تُحدَّث الخانة الأخيرة لتصبح close(10:38). وعند عبور الساعة 11:00، يصبح close(10:59) هو القيمة النهائية للشمعة المُغلقة حديثًا، وتُفتح خانة تكوين جديدة. لا يلامس أي قرار داخل تلك الساعة الإغلاق النهائي للفترة 10:00–11:00 في أي لحظة. هذه هي دلالات الشمعة المغلقة: الشموع المغلقة تسهم بإغلاقات نهائية، والشمعة قيد التكوين تسهم فقط بإغلاقها الجاري.
محرّكنا السريع (engine_multitf.py) هو إعادة تنفيذ متجهية (vectorized) ومُصرَّفة بواسطة numba لهذه الآلية. فبدلًا من حلقة Python ولائحة متنامية، يحسب المحرّك مسبقًا، لكل شمعة أساسية i، عدد الشموع التي أُغلقت بالكامل (n_closed[i])، ويرتّب نافذة HMA/HMA3 على الصورة [closed candle closes…, base_close[i]] — أي أن الإغلاق الجاري مثبّت في الخانة الأخيرة (engine_multitf.py:168–169). إنها نفس الرياضيات، لكن مُفكَّكة (unrolled) لأجل السرعة عبر ثلاثة أطر زمنية مع بوابات فصل اتجاهية. والعقد صريح: القيمة عند الشمعة i تعتمد فقط على base_close[0..i].
هذا هو الادعاء. وبقية المقالة تتناول كيف نثبته، لأن ادعاءً واردًا في تعليق توثيقي (docstring) لا قيمة له.
التطابق شرط ضروري لكنه غير كافٍ
إن محرّك numba المتجهي الذي يفكّك صنفًا يعمل بأسلوب التدفق (streaming) إلى حلقات صريحة هو تحديدًا المكان الذي تتوالد فيه أخطاء الإزاحة بمقدار واحد (off-by-one). لذا فإن البوابة الأولى هي اختبار تطابق حرفي (bit-for-bit) في مقابل المرجع القانوني، على شريحة بيانات حقيقية — ETHUSDT بإطار 1m، من يناير إلى فبراير 2024، بعدد 86,400 شمعة.
نتحقق من أمرين مستقلين في مقابل مرجعين مستقلين:
- المؤشرات والتقاطعات مقابل
RunningCandleBufferمُشغَّلًا شمعة بشمعة. لكل إطار زمني، نعيد تشغيل الصنف الحي عبر كل الشموع الـ86,400 ونقارن أحداث التقاطع — الشمعة، الاتجاه، الفصل — للحصول على تطابق تام، إضافة إلى قيم HMA/HMA3 ضمن هامش تسامح للفاصلة العائمة (المرجع يستخدمnp.dot، بينما المحرّك يستخدم حلقات صريحة، لذا يختلف ترتيب الجمع بنحو 1e-15). تتطابق التقاطعات بدقة تامة: 408 تقاطعات على HTF (إطار 1h، طول HMA يساوي 21)، و2,792 على MTF (15m/14)، و3,691 على LTF (1m/50). لا يختلف حدث تقاطع واحد لا في الشمعة ولا في الاتجاه. - الصفقات مقابل محاكاة مستقلة بلغة Python خالصة لقواعد التداول، مدفوعة بتقاطعات المرجع نفسها. تعيد هذه المحاكاة إنتاج حلقة الاختبار الخلفي الحي — اتجاهات الدخول تشكّل بوابة للدخول، وانعكاس اتجاه الخروج أو تقاطع LTF معاكس يغلق الصفقة، والتنفيذ عند
open[i+1]، ورسم 0.09% لكامل دورة الصفقة، وإغلاق قسري عند آخر شمعة — من دون أي من آلية numba الخاصة بالمحرّك. ثم نقارن صفقة بصفقة: شمعتا الدخول/الخروج، جهة الصفقة، أسعار الدخول/الخروج، الربح والخسارة (PnL)، أسباب الخروج، وإجمالي زمن النشاط في الصفقة.
عتبات الفصل المستخدمة في الاختبار ليست قيمًا افتراضية بريئة. بل اختيرت خصيصًا لتصيب زاوية صعبة — عتبة خروج على MTF أعلى من عتبة الدخول المقابلة لها — وهو ما يفرض تفعيل مسار "أول تعريف لاتجاه خروج أثناء وجود صفقة مفتوحة"، الذي يعامله المرجع كانعكاس. يجب أن يصمد التطابق في الحالات الحدّية، لا في المسار السهل فقط.
حقلًا بحقل، الصفقات متطابقة: 466 صفقة في الإعداد الثنائي، و211 صفقة في الإعداد الثلاثي، مع تطابق إجمالي الربح والخسارة حتى 1e-12، وتطابق كل حقول الصفقة ضمن هامش التسامح. تنفيذان لا يتشاركان أي كود — محرّك متجهي مُصرَّف، وحلقة Python ساذجة تعمل على تقاطعات تنفيذ ثالث — ينتجان الصفقات ذاتها حتى آخر منزلة عشرية.
هذه نتيجة قوية، لكنها ليست إثباتًا لغياب التحيز الاستباقي. فالتطابق يخبرك أن المحرّك السريع يعيد إنتاج المرجع بأمانة. ولو أن المرجع نفسه كان مسرِّبًا — لو أن RunningCandleBuffer كان يختلس النظر إلى المستقبل — لكان التطابق قد أعاد إنتاج التسرب بأمانة وتجاوز الاختبار بنجاح. فاتفاق التنفيذين يخبرك أنهما متماثلان، لا أنهما سببيّان (causal). ولإثبات السببية تحتاج إلى نوع مختلف من الاختبار، اختبار يسأل المحرّك مباشرة عمّا إذا كان الماضي قادرًا على رؤية المستقبل.
فحص إزاحة المستقبل: الإثبات الحقيقي

تعريف التحيز الاستباقي إجرائي، فاختبره إجرائيًا. التحيز الاستباقي يعني أن قرارًا في الماضي يعتمد على بيانات من المستقبل. والعكس النقيض هو اختبار يمكنك تشغيله فعليًا: إذا غيّرت المستقبل وتحرّك أي قرار من الماضي، فهذا يعني أن الماضي كان يقرأ المستقبل. إذن، غيّر المستقبل — بقسوة — وراقب الماضي.
اختر نقطة قطع j عند 60% من السلسلة (الشمعة رقم 51,840 من أصل 86,400). اعبث بـكل شمعة اعتبارًا من j فصاعدًا: اضرب جميع إغلاقات وفتحات المستقبل في 1.05. أعِد حساب كامل حزمة الإشارات لجميع الأطر الزمنية الثلاثة على البيانات المُعبَث بها. ثم تحقّق أن كل ما يقع قبل j تمامًا مطابق حرفيًا (bitwise) للتشغيل غير المُعبَث به:
j = int(n * 0.6) # bar 51,840
cl2 = cl.copy(); cl2[j:] *= 1.05 # shove the future up 5%
op2 = op.copy(); op2[j:] *= 1.05
base = [precompute_tf_signals(cl, ts, p, L) for (p, L) in tf_params]
pert = [precompute_tf_signals(cl2, ts, p, L) for (p, L) in tf_params]
for s0, s1 in zip(base, pert):
assert eq_nan(s0.hma[:j], s1.hma[:j]) # HMA identical, NaNs included
assert eq_nan(s0.hma3[:j], s1.hma3[:j])
assert np.array_equal(s0.cross[:j], s1.cross[:j]) # every cross event
assert np.array_equal(s0.sep[:j], s1.sep[:j]) # every separation
ليس "قريبًا". وليس "ضمن هامش تسامح". بل np.array_equal، مع اشتراط تطابق قيم NaN مع NaN — يجب أن تكون كل قيمة HMA، وكل قيمة HMA3، وكل علامة تقاطع، وكل قيمة فصل على الشموع الـ51,840 الماضية هي نفس الرقم العشري (float) بالضبط. ثم يُطبَّق التأكيد نفسه على الصفقات: يجب أن تبقى كل صفقة يقع خروجها قبل j بلا تغيير حقلًا بحقل. فإذا كانت دفعة بنسبة 5% للمستقبل تحرّك قيمة HMA واحدة في الماضي عند المنزلة العشرية الثانية عشرة، فهذا يعني أن قرارًا من الماضي استشار المستقبل، ويفشل الفحص عندها.
يجتاز محرّكنا هذا الفحص — على الأطر الزمنية الثلاثة كلها، وفي محاكاتي التداول الثنائية والثلاثية على السواء. فالعبث بـ34,560 شمعة مستقبلية يترك الشموع الـ51,840 الماضية، وكل صفقة أُغلقت بينها، كما هي بالضبط دون أي تغيير. هذا ليس اتفاقًا بين تنفيذين؛ بل هو برهان مباشر على أن حاجز المعلومات عبر الزمن صامد.
اختبار لا يمكن أن يفشل لا يثبت شيئًا
ثمة طريقة لاجتياز الفحص أعلاه لا تثبت شيئًا على الإطلاق: فلو كان العبث بلا أثر (no-op) — لو أن ضرب المستقبل في 1.05 لم يغيّر أي شيء في أي مكان — فإن عبارة "الماضي لم يتغير" تصبح صحيحة بداهةً وعديمة الفائدة تمامًا. علامة صح خضراء على اختبار لا يمكن أن يفشل أسوأ من غياب الاختبار أصلًا، لأنها تصنع ثقة زائفة. لذا يحمل الفحص تأكيدين إضافيين يمنحانه فعّالية حقيقية.
يجب أن يتغيّر المستقبل فعلًا. نتحقق من أن العبث قد غيّر بالفعل تقاطعات في مكان ما ضمن [j, n):
assert not np.array_equal(s0.cross[j:], s1.cross[j:]) # probe has teeth
الآن أصبحت النتيجة ذات معنى: الدفعة نفسها بنسبة 5% التي أعادت كتابة المستقبل، تركت الماضي مطابقًا حرفيًا. فالعبث حقيقي، وينتشر إلى الأمام، ويتوقف تمامًا عند نقطة القطع. تسرب أحادي الاتجاه — أي أن يقرأ الماضي المستقبل — كان سيتسرّب إلى الوراء عبر j؛ لكن هذا لا يحدث.
الحدّ يقع بدقة عند الشمعة الحالية — لا قبلها بشمعة، ولا بعدها بشمعة. ثمة فشل أدق يمكن أن يقع فيه محرّك سببي (causal) لكنه متأخر (stale): يتجاهل الإغلاق الجاري للشمعة الحالية ويستجيب متأخرًا بشمعة (لا تسرب هنا، لكنه تأخر لن يحدث في التداول الحي)، أو محرّك يستجيب مبكرًا بشمعة (وهو تسرب بمقدار شمعة واحدة). لذا نعبث بشمعة واحدة فقط، j (بضربها في 1.02)، ونتحقق من أمرين في آن واحد: أن الماضي [0, j) لم يُمس، وأن hma[j] يستجيب فورًا.
cl3 = cl.copy(); cl3[j] *= 1.02 # nudge exactly one bar
s3 = precompute_tf_signals(cl3, ts, p_ltf, L_ltf)
assert eq_nan(s0.hma[:j], s3.hma[:j]) # nothing before j moves
assert s0.hma[j] != s3.hma[j] # bar j reacts on the same bar
هذا يحدد موضع الحد بدقة تامة. فالإغلاق الجاري للشمعة قيد التكوين يدخل إلى المؤشر عند الشمعة j بـتأخير صفري واستبصار صفري: الشمعة j ترى إغلاقها الخاص فورًا، ولا شمعة سابقة لها تراه إطلاقًا. هذه هي بالضبط حافة السكين التي يُفترض أن تقف عليها دلالات الشمعة المغلقة، ويؤكد الاختبار أن المحرّك يقف عليها فعلًا.
إليك البوابة الكاملة — الفحوص الـ25 كلها التي تفصل بين هذا المحرّك وبين اختبار خلفي مُلفَّق:
| المجموعة | ما يتحقق منه كل فحص | العدد |
|---|---|---|
| المؤشرات والتقاطعات (×3 أطر زمنية) | تطابق تام لأحداث التقاطع مقابل RunningCandleBuffer؛ درجة الفصل عند التقاطعات؛ قيم HMA/HMA3 (بهامش تسامح نسبي rtol 1e-9) |
9 |
| الصفقات (ثنائي + ثلاثي) | عدد الصفقات؛ تطابق حقلًا بحقل؛ إجمالي الربح والخسارة حتى 1e-12؛ زمن النشاط في الصفقة | 8 |
| فحص إزاحة المستقبل (ثنائي + ثلاثي) | الإشارات الماضية بلا تغيير حرفي؛ فعّالية الفحص (المستقبل تغيّر بالفعل)؛ الصفقات قبل j بلا تغيير؛ توطّن أثر العبث بشمعة واحدة |
8 |
| الإجمالي | 25 |
تثبت المجموعتان الأوليان أن المحرّك السريع هو المرجع الحي نفسه. أما المجموعة الثالثة فتثبت أن المرجع سببي (causal). وأنت بحاجة إلى الثلاثة معًا: فمحرّك سريع مسرِّب، ومحرّك مسرِّب لكنه سببي، ومحرّك سببي لكنه متأخر، هي ثلاثة إخفاقات مختلفة، وتستبعد البوابة كلًا منها.
لماذا الفحص مستقل عن الإطار الزمني
تكمن أناقة فحص إزاحة المستقبل في أنه لا يعرف ولا يهتم أين قد يكمن التسرب. فهو لا يذكر إطلاقًا الأطر الزمنية، ولا إعادة أخذ العينات (resampling)، ولا حدود الشموع. كل ما يسأله هو: هل يحرّك العبث بالمستقبل الماضي؟ وهذا بالضبط ما يجعله الأداة المناسبة لتسرب متعدد الأطر الزمنية، وهو ما يفوّته اختبار إزاحة التنفيذ.
تأمّل مباشرة خطأ إعادة أخذ العينات الساذج لكامل السلسلة. فلو كانت سلسلة الـ1h قد بُنيت بإعادة أخذ عينات للسلسلة الكاملة سلفًا، فإن قراءة "إغلاق الـ1h" عند الشمعة j-1000 (الواقعة تمامًا داخل الساعة التي تُغلق بعد j) ستكون هي الإغلاق النهائي لشمعة يعتمد إغلاقها النهائي على شموع عند j وما بعدها. اضرب المستقبل في 1.05 وسيتغيّر ذلك الإغلاق النهائي — فيتغيّر مؤشر HTF عند j-1000، وتتغيّر البوابة عند j-1000، ويتحرك قرار من الماضي. وسيضيء الفحص فورًا على سلسلة HTF، عند شمعة تبعد ألف خطوة قبل نقطة القطع.
سلسلة HTF في محرّكنا لا تتحرك، لأن الشمعة قيد التكوين عند j-1000 تسهم فقط بـbase_close[j-1000] — وهو إغلاق ماضٍ — ولا يُستشار الإغلاق النهائي للشمعة إطلاقًا حتى يُعبَر الحد. الفحص أعمى تجاه الآلية الداخلية، ومع ذلك يلتقط الخطأ، وهذا بالضبط ما تريده من إثبات: فهو يقيّد السلوك (لا قرار في الماضي يعتمد على بيانات من المستقبل) بدلًا من تدقيق التنفيذ (هل فهرسنا إعادة أخذ العينات بشكل صحيح؟). السلوك هو ما يُتداول عليه؛ أما التنفيذ فهو ما تأمل أن يطابقه.
الاختبار الخلفي والتداول الحي يتشاركان حقيقة واحدة

ثمة سبب إضافي يجعل هذا أهم من تدقيق اختبار خلفي عادي. فالمرجع الذي يُثبَت المحرّك في مقابله — RunningCandleBuffer — ليس تجهيزة اختبار (test fixture) كُتبت لتجميل مظهر الاختبار الخلفي. بل هو منطق الشموع الخاص بالبوت الحي نفسه، مأخوذ حرفيًا من محاكي التِك الذي يعمل في بيئة الإنتاج. وقاعدة الشمعة المغلقة التي يتحقق منها الفحص هي القاعدة نفسها التي ينفّذها البوت الحي، شمعة بشمعة.
لذا فإن بوابة التطابق تؤدي وظيفة مزدوجة. فهي تثبت أن المحرّك السريع يطابق المرجع، ولأن المرجع هو النواة الحية نفسها، فهي تثبت أن المحرّك السريع يطابق التداول الحي. وقد حذّرت المقالة السابقة من أن التسرب هو التفسير الأنظف لأي فجوة تطابق بين الاختبار الخلفي والتداول الحي — فالبوت الحي هو المكان الوحيد الذي لا يمكنك فيه آليًا أن تختلس النظر إلى المستقبل، لذا فإن أي اختبار خلفي يفعل ذلك سيتباعد عن الواقع بمجرد أن يبدأ العمل الفعلي. وهنا تُغلق تلك الفجوة بحكم البنية ذاتها: الاختبار الخلفي والبوت يتشاركان مخزن شموع واحدًا، وقاعدة تقاطع واحدة، وتعريفًا واحدًا لما هو "معروف عند الشمعة i". فالرقم الذي يُحسِّنه البحث هو نفسه الرقم الذي يستطيع البوت تداوله فعليًا.
هذا هو صلب الفكرة من إثبات غياب التحيز الاستباقي بدلًا من افتراض غيابه. فالبحث متعدد الأطر الزمنية يُشغّل آلاف الإعدادات؛ ولو كان المحرّك مسرِّبًا، لعثر البحث على الإعداد الذي يستغل التسرب بأقصى قدر من العدوانية، وسلّمك فائزًا مُلفَّقًا — وهو نمط الفشل الذي قاسه التصنيف عند نسبة شارب (Sharpe) تبلغ 15 ناتجة عن ضوضاء خالصة. والفحص هو ما يتيح لك الوثوق بأن الفائز حقيقي قبل أن تربطه برأس مال فعلي.
ما يثبته الفحص وما لا يثبته
الدقة في التعامل مع الاختبار سلاح ذو حدين، فكن دقيقًا بشأن نطاقه. يثبت فحص إزاحة المستقبل خاصية واحدة محددة: أنه، على هذه البيانات، لا توجد إشارة أو قرار صفقة عند الشمعة j أو قبلها يعتمد على أي شمعة بعد j — أي أن حاجز المعلومات عبر الزمن صامد على طول مسار المؤشر والتقاطع والصفقة. هذه بالضبط هي التسرب متعدد الأطر الزمنية الذي سعينا للقضاء عليه، وهي الخاصية التي لا يمكن لمراجعة الكود إثباتها.
وهو لا يثبت أن للاستراتيجية ميزة حقيقية (edge). فمحرّك سببي تمامًا يمكن أن يخسر المال بأمانة، والفحص صامت حيال ذلك — وهذا صحيح تمامًا؛ فإثبات غياب التسرب وإيجاد ميزة حقيقية مهمتان منفصلتان، والخلط بينهما هو بالضبط كيف تُنشَر الاختبارات الخلفية المسرِّبة إلى الإنتاج. كما أنه لا يغطي التحيزات غير الزمنية: كتحيز البقاء (survivorship) في الأداة المتداولة، أو تحيز الاختيار الناتج عن تشغيل الفحص فقط بعد أن بدا المحرّك جيدًا، أو نموذج رسوم متساهل أكثر من اللازم. كذلك لا يثبت بذاته أن تنفيذات التداول الحي تطابق تنفيذات الاختبار الخلفي — فالانزلاق السعري (slippage) وزمن الاستجابة (latency) فجوات حقيقية لا يستطيع الفحص رؤيتها، لأنه يعمل على مسار القرار، لا على منصة التنفيذ. أما ما يُغلق تلك الفجوة فهو حقيقة منفصلة، وهي أن نواة الشموع في المحرّك مطابقة حرفيًا لنواة البوت الحي.
وثمة تحفظ صادق بشأن تصميم الفحص نفسه: فهو يقطع عند نقطة j واحدة (60% من السلسلة). والخاصية التي يتحقق منها متجانسة بالنسبة لـj — لا شيء مميز في الشمعة 51,840 تحديدًا — لذا فإن قطعًا واحدًا هو اختبار عادل لخاصية بنيوية، لكن نسخة أكثر تشككًا كانت ستمسح j عبر السلسلة بأكملها. نحن نعتبر أن قطعًا واحدًا مُختارًا بعناية، إلى جانب فحص التوطّن بشمعة واحدة، كافيان، لأن تسربًا يختبئ من عبث مستقبلي بـ34,560 شمعة لكنه يظهر عند نقطة قطع أخرى سيكون خللًا غريبًا جدًا. الهدف هو معرفة حدود دليلك، لا التظاهر بأن اختبارًا واحدًا هو مُكمِّم شامل (universal quantifier).
الخلاصات
- الاستراتيجيات متعددة الأطر الزمنية تسرّب عبر الشمعة قيد التكوين، لا عبر التنفيذ. يجب أن يعتمد أي قرار داخل فترة HTF غير مكتملة على الإغلاق الجاري لتلك الشمعة فقط (أحدث إغلاق أساسي)، لا على إغلاقها النهائي إطلاقًا. الاختبارات الخلفية التي تعيد أخذ عينات لكامل السلسلة تسلّم كل قرار داخل الفترة إلى ما يصل إلى فترة HTF كاملة من المستقبل.
- اختبار إزاحة الشمعة الواحدة لا يصل إلى هذا التسرب. فهو يلتقط التحيز الاستباقي في التنفيذ؛ أما التسرب متعدد الأطر الزمنية فيكمن في طريقة بناء المؤشر. أنت بحاجة إلى فحص مختلف.
- أعِد إنتاج قاعدة البوت الحي بدقة تامة، ثم أثبتها. أعدنا بناء دلالات الشمعة المغلقة لصنف
RunningCandleBufferالخاص بالبوت كمحرّك متجهي، وحصّناه خلف 25 فحصًا: تقاطعات مطابقة تمامًا للمرجع (408 / 2,792 / 3,691)، وصفقات متطابقة حقلًا بحقل (466 في الوضع الثنائي، 211 في الوضع الثلاثي)، وربح وخسارة متطابق حتى 1e-12. - التطابق شرط ضروري لكنه غير كافٍ. مطابقة المرجع تثبت أنك مماثل له، لا أنك سببي. فمرجع مسرِّب يُعاد إنتاجه بأمانة يظل مسرِّبًا.
- فحص إزاحة المستقبل هو الإثبات الحقيقي. اعبث بكل شمعة عند
jأو بعدها؛ وتحقق أن كل إشارة وصفقة قبلjبلا تغيير حرفي. فإن كان بمقدور المستقبل تحريك الماضي، فلديك تحيز استباقي. - امنح الفحص فعّاليته. تحقّق أن المستقبل قد تغيّر فعلًا (أن العبث ليس بلا أثر) وأن دفعة بشمعة واحدة تستجيب في تلك الشمعة نفسها (لا تسرب، ولا تأخر). فاختبار لا يمكن أن يفشل لا يثبت شيئًا.
- عندما يتشارك الاختبار الخلفي والتداول الحي نواة واحدة، ينتقل الإثبات معهما. لأن المحرّك مطابق حرفيًا لمنطق الشموع الخاص بالبوت الحي، فإن غياب التحيز الاستباقي يصبح خاصية للنظام الحي أيضًا — وتُغلق فجوة التطابق بين الاختبار الخلفي والتداول الحي بحكم البنية ذاتها.
أظهرت المقالة السابقة كيف يُلفّق تسرب من سطر واحد نسبة شارب تبلغ 15. أما هذه المقالة فتُظهر الانضباط المعاكس: ليس "كيف يخدعك التسرب"، بل كيف تثبت، آليًا وعلى بيانات حقيقية، أن محرّكًا محددًا لا يسرّب. اعبث بالمستقبل. فإن لم يتزحزح الماضي، فأنت تتداول في الحاضر.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.