تخطٍّ إلى المحتوى
الجمعة، 23 أكتوبر 2026 · لندنENع
ريان الأزهري.الاستدامة · الطاقة · الكربون · البيئة المبنيةومنعطفات عابرة إلى الفلسفة أو الدين أو البرمجة، حيثما قاد الفضول
التداول الكمي الإنتاجي

لماذا ينحرف التداول الفعلي عن اختبار المحاكاة: الأسباب البرمجية التي لا يذكرها أحد

حين ينجرف أداء حسابك الحقيقي عن اختبار المحاكاة الذي اعتمدته، فالمتهم نادراً ما يكون نفسياً. إنه خلل برمجي: المقياس نفسه محسوب بطريقتين مختلفتين قليلاً بين شيفرة البحث ومسار التحجيم الفعلي. هنا فاحص التطابق (Parity Oracle) الذي أفرضه الآن بوابةً قبل كل إطلاق، والقاعدة الأعمق خلفه: يجب أن يستدعي التداول الفعلي والبحث الشيفرة نفسها، لا نسختين منها.

ريان الأزهريأخصائي بيئي معتمد · 9 دقيقة قراءة
بطاقة عنوان لمقال اختبار المحاكاة لا يطابق التداول الفعلي، تُظهر منحنى اختبار محاكاة ومنحنى تداول فعلي يتفرعان من نقطة بداية واحدة فوق مقتطف شيفرة يقرأ ddof=1 مقابل ddof=0.

أضعتُ الجزء الأكبر من أسبوع كامل خلف انحراف تبيّن في النهاية أنه لا يتجاوز أربعة أحرف. كان لدى Titan، نظامي متعدد الاستراتيجيات الذي يتداول بأموال حقيقية، شريحة (Sleeve) تتداول تداولاً ورقياً (Paper)، بدأ منحنى رأس مالها ينفصل عن اختبار المحاكاة الذي كنت قد اعتمدته. لا شيء درامي: بضع نقاط أساس هنا، ودخول متأخر بشمعة واحدة هناك، انجراف بطيء وصبور يمكنك في أي يوم بمفرده أن تقنع نفسك بأنك لم تره. وبحلول نهاية الأسبوعين كان المنحنيان قد أصبحا بلا لبس حيوانين مختلفين انطلقا من البوابة نفسها. فتحققتُ من تغذية البيانات. تحققتُ من التنفيذات (Fills). تحققتُ من الساعة. لم تكن العلة في أيٍّ منها. كانت شيفرة البحث تحسب الانحراف المعياري المتحرك بـddof=1 (مُقدِّر العينة)، بينما كان مسار التحجيم الفعلي يحسب الانحراف المعياري نفسه بـddof=0 (مُقدِّر المجتمع). ويختلف الاثنان بمعامل قدره sqrt(n / (n - 1))، أي نحو عُشر نقطة مئوية واحدة على نافذتي. وذلك العُشر من نقطة مئوية، حين طُبِّق عند عتبة دخول بعينها، كان كافياً لينقل نقطة العبور إلى شمعة مختلفة، بتكرار كافٍ لثني المنحنى بأكمله.

إن كتبتَ يوماً عبارة "التداول الفعلي لا يطابق اختبار المحاكاة" في محرك بحث، فالأرجح أنك صادفت الحكمة الجاهزة: التداول الفعلي مختلف لأن مشاعرك تتدخل. وهذا مقبول من متداول تقديري. أما من متداول منهجي فالجواب أسوأ من عديم الفائدة، لأن الآلة لا تشعر بشيء. كانت شريحتي الفعلية مجرد صف بايثون (Python) يستمع إلى الشموع ويُصدر الأوامر؛ لم تشعر بشيء على الإطلاق. فالانحراف لم يكن نفسياً، بل كان حسابياً بحتاً. والسبب في أن الإجابة الجاهزة تلجأ دائماً إلى الحكاية العاطفية هو أن الأسباب الحقيقية غير براقة، برمجية المصدر، وغير مرئية ما لم تبحث عنها بالأداة الصحيحة.

