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

في أحد الصباحات رفضت إحدى حاوياتي (Container) أن تبقى قائمة. كانت تُقلِع، ثم تموت، فيعيد المنسّق (Orchestrator) تشغيلها؛ تُقلِع، تموت، ثم تعاد مرة أخرى. حلقة تعطّل (Crash-Loop) أنيقة صغيرة، كل بضع ثوانٍ، تلوّن السجل بالأحمر من أعلاه إلى أسفله. كانت غريزتي الأولى هي الطبيعية: شيء ما معطّل، أوقفه. لكن حين قرأت سطر السجل الذي كانت تموت عنده، هدأت معدتي بدل أن تنقبض. كانت العقدة (Node) ترفض البدء لأن إحدى سلاسل الأسعار على القرص كانت تحمل صفوفاً أقل مما كانت عليه آخر مرة اعتمدتُها فيها. لم تكن معطّلة، بل كانت تعمل. حلقة التعطّل كانت صوت نظام يرفض التداول على بيانات لا يثق بها، وقد أنقذتني للتو من تحجيم مركز حقيقي بالاستناد إلى تاريخ فقد عقداً كاملاً بصمت بين عشية وضحاها.
هذا المقال عن ذلك الموقف الذي لا يكتب عنه أحد تقريباً، لأنه غير برّاق ومُحرِج بعض الشيء أن تعترف بأنك بحاجة إليه: يجب أن ترفض عقدة التداول الإقلاع على بيانات فاسدة. الإخفاق المُغلَق (Fail Closed)، لا الإخفاق المفتوح (Fail Open). والجزء المخالف للحدس هنا أن التعطّل الظاهر هو النتيجة الجيدة، وسأروي القصة من الجانبين، بما في ذلك المرة التي كانت فيها بوابتي أنا شديدة الحماس فحبستني خارج سوق كان ينبغي أن أكون فيه.
لماذا الفشل الصامت هو الذي يُنهي الحسابات
تقوم العقدة الفعلية (Live) بكمّ مخيف من العمل في ثوانيها القليلة الأولى: تُقلِع، وتقرأ تاريخها من القرص، وتُسخّن مؤشراتها، وتبدأ في وضع الأوامر، كل ذلك قبل أن يراقبها إنسان. وأياً كان ما قالته البيانات في تلك اللحظة، يصبح هو الحقيقة المرجعية (Ground Truth) التي تُبنى عليها كل قرارات الاستراتيجية طوال تلك الجلسة. فالفشل الذي يخيفني ليس الفشل الصاخب: فتغذية بيانات تُطلق استثناءً (Exception) هي هدية، إذ تموت العملية ويصلك التنبيه فتُصلحها. أما الفشل الذي يُنهي الحسابات فهو الصامت.
تخيّل الطرق التي يمكن أن يسلّمك بها تحديث بيانات روتيني بيانات أسوأ دون أن يُطلق خطأً واحداً. فمزوّد بيانات يقيّد سجل الفئة المجانية، فيصبح ملفك الذي كان يبدأ من عام 2003 يبدأ الآن من عام 2011، بالتنسيق نفسه، وملف Parquet صالح، وعقد كامل من نظام السوق (Regime) اختفى. أو تحميل يتوقف في منتصفه فلا يصل الأسبوع الأخير، فيُقرأ الملف بلا مشكلة ويكون فقط ناقصاً أحدث الشموع التي تعتمد عليها مؤشراتك أكثر من غيرها. أو مهمة تكتب فوق ملف مهمة أخرى في المسار نفسه، بمحتوى معقول الشكل لكن بمنشأ (Provenance) مختلف، وربما باصطلاح تسوية مختلف لا تستطيع تمييزه بالعين المجردة. أو نص تنظيف (Script) يُعيد تسمية سلسلة تعتمد عليها استراتيجية فعلية فتختفي.
لا شيء من هذا يُطلق خطأً. نظام الملفات راضٍ، وpandas راضٍ، والاستراتيجية راضية. إنها فقط تتداول على رؤية للعالم أسوأ صراحةً من تلك التي تحقّقت منها، وتفعل ذلك بصمت. هذا هو الخطر كله في جملة واحدة: إخفاقات البيانات التي تفسد بهدوء هي بالضبط تلك التي تنجو من انتباهك، للسبب نفسه الذي يجعل أخطاء اختبار المحاكاة تنحاز نحو التفاؤل. لا أحد يحذف استراتيجية بدأت فجأة تبدو جيدة. ولا أحد يلاحظ تغذية تقلّصت بهدوء.
الإخفاق المفتوح: كيف تُحجّم مركزاً على قيمة NaN
إليك الإعداد الافتراضي (Default) الذي تأتي عليه معظم الأنظمة الهاوية من البداية، عادةً دون أن يقرر أحد ذلك عمداً. تصل بيانات فاسدة، فيفعل الكود الشيء المهذّب: يملأ الفجوة. يتحول سعر مفقود إلى آخر قيمة مُعادة تعبئتها للأمام (Forward-Fill)، أو إلى صفر، أو إلى dropna() يحذف الصف بصمت، أو إلى NaN يمرّ عبر عملية حسابية ويخرج من الطرف الآخر رقماً يبدو وكأنه بيانات. هذا هو الإخفاق المفتوح. يواجه النظام الفساد ويستمر.
أريد أن أكون دقيقاً بشأن سبب كون هذا هو الخيار الخطير، لأنه للوهلة الأولى يبدو متيناً: النظام لم يتعطل، بل تكيّف. لكن انظر إلى ما فعله فعلياً. سعر إغلاق NaN، يتحول افتراضياً إلى صفر، يُغذّي تقدير تقلّب (Volatility) ينهار إلى ما يقارب الصفر، وهذا بدوره يُغذّي مُحجِّم مركز (Position Sizer) يقرأ مخاطرة تقارب الصفر فيطلب أقصى حجم يسمح به الحساب. وسعر قديم مُعاد تعبئته للأمام يجعل سوقاً سريع الحركة يبدو مجمّداً، فتراهن إشارة انعكاس نحو المتوسط (Mean-Reversion) بسعادة ضد حركة وقعت فعلاً. القيمة الافتراضية الصامتة لم تُحيّد البيانات الفاسدة، بل بيّضتها إلى رقم واثق وخاطئ، والأرقام الواثقة الخاطئة هي ما يُحرّك المال الحقيقي.
Figure
Fail open versus fail closed
The choice a data gate makes when bad data arrives.
Fail open
silent garbage
A NaN or stale price defaults to zero or forward-fills, volatility looks tiny, the sizer reads low risk, and a maximum position goes on. Uptime kept, loss unexplained.
Fail closed
loud halt
The gate detects the regression, logs critical, alerts the operator, and exits before trading. A visible outage, and no position taken on data you cannot trust.
An outage you can see beats a position you cannot explain.
هذه هي المقايضة (Trade-off) التي يفرضها التصميمان، وهي مقايضة صارخة. الإخفاق المفتوح يشتري لك وقت تشغيل مستمراً ويدفع ثمنه مركزاً لا تستطيع تفسيره. أما الإخفاق المُغلَق فينفق تعطّلاً ظاهراً ويشتري لك الضمان بأنك لن تتداول أبداً على بيانات تالفة. وحين تكون الخسارة المحتملة غير متكافئة، وهي هنا غير متكافئة بوحشية، فإن تعطّلاً تراه يتفوق على مركز سيئ لا تراه. وأنا أفضّل أن أحدّق في حاوية ميتة لساعة كاملة على أن أكتشف عند التسوية أنني حجّمت صفقة استناداً إلى قيمة افتراضية كانت تتنكّر في زي سعر.
البوابة، في جملة واحدة
لذا وضعتُ بوابة أمام الإقلاع. فقبل أن يُستعلَم من الوسيط، وقبل أن تُبنى عقدة التداول أصلاً، يتحقق المُشغِّل (Runner) من البيانات على القرص مقابل سجل لما كانت عليه البيانات المعروفة الجودة آخر مرة اعتمدتها. فإن كانت البيانات قد تراجعت، أي صارت أصغر، أو أقصر، أو استُبدلت بمصدر مختلف، فالعقدة لا تساوم. تسجّل خطأً حرجاً (Critical)، وتُطلق تنبيهاً إلى قناة المناوبة (On-Call) الخاصة بي، وتخرج دون أن تلمس الحساب. أما إن كانت البيانات نظيفة، فتُقلِع وتتداول.
Figure
The boot-time data-quality gate
The gate sits before the broker is ever contacted. Growth is always allowed; a regression halts the boot.
- 1
Data feed and files on disk
the input, checked before any broker query
- 2
Quality gate: has known-good data regressed?
smaller, shorter, swapped, missing, or all-NaN
- 3
Clean or grown: boot, warm up, trade
growth and additions always take the clean path
- 4
Regressed: halt, log critical, alert on-call, exit
do not trade; refuse to proceed on garbage
A crash-loop on bad data is the wall holding, not the system failing. But make it fail-closed-and-loud, never fail-closed-and-silent.
وخيار التصميم الوحيد الذي يجعل هذا قابلاً للعيش معه، بدل أن يكون فخّاً يُطلق عند كل نجاح، هو اللاتناظر (Asymmetry): النمو مقبول، والانكماش هو الإشارة. فزيادة عدد الصفوف، وتاريخ بداية أبكر، ونهاية أحدث، وسلسلة جديدة تماماً، كلها تمرّ مباشرة دون اعتراض. لا تُطلق البوابة إلا حين تصبح سلسلة كنت أثق بها بالفعل أصغر، أو أقصر، أو تُستبدَل بهدوء. وهذا متعمّد، لأنك بالكاد تكسب بيانات جيدة عن طريق الصدفة، لكنك قد تخسرها بعشرات الطرق دون أن تنتبه. وبوابة تحجب عند كل تغيير كانت ستُطلق عند انتصاراتك أيضاً، وبوابة تُطلق عند الانتصارات هي بوابة يتعلم المُشغِّلون تعطيلها. أما البصمة الدقيقة التي أُسجّلها لكل ملف، وما يُعدّ تراجعاً بالضبط، فهي الآلية الموجودة في الكتاب؛ أما المبدأ فهو الجزء الذي يمكنك تطبيقه اليوم.
الوجه الآخر: حين حبستني بوابتي خارج سوق بحلقة تعطّل
والآن النصف الصادق، لأن الإخفاق المُغلَق موقف يمكن أن تتشدد فيه أكثر مما ينبغي. حلقة التعطّل الأولى كانت النظام يعمل كما ينبغي. أما حلقة لاحقة فكانت النظام يتصرف بغباء، والذنب ذنبي.
كنت قد أعددتُ البوابة لتكون صارمة، والأهم من ذلك، أن تكون صامتة حيال فئة من المشكلات كان ينبغي أن تكتفي بالتحذير منها فقط. فقد تعرّض مصدر بيانات كنت أعتمد عليه لانقطاع قصير، وجاء الملف الذي كتبه أرقّ من المعتاد، لا لأن تاريخاً قد ضاع، بل لأن سحب تلك الصبيحة كان ناقصاً فعلياً وكان سيصبح سليماً بعد ساعة. فعلت البوابة ما طلبته منها بالضبط: رأت انكماشاً، فرفضت الإقلاع، ودخلت الحاوية في حلقة تعطّل. لا بأس حتى هذه النقطة. لكن المشكلة كانت أن التنبيه لم يُطلَق كما ظننت، فجلست العقدة تموت كل بضع ثوانٍ ودون أن يصلني شيء. اكتشفت الأمر لأن استراتيجية كان يُفترض أن تكون في السوق لم تكن فيه ببساطة، فذهبت أبحث. لقد تدهور الإخفاق المُغلَق إلى إخفاق صامت (Fail Silent)، وهذا فخ قائم بذاته: توقّف لا يستطيع أحد رؤيته من الخارج لا يمكن تمييزه عن تجمّد.
لم يكن الحل إضعاف البوابة. فإضعافها كان سيعيد تفعيل الخطأ نفسه الذي وُجدت البوابة أصلاً للإمساك به. كان الحل من جزأين. أولاً، أن أفصل بين المهمتين اللتين كنت قد خلطت بينهما: بوابة جودة حاجبة (Blocking) تُوقِف عند انكماش حقيقي في تاريخ معروف الجودة، وفحص نضارة أليَن، استشاري يحذّر حين تكون البيانات متأخرة قليلاً فقط أو حين تبدو مهمة مجدولة (Cron) متأخرة، ولا يُوقِف شيئاً. فخلط المهمتين يمنحك بوابة مفرطة الحساسية يتعذّر التعايش معها. ثانياً، وهذا هو الدرس الحقيقي: الإخفاق المُغلَق نعم، لكن الإخفاق الصامت أبداً. يجب أن يكون التوقّف صاخباً في خروجه بقدر ما هو حازم. الرسالة نفسها من سطر واحد، على ثلاثة مستويات صوت: سطر في السجل، وتنبيه إلى القناة التي أقرأها فعلاً، وخروج (Exit) صريح بقيمة غير صفرية يعامله المراقب (Watchdog) كفشل حقيقي. فإن أوقفت البوابة العقدة، أعرف خلال ثوانٍ أي ملف انكمش، وبكم، وما العلاج، دون أن أفتح سجلاً واحداً.
القاعدة القابلة للنقل
إن احتفظت بشيء واحد من هذا المقال، فلتكن صورة القرار نفسها، لا تفاصيل تطبيقي له.
عند سوء البيانات، ارفض التداول ونبّه بصوت عالٍ. لا تمضِ أبداً بقيمة افتراضية تبدو وكأنها بيانات.
هذا كل ما في الأمر. حين تكون قيمة مفقودة، أو فاسدة، أو قديمة، أو NaN بالكامل، يكون أمامك خياران صادقان لا ثالث لهما، والاستعاضة الهادئة عنها برقم معقول ليست أحدهما. فإما أن تكون البيانات جيدة بما يكفي للعمل عليها، أو لا تكون، فتتوقف وتصرخ. أما الخطوة المحظورة، تلك التي تجدها في كل نظام ساذج تقريباً، فهي المسار الثالث: أن تملأ الفجوة بصفر، أو بتعبئة أمامية (Forward-Fill)، أو بـfillna، أو بـdropna، أو بأي شيء يحوّل "لا أعرف" إلى رقم واثق سيعامله المُحجِّم بوصفه حقيقة. وقيمة NaN تصل إلى مُحجِّم المركز لديك ليست خللاً صغيراً، بل هي الآلية التي يُحجِّم بها النظام صفقة حقيقية استناداً إلى لا شيء على الإطلاق.
والنتيجة التشغيلية المترتبة على ذلك، والتي تعلّمتها بالطريقة المُكلفة: الجزء الصاخب ليس اختيارياً. فبوابة تتوقف بصمت تُهدر معظم قيمتها، لأن توقفاً لا تراه يتحول إلى مركز لا تستطيع تفسيره في اللحظة التي يلجأ فيها أحد إلى مفتاح التجاوز (Override) "لمجرد إعادة التداول هذا الصباح." اجعل التعطّل ملحوظاً من خارج الشيء الذي يحرسه، وإلا تكون قد بنيت إخفاقاً صامتاً وأسميته إخفاقاً مُغلَقاً.
أين يكمن الباقي
لم أُطلعك عمداً على مخطط (Schema) البوابة، ولا الحقول الدقيقة التي أُسجّل بصمتها، ولا الفرق (Diff) الذي يحدد ما يُعدّ تراجعاً، ولا المسار السريع وقت الإقلاع الذي يجعل هذا رخيصاً بما يكفي ليعمل عند كل تشغيل. تلك الآلية، أي البيان (Manifest) الذي يحفظ البيانات المعروفة الجودة، وبصمة المحتوى (Content Hash) التي تكشف استبدالاً صامتاً بنفس الحجم، وانضباط التجاوز الذي يتطلب خطوتين متعمدتين لإعادة ضبط خط الأساس عمداً، ودليل التشغيل (Runbook) لما تفعله فعلياً في الثالثة فجراً حين تدخل العقدة في حلقة تعطّل بسبب مشكلة بيانات، كل ذلك في الجزء الثالث من كتاب Building a Production Quant Trading System. وهو نسخة رقمية حيّة على Leanpub، ونسخة ورقية مطبوعة على Amazon. وثمة إطار عمل (Framework) مرافق، منقّح ومُرخَّص بموجب Apache-2.0، يمكنك أن ترى فيه شكل البوابة في شيفرة حقيقية بدل النثر، موجود على GitHub.
احمل جملة واحدة معك من هنا: حلقة التعطّل على بيانات فاسدة ليست نظامك يفشل، بل هي نظامك يرفض أن يكذب عليك. ابنِه بحيث يرفض بصوت عالٍ.
هذا مقال هندسي، وليس نصيحة استثمارية، ولا يحتوي على أي استراتيجية قابلة للتداول. جميع الأرقام توضيحية ومُنقّحة، وقصص الحرب فيه عن الأخطاء البرمجية، لا عن الأرباح.
Figure
The boot-time data-quality gate
The gate sits before the broker is ever contacted. Growth is always allowed; a regression halts the boot.
- 1
Data feed and files on disk
the input, checked before any broker query
- 2
Quality gate: has known-good data regressed?
smaller, shorter, swapped, missing, or all-NaN
- 3
Clean or grown: boot, warm up, trade
growth and additions always take the clean path
- 4
Regressed: halt, log critical, alert on-call, exit
do not trade; refuse to proceed on garbage
A crash-loop on bad data is the wall holding, not the system failing. But make it fail-closed-and-loud, never fail-closed-and-silent.
Figure
Fail open versus fail closed
The choice a data gate makes when bad data arrives.
Fail open
silent garbage
A NaN or stale price defaults to zero or forward-fills, volatility looks tiny, the sizer reads low risk, and a maximum position goes on. Uptime kept, loss unexplained.
Fail closed
loud halt
The gate detects the regression, logs critical, alerts the operator, and exits before trading. A visible outage, and no position taken on data you cannot trust.
An outage you can see beats a position you cannot explain.
قراءات إضافية
- الشك قبل الاحتفال: قراءة في كتاب "بناء بيئة برمجية للتداول الكمي الإنتاجي"
مقدمة لكتابي "بناء بيئة برمجية للتداول الكمي الإنتاجي"، وهو دليل عملي لنسبة الـ 90% غير البراقة من التداول المنهجي: لماذا كتبته، وماذا تتوقع، وماذا لا تتوقع.
مقالات ذات صلة
الختم عند الفتح أم عند الإغلاق: طابع زمني واحد يُسرّب شمعة كاملة من التحيز التطلعي
تحمل كل شمعة طابعاً زمنياً واحداً، لكن ذلك الطابع غامض في جوهره: هل الشمعة الموسومة بالساعة 09:00 هي فتح الفترة القادمة أم إغلاق الفترة الماضية؟ يختلف مزوّدو البيانات في الإجابة، وقراءة الطرف الخاطئ تصنع تحيزاً تطلعياً نظيفاً بمقدار شمعة كاملة يمر دون أن يلحظه أحد في المراجعة. هنا قصة الحرب، وسبب كون هذا التسرب غير مرئي، والقاعدة التي أفرضها الآن عند إدخال كل تغذية بيانات.
الأرباح الموزعة التي احتسبتها مرتين: كيف يصنع الخلط بين الإغلاق المعدَّل وغير المعدَّل فرصة وهمية في اختبار المحاكاة
سلسلة العائد الإجمالي وسلسلة عائد السعر تبدوان متطابقتين تماماً على أي رسم بياني، بينما تفترقان تحت السطح بنسبة مئوية في السنة. اخلط بينهما، أو احسب الأرباح الموزّعة مرتين، وسينحرف اختبار المحاكاة الطويل الأفق فوق نظيره الصادق بضع نقاط مئوية كل عام، ليتراكم ذلك مع الزمن إلى فرصة وهمية بالكامل. وهذا هو فحص إعادة البناء الذي أُجريه الآن: أبني العائد الإجمالي بنفسي من السعر وسلسلة توزيعات الأرباح، ثم أقارنه بسلسلة المزوّد المعدَّلة.
تحيز النجاة في اختبار المحاكاة: اختبار الرائحة القائم على نسبة الأسهم المشطوبة
تحيز النجاة مصطلح يعرفه كل ممارس للتداول الكمي، لكن قلة منهم يحوّلونه إلى إجراء عملي. إليك الفحص المكوّن من رقم واحد الذي أُجريه قبل أن أثق بأي اختبار محاكاة مقطعي: عدّ الأسهم الميتة كنسبة من كونك الاستثماري على مدى عقد كامل. فإذا كانت هذه النسبة قريبة من الصفر، فبياناتك حذفت الخاسرين بصمت، وكل نتيجة تنتجها منحازة نحو الأعلى.