التداول الفعلي والبحث: برنامجان، ولا شيء يفرض تساويهما

إليك الحقيقة البنيوية الكامنة خلف كل تشريح لجثة يبدأ بعبارة "اختبار محاكاتي كان كذبة". حين تُصادق على استراتيجية، فأنت تُصادق على أثر بحثي (Research Artefact): دالة تُحوّل تاريخ الأسعار إلى سلسلة عوائد، تُشغَّل بصيغة متجهة (Vectorised) على لوحة بيانات كاملة داخل دفتر ملاحظات. وحين تُطلقها للتداول الفعلي، فأنت تُشغّل برنامجاً مختلفاً تماماً: صفاً فعلياً يستهلك الشموع واحدة تلو الأخرى من تغذية الوسيط، ويحمل مخزناً مؤقتاً (Buffer)، ويدير فترة الإحماء، ويُصدر الأوامر داخل حلقة أحداث (Event Loop). النية واحدة. لكن الشيفرة مختلفة، وشكل البيانات مختلف، ومسار التحكم مختلف.

لا شيء يُلزم هذين البرنامجين بحساب الشيء نفسه. يبدآن متطابقين تقريباً في كل مرة، لأنك كتبت الصف الفعلي انطلاقاً من الدالة البحثية نفسها. ثم ينجرفان، وينجرفان لأكثر أسباب الهندسة إنسانيةً. تحصل شيفرة البحث على تسريع بصيغة متجهة. وتحصل شيفرة التداول الفعلي على تقريب "بسيط" كي تتلاءم مع حلقة الأحداث. ويُغيّر أحدهم نافذة استرجاع (Lookback) في موضع وينسى الموضع الآخر. ويختلف الإعداد الافتراضي لمكتبة برمجية بين موضعي الاستدعاء. لا شيء من هذا يظهر في اختبار المحاكاة، لأن اختبار المحاكاة لا يُشغّل الصف الفعلي أبداً. بل يظهر كمنحنى أرباح وخسائر لا يطابق المنحنى الذي اعتمدته، وبحلول اللحظة التي تلمح فيها العين البشرية الفجوة، يكون الانحراف قد أنفق مالاً حقيقياً لأسابيع.

Chart

The gap that accumulates, never breaks

A validated backtest and a live account leaving the same start and drifting apart, with no single sharp break. The cause is code-level: the same quantity computed two subtly different ways. Illustrative and sanitised.

012345671.7 backtest (validated)1.4 live
Source: Building a Production Quant Trading System (Titan), illustrative

هذا هو الفخ في صورة واحدة: منحنيان من بداية واحدة، يتباعدان ببطء شديد بحيث لا يُطلق يوم واحد بمفرده أي إنذار. فالانحراف ليس نظاماً سوقياً (Market Regime)، بل خلل برمجي، وذلك الخلل كمية واحدة ترتدي ثوبين.

الخلل: كمية واحدة في ثوبين

بمجرد أن تقبل أن التداول الفعلي والبحث برنامجان، يصبح لنمط الفشل اسم: المقياس نفسه، محسوباً بطريقتين مختلفتين قليلاً. وهو أبداً ليس فارقاً غريباً أو نادراً، بل فارق مملّ، يفرضه إعداد افتراضي لمكتبة برمجية أو اصطلاح طارئ، وهو صغير بما يكفي لتتفق الإشارة في اتجاهها 99.9 في المائة من الوقت. لكن "تتفق في الغالب" ليست "تتفق دائماً"، والموضع الوحيد الذي تختلف فيه النسختان هو بالضبط الموضع الذي يهم: عند عتبة بعينها، حيث تعبر الإشارة على شمعة مختلفة وتتفرع السلسلة التالية بأكملها.

Figure

The same metric, wearing four costumes

The three ways live quietly diverges from research, and the oracle that catches all of them.

ddof default

sample vs population

One path computes standard deviation with ddof=1, the other with ddof=0. They differ by a factor of sqrt(n/(n-1)), enough to drift a sizing decision.

Warm-up peek

window not yet full

One path emits a value before its rolling window has filled; the other waits. The first bars disagree, and the difference compounds.

Repaint

provisional vs settled

A value keeps revising after the bar closes. Research reads the settled number; live acts on the provisional one.

The parity oracle

feed both, assert equal

One synthetic input feeds both the research reference and the live path; an equality check in CI flags any mismatch, byte for byte.

Live and research must call the same code, not two copies that drift apart.

Source: Building a Production Quant Trading System (Titan)

كان فخ ddof أعلى الأثواب صوتاً، لكن الخزانة عميقة، وقد أطلقتُ معظم ما فيها إلى الإنتاج بنفسي:

  • الإعداد الافتراضي للمُقدِّر. الدالة numpy.std() تحسب انحراف المجتمع (ddof=0)؛ والدالة pandas.rolling().std() تحسب انحراف العينة (ddof=1). موضعا استدعاء، وإعدادان افتراضيان، ودرجة z خاطئة بصمت. ويكمن الفخ نفسه في اصطلاحات النسب المئوية (Percentile)، وفي كيفية حسم التعادل بين الوسيط والمتوسط، وفي طريقة استيفاء كل مكتبة لقيمة الكميم (Quantile).
  • نافذة الإحماء. كانت هذه النصف الثاني من صيدي الذي استغرق أسبوعاً كاملاً. كان مقياس البحث، إذ يُشغَّل على اللوحة كاملةً، يملك بهدوء كامل نافذة الاسترجاع متاحةً عند كل شمعة يُبلغ عنها. أما الصف الفعلي، وهو يملأ مخزنه المؤقت شمعةً شمعة، فكان يُصدر قيمة قبل أن تمتلئ النافذة فعلياً، فيحسب "المقياس نفسه" على عينة أقصر تختلس النظر خلال المدة الأولى بعد كل إعادة تشغيل. مسار واحد أخذ إحماءه كاملاً؛ والآخر اختلس النظر متجاوزاً حدوده. الصيغة واحدة، لكن كمية التاريخ التي تُغذّيها مختلفة.
  • إعادة الرسم (Repaint). قيمة تستمر في التغيّر بعد إغلاق الشمعة. مُنعِّم يُراجع قراءة الأمس حين يصل اليوم، إشارة تبدو مستقرة في اختبار المحاكاة (حيث السلسلة مُجمَّدة) لكنها تتأرجح في التداول الفعلي حتى تُثبّتها شموع لاحقة. يرى البحث الرقم النهائي المستقر. ويتصرف التداول الفعلي بناءً على الرقم المؤقت.

لاحظ الخيط المشترك. فليس شيء من هذا فارقاً فيما تفعله الاستراتيجية، بل كل واحد منها فارق في كيفية حساب رقم واحد بين نسختين من المنطق نفسه. ولا يمكنك أن تمسك بأيٍّ منها بإعادة قراءة الشيفرة، لأن النسختين تبدوان معقولتين تماماً كلٌّ بمفردها. سطر numpy صحيح. وسطر pandas صحيح. وهما خاطئان فقط بالنسبة إلى بعضهما بعضاً، ولن يكشف أي قدر من التحديق في ملف واحد خلافاً لا وجود له إلا عبر ملفين.

النسخة القابلة للنقل: فاحص تطابق في التكامل المستمر (CI)

إليك التشخيص الذي أريدك أن تحتفظ به، الشيء الذي أنهى صيدي الذي استغرق أسبوعاً كاملاً والذي يعمل الآن على كل استراتيجية قبل أن يُسمح لها بالدمج (Merge). سمِّه فاحص تطابق (Parity Oracle). الشكل واحد دائماً، وهو رخيص بما يكفي ليكون جزءاً من مسار الدمج.

ابنِ تاريخاً اصطناعياً حتمياً: مسيرة عشوائية (Random Walk) ببذرة ثابتة، بلا مسار تغذية بيانات، بلا ملفات Parquet، سريعة داخل التكامل المستمر. أطعم ذلك المدخل نفسه لكلٍّ من المرجع البحثي ومسار الإشارة الفعلي. ثم أكّد أن الاثنين يُعيدان الرقم نفسه، مطابقةً تامة على مستوى البِت (Bit-Exact) حيث تكون الرياضيات متطابقة فعلاً، وضمن هامش تسامح مُعلَن ومُبرَّر فقط حيث يختلف التطبيقان اختلافاً مشروعاً.

def test_live_matches_research_reference():
    closes = build_synthetic_universe(n=500, seed=42)  # حتمي، بلا إدخال/إخراج

    # الفاحص (Oracle): دالة البحث التي أنتجت المنحنى المُعتمَد.
    reference = simulate_strategy(closes, ref_cfg).iloc[-1]

    # الإنتاج: الصف الفعلي، متجاوزاً حلقة الأحداث، والمخزن المؤقت مُحمَّل مسبقاً.
    live_targets = make_live_strategy(closes, live_cfg)._compute_targets_now()

    # المدخل نفسه، الرقم نفسه، وإلا فهو خلل:
    assert live_targets.signal == reference["signal"]           # مطابقة تامة على مستوى البِت
    assert live_targets.z_score == pytest.approx(               # فخ ddof
        reference["z_score"], rel=1e-12)                        # يموت هنا

قراران تصميميان يجعلان هذا الفاحص أميناً، وهما بيت القصيد كله.

يجب أن يكون الفاحص مستقلاً. أكثر طريقة مُغرية لكتابة فحص تطابق هي أن تستدعي الصف الفعلي مرتين وتتحقق من أنه يتفق مع نفسه. هذا الفحص ينجح إلى الأبد ولا يُثبت شيئاً، لأن خلل التطابق لا يمكن أن يعيش إلا داخل الصف الفعلي، وفحص لا يستشير سوى الصف الفعلي عاجز عن رؤيته. إنه يقيس الحتمية (Determinism)، لا التطابق (Parity). يجب أن يكون المرجع هو شيفرة البحث نفسها التي أنتجت سجلك المُعتمَد، أو اشتقاقاً نظيفاً مُعاداً لا يتشارك أي آلية مع المسار الفعلي. وحين يختلف الاثنان، فالاختلاف نفسه هو الاكتشاف. عاش خلل ddof لديّ داخل المُحجِّم الفعلي؛ ولم يكن ليُرصَد إلا بفاحص يُشغّل مُقدِّر البحث نفسه، وقد رصده فعلاً من أول تشغيل.

ثبِّت كل إعداد افتراضي كتابةً. سبب كون الفخ غير مرئي أن ddof لم يُكتب أبداً في أي من الملفين؛ فكل مكتبة استخدمت إعدادها الافتراضي الخاص فحسب. لذا يؤكد الفاحص الرقم بدقة، والأهم أن الاستراتيجية تُثبّت ddof=1 صراحةً عند موضعي الاستدعاء كليهما، بحيث لا يمكن للإعداد الافتراضي أن يختلف مرة أخرى أبداً. وتنسحب القاعدة نفسها على الإحماء: فحص التطابق لا يقارن الصف الأخير فحسب، بل يُعيد تشغيل سلسلة الشموع كاملةً عبر المسارين ويؤكد التطابق عند كل شمعة، بحيث لا يستطيع خلاف لا يوجد إلا أثناء الإحماء أن يختبئ في حالة الاستقرار. فحص بلقطة واحدة كان سيبدو ناجحاً بينما الاختلاس لا يزال قائماً.

شغّل هذا عبر بضع بذور عشوائية، واربطه بالتكامل المستمر، وستعجز أي استراتيجية تنجرف عن البحث عن الوصول إلى فرع الإطلاق (Deploy) ببساطة. يكلّف أجزاء من الثانية. ويصعب بوحشية خداعه، لأنه لا يسأل "هل تبدو هذه الشيفرة صحيحة؟" بل يسأل السؤال الوحيد الذي يهم: "هل يُعيد البرنامجان الرقم نفسه على المدخل نفسه؟"

القاعدة الأعمق: مسار شيفرة واحد، لا نسختان

يمسك فاحص التطابق الانحراف. أما القاعدة التي خلفه فتمنع الانحراف من الأساس، وهي الجملة التي تستحق أن تُوشَم على كل بناء جديد: يجب أن يستدعي التداول الفعلي والبحث الشيفرة نفسها، لا نسختين منها.

والطريقة التي أُغلق بها الفجوة في Titan أن الصف الفعلي لا يُعيد تطبيق الرياضيات الصعبة على الإطلاق. بل يُفوِّض حساب الإشارة إلى دالة البحث نفسها التي استدعاها اختبار المحاكاة. عندئذ يتحقق التطابق ببنية التصميم ذاتها، ووظيفة الفاحص أن يُثبت أن ذلك التفويض لا يُستبدَل يوماً بهدوء بتقريب فعلي "أسرع". فاللحظة التي يُحسِّن فيها أحدهم المسار الفعلي إلى نسخته الخاصة من المُقدِّر، يكون قد أعاد خلق مشكلة البرنامجين من جديد، ويفشل الفحص قبل أن يصل الأمر إلى رأس المال. مقياس موجود في مكان واحد بالضبط لا يمكن أن يختلف مع نفسه. أما مقياس موجود في مكانين فسيختلف يوماً ما، عند الشمعة الوحيدة التي تهم.

هذا هو الانضباط كله مضغوطاً في جملتين. فالانحراف بين اختبار المحاكاة والتداول الفعلي ليس لغزاً في مزاج المتداول، بل هو فارق (Diff) بين دالتين سمحتَ لهما بالانجراف، وتُغلقه برفض أن تكونا دالتين من الأساس.

أما الآلية الكاملة، أي حزام التطابق (Parity Harness) الذي يمر على سلسلة القرار بأكملها من الإشارة إلى المركز الصحيح، وفحوص الشبكة الحدّية (Boundary-Grid) التي تختبر بقسوة العتبات الهشة أمام أخطاء الفاصلة العائمة (Float)، وعقد صف الاستراتيجية الذي يجعل التفويض الأحادي المصدر قابلاً للفرض، وبوابة السببية التي تُثبت أن الشيفرة المشتركة ليست متطابقة فحسب بل مشروعة، فتعيش كلها في النصف المدفوع من كتابي، Building a Production Quant Trading System، كنسخة رقمية حية على Leanpub ونسخة ورقية مطبوعة على Amazon. وهيكل مُنقّح مرخّص بموجب Apache-2.0 لفاحص التطابق وللبنية التي يندرج ضمنها موجود على GitHub. فإن كان منحناك الفعلي ينسحب بهدوء بعيداً عن اختبار محاكاتك، فلا تلجأ إلى التفسير العاطفي. أطعم المدخل نفسه للمسارين وراقب أي رقم يرمش.

هذا مقال هندسي، وليس نصيحة استثمارية، ولا يحتوي على أي استراتيجية قابلة للتداول. جميع الأرقام توضيحية ومُنقّحة، وقصص الحرب فيه عن الأخطاء البرمجية، لا عن الأرباح.

Chart

The gap that accumulates, never breaks

A validated backtest and a live account leaving the same start and drifting apart, with no single sharp break. The cause is code-level: the same quantity computed two subtly different ways. Illustrative and sanitised.

012345671.7 backtest (validated)1.4 live
Source: Building a Production Quant Trading System (Titan), illustrative

Figure

The same metric, wearing four costumes

The three ways live quietly diverges from research, and the oracle that catches all of them.

ddof default

sample vs population

One path computes standard deviation with ddof=1, the other with ddof=0. They differ by a factor of sqrt(n/(n-1)), enough to drift a sizing decision.

Warm-up peek

window not yet full

One path emits a value before its rolling window has filled; the other waits. The first bars disagree, and the difference compounds.

Repaint

provisional vs settled

A value keeps revising after the bar closes. Research reads the settled number; live acts on the provisional one.

The parity oracle

feed both, assert equal

One synthetic input feeds both the research reference and the live path; an equality check in CI flags any mismatch, byte for byte.

Live and research must call the same code, not two copies that drift apart.

Source: Building a Production Quant Trading System (Titan)

قراءات إضافية

ملاحظات في التداول الكمي، الجزء 15 من 25

مقالات ذات صلة

التحقق من البيانات بالإخفاق المُغلَق: لماذا يجب أن ترفض عقدة التداول الإقلاع على بيانات فاسدة

بوابة جودة بيانات تدخل في حلقة تعطّل متكررة أمام بيانات فاسدة أو قديمة أو خالية من القيم فعلياً ليست عطلاً، بل هي تعمل كما ينبغي: فالبديل هو الإخفاق المفتوح، أي التداول على بيانات تالفة بقيم افتراضية صامتة. هنا قصة الحرب من الجانبين، والمرة التي بالغت فيها بوابتي في التشدد فحبستني خارج سوق كان ينبغي أن أكون فيه، والقاعدة الواحدة القابلة للنقل: عند سوء البيانات، ارفض التداول ونبّه بصوت عالٍ، ولا تمضِ أبداً بقيمة افتراضية تتنكّر بمظهر بيانات حقيقية.

· 9 min

الختم عند الفتح أم عند الإغلاق: طابع زمني واحد يُسرّب شمعة كاملة من التحيز التطلعي

تحمل كل شمعة طابعاً زمنياً واحداً، لكن ذلك الطابع غامض في جوهره: هل الشمعة الموسومة بالساعة 09:00 هي فتح الفترة القادمة أم إغلاق الفترة الماضية؟ يختلف مزوّدو البيانات في الإجابة، وقراءة الطرف الخاطئ تصنع تحيزاً تطلعياً نظيفاً بمقدار شمعة كاملة يمر دون أن يلحظه أحد في المراجعة. هنا قصة الحرب، وسبب كون هذا التسرب غير مرئي، والقاعدة التي أفرضها الآن عند إدخال كل تغذية بيانات.

· 9 min

الأرباح الموزعة التي احتسبتها مرتين: كيف يصنع الخلط بين الإغلاق المعدَّل وغير المعدَّل فرصة وهمية في اختبار المحاكاة

سلسلة العائد الإجمالي وسلسلة عائد السعر تبدوان متطابقتين تماماً على أي رسم بياني، بينما تفترقان تحت السطح بنسبة مئوية في السنة. اخلط بينهما، أو احسب الأرباح الموزّعة مرتين، وسينحرف اختبار المحاكاة الطويل الأفق فوق نظيره الصادق بضع نقاط مئوية كل عام، ليتراكم ذلك مع الزمن إلى فرصة وهمية بالكامل. وهذا هو فحص إعادة البناء الذي أُجريه الآن: أبني العائد الإجمالي بنفسي من السعر وسلسلة توزيعات الأرباح، ثم أقارنه بسلسلة المزوّد المعدَّلة.

· 9 min

مقالات إلى بريدك

كتابات جديدة عن سوريا والاستدامة والتمويل، بضع مرات في الشهر.

يمكنك إلغاء الاشتراك في أي وقت. يقرأها أكثر من ٤٢٠٠ متخصّص.