← العودة إلى قائمة المقالات
July 2, 2026
5 دقائق للقراءة

ضريبة الإطار: عندما تكون مكتبة الاختبار الرجعي لديك أبطأ من حلقة pandas ساذجة

ضريبة الإطار: عندما تكون مكتبة الاختبار الرجعي لديك أبطأ من حلقة pandas ساذجة
#التداول الخوارزمي
#الاختبار الرجعي
#الأداء
#بحث المعاملات
#التوجيه المتجهي
#قياس الأداء
Part 2 of 10 · Collection
High-Performance Backtest Engines

جزء من سلسلة "اختبارات رجعية بلا أوهام".

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

قِسنا أداء ثمانية محركات اختبار رجعي على عملية مسح معاملات واحدة متطابقة، ووجدنا شيئا ينبغي أن يغيّر طريقة اختيارك لأداة البحث. اثنان من أشهر الأطر المفتوحة المصدر المدفوعة بالأحداث — backtrader وbt — شغّلا المسح أبطأ من حلقة pandas ساذجة كتبناها كخط أساس نُلقيه بعيدا. ليس أبطأ بقليل. استغرق backtrader نحو 2.5× من زمن خط أساس pandas؛ أما bt فنحو 4.7×. في الوقت نفسه شغّل محرك متجهي/مُصرَّف على العمل نفسه أسرع بنحو 13,000× من bt.

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

كل رقم هنا يأتي من منظومة تجارب واحدة قابلة لإعادة الإنتاج (benchmarks/bench_oss_engines.py، الالتزام 250dbb5) على حِمل عمل متطابق ومثبّت التكافؤ. حيثما لم نشغّل محركا بأنفسنا، نقول ذلك ونضعه في قسم أمانة منفصل بدلا من اختراع رقم.

تشغيل واحد مقابل عشرة آلاف

حسبة الاستراتيجية نفسها تتدفق عبر ثلاثة مسارات متباينة: مسار مدفوع بالأحداث باللون الأحمر البرتقالي مُجبر على التوقف عند كل محطة شمعة، وشجرة أوزان/إعادة موازنة من عُقد الخوارزميات باللون الكهرماني، وشعاع متجهي واحد بلون الزمرد والسماوي يحلّ السلسلة كلها دفعة واحدة — النموذج قدر محتوم بالنسبة لسرعة المسح

الحقيقة الحاسمة في بحث المعاملات هي أن المحرك يعمل آلاف المرات لكن التحليل يحدث مرة واحدة. أيّا كانت التكلفة الثابتة التي تدفعها لإعداد اختبار رجعي — بناء حلقة الأحداث، وإنشاء وسيط، وتخصيص كائن لكل شمعة — فأنت تدفعها في كل توليفة على حدة. تكلفة غير مرئية في تشغيل واحد ("من يهتم بـ6 ثوانٍ؟") تصبح الفاتورة بأكملها عبر المسح ("6 ثوانٍ × 10,000 = 16.6 ساعة").

تنقسم محركات الاختبار الرجعي إلى ثلاثة نماذج، والنموذج هو قدر محتوم بالنسبة لأداء المسح:

  1. المدفوع بالأحداث — يمشي المحرك شمعة بشمعة، مطلقا الأحداث، ومستدعيا استدعاءك الراجع next()/onBars()، وموجّها الأوامر عبر كائن وسيط. هذه هي بنية backtrader وbacktesting.py وPyAlgoTrade وzipline وnautilus_trader. إنها تعكس كيف يعمل التداول الحي فعلا، وهذا بالضبط سبب الوثوق بها من أجل الواقعية — وبالضبط سبب بطئها: يُدفع العبء لكل شمعة على مستوى Python 150,000 مرة لكل توليفة.
  2. الأوزان/إعادة الموازنة — يجلس bt هنا. تسلّمه مصفوفة أوزان مستهدفة فيعيد الموازنة في التواريخ التي تحددها. لا استدعاء راجع لكل شمعة، لكن ما زال هناك رسم كائنات لكل حدث (شجرة خوارزميات، ودفتر معاملات) يُقيّم في Python.
  3. المتجهي / المُصرَّف — يُعبَّر عن الاستراتيجية كلها كعمليات مصفوفات (vectorbt)، أو نواة مُصرَّفة بـ JIT (numba)، أو شيفرة أصلية (Rust، أو نواة MLX على GPU). لا يوجد أي Python لكل شمعة على الإطلاق. تعمل الحلقة، إن وُجدت، بسرعة الآلة.

بقية هذه المقالة هي النتيجة التجريبية لذلك التصنيف. بنينا حِمل عمل واحدا، وجعلنا كل محرك يؤدي — بشكل قابل للإثبات — العمل نفسه، وقِسنا الزمن.

حِمل العمل: استراتيجية واحدة، ثمانون مقبضا، متطابقة للجميع

القياس المرجعي أمين فقط إذا كان كل محرك يؤدي المهمة نفسها. حِملنا عادي عمدا — حِمل عمل يكون فيه الشيء الوحيد المختلف بين المحركات هو المحرك نفسه.

  • البيانات. سلسلة إغلاق واحدة اصطناعية من حركة براونية هندسية: 150,000 شمعة، seed=42، تقلب لكل شمعة sigma=0.0008، x0=30000. حتمية، فيمكن لأي شخص إعادة إنتاجها بت ببت. إغلاق فقط بحكم البناء — تُضبط OHLC على الإغلاق في كل مرحلة، لأن الاستراتيجية تقاطع إغلاق.
  • الاستراتيجية. تقاطع متوسط هال المتحرك: HMA بطول L مقابل نسخة HMA-الثلث الأسرع (HMA مقابل HMA3). دائما في السوق، مع الانقلاب بين الشراء والبيع عند كل تقاطع. هذا مؤشر حقيقي غير تافه — متوسطان متحركان مرجّحان متداخلان بالإضافة إلى مُنعّم نافذة الجذر التربيعي — وليس SMA لعبة، فالعمل لكل شمعة تمثيلي.
  • المسح. 80 طولا لـ HMA تمتد على 6..200. هذه هي "العشرة آلاف تشغيل" مصغّرة بما يكفي لقياسها مباشرة: 80 توليفة مستقلة، كل منها اختبار رجعي كامل على 150k شمعة.
  • التكاليف. رسم ذهاب وإياب قدره 0.09%، مقسوم لكل جانب للمحركات التي تنمذج عمولات الجانبين. تنفيذ في الشمعة نفسها عند close[i] — تُنفّذ الإشارة على الشمعة i عند إغلاق تلك الشمعة، وهي العُرف الذي يستخدمه محرك الإنتاج لدينا.

مؤقّت كل توليفة يلفّ شيئين تحديدا: حساب HMA المسبق بـ numpy وتشغيل المحرك. أما الإعداد الذي يُجرى فعلا لمرة واحدة (تحميل البيانات، بناء كائنات الشموع) فيقع خارج المؤقّت. هناك تشغيل إحماء واحد، ثم أفضل-من-N تكرارات، و— لأن المحركات المدفوعة بالأحداث بطيئة بما يكفي لأن تستغرق 80 توليفة كاملة عدة دقائق إلى أكثر من ساعة — نقيس زمن عيّنة موحدة من الشبكة ونستقرئ خطيا. التوليفات مستقلة، لذا فالاستقراء الخطي دقيق بالتوقّع؛ ويُطبَّق العُرف نفسه على خط أساس pandas، فلا يتميّز أي محرك بسببه.

التكافؤ: إثبات أن الجميع يؤدون العمل نفسه

هنا الفخّ الذي تقع فيه مقارنة المحركات الساذجة: قد يكون محرك "سريع" ببساطة يؤدي عملا أقل. إذا سجّل backtrader 2,700 صفقة وسجّل محركك المتجهي 40، فالمحرك المتجهي ليس أسرع — بل هو خاطئ، والمقارنة بلا معنى.

لذا نثبّت المقارنة بـ فحص تكافؤ عدد الصفقات. عند L=104، ينتج المرجع numpy بالضبط 2,707 صفقة مغلقة. على كل محرك أن يعيد إنتاج ذلك ضمن تفاوت مقداره ±1، وإلا أُجهض التشغيل بتأكيد work-parity FAILED. لا يوجد التفاوت إلا لأن المحركات تختلف على أعراف مسك الدفاتر — هل يُغلق المركز المفتوح النهائي بالإجبار ويُحسب، هل يُعدّ الدخول الأول "صفقة" — لا على الصفقات نفسها:

المحرك الصفقات المُبلَّغ عنها عند L=104 العُرف
مرجع numpy 2707 جولات ذهاب وإياب مغلقة
backtesting.py 2708 +1: المركز النهائي يُغلق بالإجبار عند النهاية
backtrader 2707 المركز المفتوح النهائي لا يُحسب
bt 2708 +1: الدخول الأول يُحسب كمعاملة
PyAlgoTrade 2708 +1: الدخول الأول يُحسب كتنفيذ

كل محرك يستقر عند 2707 ± 1. أيّا كانت فروق السرعة التي تتبيّن في النهاية، فهي ليست أثرا لمحرك يتخطى العمل بهدوء. هذا هو الانضباط الذي يتيح لنا أن نضع إطارا مدفوعا بالأحداث ونواة GPU في الجدول نفسه ونعني ذلك.

النتائج

هذا هو الجدول كاملا، مرتبا من الأسرع إلى الأبطأ. combos/s هي معدل الإنتاجية؛ والعمود الأخير هو المدة التي يستغرقها المسح الكامل ذو الـ80 توليفة. صف خط الأساس هو M0 — محرك pandas الساذج، حلقة for على الشموع بمسك دفاتر عددي، الشيء الذي قد تكتبه في عصر يوم واحد وترميه. كل ما هو أبطأ من ذلك الخط الأساس معروض بخط عريض.

المحرك combos/s النموذج المسح الكامل ذو الـ80 توليفة
نواة MLX على GPU 779 متجهي (Apple GPU) 0.10 s
Rust أصلي ~350 مُصرَّف 0.23 s
mp + numba 246 JIT مُصرَّف + متعدد العمليات 0.33 s
vectorbt 56.9 متجهي (numpy/numba) 1.4 s
numba (نواة واحدة) 39.7 JIT مُصرَّف 2.0 s
backtesting.py 1.42 مدفوع بالأحداث 56 s
PyAlgoTrade 0.51 مدفوع بالأحداث 2.6 min
M0 — pandas ساذج + حلقة 0.28 خط أساس عددي 4.8 min
backtrader 0.11 مدفوع بالأحداث 12.7 min
bt 0.06 أوزان / إعادة موازنة 22.5 min

اقرأ الجدول من أعلى إلى أسفل فيرتّب النموذجُ نفسَه: الخمسة الأوائل كلهم متجهيون أو مُصرَّفون، والخمسة الأواخر كلهم مدفوعون بالأحداث أو قائمون على رسم كائنات — مع جلوس حلقة pandas الساذجة فوق إطارين ناضجين شائعين. الفارق من الأعلى إلى الأسفل أربع مراتب من حيث القدر. على حِمل العمل نفسه بالضبط ذي الـ2,707 صفقة، تُنهي نواة MLX المسح في عُشر ثانية؛ بينما يحتاج bt إلى اثنتين وعشرين دقيقة ونصف. هذا عامل قدره نحو 13,000×.

الفضيحة في وسط الجدول

المفارقة في وسط الجدول: مُشغِّل حلقة for ساذج بلون رمادي-أزرق باهت يعبر خط النهاية متقدما على آلتَي إطارين مدفوعَين بالأحداث مُثقلتين بالميزات ومتوهجتين بالأحمر البرتقالي لا تزالان تكدّان خلفه، مثقلتين بنماذج الوسطاء وأكوام المحللات — خط الأساس المتواضع يهزم الأطر الناضجة على حِمل العمل نفسه ذي الـ2,707 صفقة

الأرقام اللافتة للنظر في الأطراف، لكن النتيجة التعليمية في الوسط: كلٌّ من backtrader (0.11 combos/s) وbt (0.06 combos/s) أبطأ من خط أساس pandas الساذج (0.28 combos/s).

هذا يستحق أن يترسّخ. M0 ليس محركا ذكيا. إنه حلقة for في Python تفهرس داخل DataFrame، وتتتبع مركزا ونقدا بأعداد عددية بسيطة، وتُلحق الصفقات بقائمة — إنه "الشاهد" غير المُحسَّن عمدا الذي أدرجناه لكي يكون لدينا شيء سيئ بوضوح نتفوّق عليه. الوصول لكل صف في pandas بطيء بشهرة، وقد استغللنا ذلك. ومع ذلك تخسر أمامه اثنتان من أكثر مكتبات الاختبار الرجعي التي يُوصى بها في المنظومة: backtrader بـ 2.5×، وbt بـ 4.7×.

الفارق الدقيق الذي يبقي هذا أمينا: ليس كل محرك مدفوع بالأحداث أبطأ من pandas. يتفوق backtesting.py (1.42 combos/s) على خط الأساس بـ 5×، لأنه حلقة أحداث رشيقة مدعومة بـ numpy تُبقي إنشاء الكائنات لكل شمعة عند حده الأدنى. وPyAlgoTrade (0.51) يتقدّم على خط الأساس أيضا بقليل. فـ"مدفوع بالأحداث" ليس حكما بالإعدام تلقائيا — لكن كلما ثقلت آلية كل شمعة، ساء الأمر، وbacktrader وbt يحملان أثقل آلية هنا. النموذج يضع السقف؛ والتنفيذ يقرّر أين تحت ذلك السقف تحطّ.

المقصد ليس أن هذه مكتبات سيئة. نموذج الوسيط في backtrader وتصميم شجرة-الخوارزميات في bt موجودان لكي يشتريا لك الصحة والتعبيرية — معالجة أوامر واقعية، وإعادة موازنة المحفظة، ومحلّلات. لتلك الميزات تكلفة تشغيل، وتلك التكلفة غير مرئية حين تشغّل مرة واحدة. عبر المسح تصبح القصة بأكملها.

لماذا تدفع المحركات المدفوعة بالأحداث الضريبة

أين تُفرَض ضريبة الإطار، لكل شمعة: تكرار شمعة واحد مُفكَّك إلى مكوناته المكلفة — كائن شمعة مُتحقَّق، وإطار استدعاء راجع، ووسيط يوجّه أمرا، ودفتر يُلحَق به — كل واحد بمثابة كشك رسوم متوهج بالأحمر البرتقالي يقتطع عملة، مكرّرا على طول رواق شاسع متلاشٍ من اثني عشر مليون تكرار شمعة متطابق

الآلية ليست غامضة. اختبار رجعي مدفوع بالأحداث، لكل شمعة، يفعل شيئا كهذا:

  1. يقدّم الساعة، ويقتطع الشمعة التالية من تغذية البيانات، ويُجسّدها ككائن (Bar، Line، قاموس).
  2. يُطلق استدعاء راجعا في شيفرة المستخدم (next()، onBars())، وهو استدعاء دالة Python بإطاره الخاص.
  3. داخل الاستدعاء الراجع، يستعلم عن حالة الوسيط/المركز، مرة أخرى عبر استدعاءات توابع وبحوث سمات.
  4. إذا أُنشئ أمر، يوجّهه عبر الوسيط: يتحقق منه، ويفحص الهامش/النقد، ويجدول تنفيذا، ويطفر كائن محفظة، ويُلحق بدفتر معاملات.
  5. يحدّث المحللات والمراقبين وأي مسك دفاتر يحافظ عليه الإطار.

الآن اضرب في 150,000 شمعة، ثم في 80 توليفة: اثنا عشر مليون تكرار شمعة لكل مسح، كل واحد منها حفنة من تخصيصات الكائنات على مستوى Python والإرسال الديناميكي. عبء Python لكل عملية — عشرات إلى مئات النانوثوانٍ لبحث سمة أو تخصيص صغير — تافه مرة واحدة ومُهلك اثني عشر مليون مرة. حالة bt هي نوع من المرض نفسه: رغم أنه يعيد الموازنة فقط في تواريخ الصفقات لا في كل شمعة، فإن كل إعادة موازنة تُقيّم شجرة من كائنات الخوارزميات وتمسّ دفتر محفظة مدعوما بـ pandas، وهناك 2,707 منها لكل توليفة، ضربا في 80.

تتفوق حلقة pandas الساذجة على backtrader وbt لسبب فجّ: إنها تفعل أقل لكل شمعة. تتخطى الوسيط، وكائنات الأحداث، وكومة المحللات، وآلة حالة توجيه الأوامر. تدفع ضريبة pandas القبيحة لكل صف، لكن تلك الضريبة القبيحة الواحدة ما زالت أرخص من ضريبة الإطار الأنيقة الكاملة الميزات ذات الكائن-لكل-حدث. حين تجرّد الاختبار الرجعي إلى "المركز × العائد التالي − الرسوم"، فإن معظم ما يفعله الإطار لكل شمعة هو عبء لا تستخدمه أثناء البحث.

وهنا الفخّ: العبء هو السبب الذي اخترت من أجله الإطار. أردتَ الوسيط الواقعي. أردتَ المحللات. أثناء التحقق النهائي، تريده كله. أثناء بحث ذي 10,000 توليفة تقرأ منه فقط هدفا عدديا واحدا من الطرف الآخر، أنت تدفع أجرة ليموزين لتدور بها الدورات.

الطرف الآخر من الجدول: المتجهي والمُصرَّف

أعلى الجدول هو ما يحدث حين تحذف Python لكل شمعة تماما.

  • vectorbt (56.9 combos/s) يعبّر عن الاستراتيجية كلها كعمليات مصفوفات numpy/numba. لا توجد حلقة شموع في Python — الإشارة والمركز والربح/الخسارة كلها على مستوى المصفوفة. يشغّل المسح في 1.4 ثانية مقابل 22.5 دقيقة لـbt: أسرع بنحو 950× على العمل نفسه. (نغطي تصميم vectorbt بمزيد من العمق في نظرة عامة على vectorbt والمقارنة الأوسع pandas-vs-polars.)
  • numba (39.7) هي حلقة pandas نفسها، دون تغيير في شكلها، مُصرَّفة بـ JIT إلى شيفرة أصلية. الخوارزمية نفسها كـ M0، لكن @njit يحوّل 0.28 combos/s إلى 39.7 — تسريع بنحو 140× من مُزيِّن، لأن عبء المفسّر الذي سيطر على الحلقة العددية يتبخّر ببساطة.
  • mp + numba (246) يشغّل النواة المُصرَّفة عبر أنوية المعالج. التوليفات متوازية إلى حد مخجل — كل منها مستقلة — فتتوسّع المعالجة المتعددة خطيا تقريبا فوق الـ JIT.
  • Rust أصلي (~350) يزيل آخر ما تبقّى من غراء Python: المسح كله أصلي.
  • MLX على GPU (779) يُسقط المسح على نواة GPU بسيليكون Apple. تصبح الـ80 توليفة 80 مسارا متوازيا من الحسابات؛ ينتهي المسح قبل أن تُطلق زر الإدخال.

شيئان يستحقان أن يُسمّيا بدقة. أولا، numba يثبت أن النموذج يهمّ أكثر من اللغة. M0 وnumba يشغّلان الخوارزمية نفسها — الفرق هو محض أن أحدهما Python مُفسَّر لكل شمعة والآخر مُصرَّف. هذه هي ضريبة الإطار بأكملها في تجربة أ/ب مضبوطة: نحو 140× مقابل حذف المفسّر من الحلقة الداخلية. ثانيا، القفزة من numba (39.7) إلى mp+numba (246) إلى MLX (779) لم تعد عن المحرك على الإطلاق — بل عن التنظيم والعتاد. بمجرد اختفاء ضريبة كل شمعة، تصبح السرعة مسألة كم توليفة تشغّل بالتوازي وعلى أي سيليكون. نمشي في هذا التدرّج الكامل في سلّم سرعة محركات الاختبار الرجعي، وسبب هيمنة تكلفة العمليات/التسلسل على الميل الأخير في مقالة ضريبة IPC.

ما لم نقسه (ولماذا نخبرك بذلك)

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

  • zipline / zipline-reloaded — مدفوع بالأحداث، من سلالة Quantopian. إعداد ثقيل (تقويم تداول كامل وحزمة بيانات)، ما يجعل توقيت كل توليفة تفاحة-بتفاحة عسيرا. من الناحية المعمارية يجلس مع backtrader في معسكر المدفوع بالأحداث؛ نتوقع أن يكون قرب ذلك الطرف من الجدول، لكننا لم نثبت ذلك.
  • nautilus_trader — مدفوع بالأحداث بنواة Rust/Cython، مصمّم صراحة لتكافؤ التداول الحي. نواته مُصرَّفة، فهو المحرك المدفوع بالأحداث الأرجح لـ ألّا يدفع ضريبة Python الكاملة — قياس مثير للاهتمام حقا لم نُجرِه بعد.
  • QuantConnect Lean — قائم على C#، وقت تشغيل مختلف تماما؛ غير قابل للمقارنة مباشرة في منظومة Python.
  • Jesse — مدفوع بالأحداث، متمحور حول الكريبتو؛ راجعنا تصميمه في ملاحظة منفصلة لكننا لم نقس أداءه هنا.
  • QSTrader — مدفوع بالأحداث، موجّه نحو المحفظة؛ التحفظات النموذجية نفسها.
  • fastquant — حاولنا؛ كان التثبيت/الواجهة معطلا في بيئتنا، فلا يوجد رقم. لن نخمّن واحدا.

تحفظان أمينان حول الأرقام التي نبلّغ عنها. أرقام vectorbt وnumba وmp+numba وRust الأصلي وMLX تأتي من سلّم محركاتنا على حِمل العمل نفسه، لا من منظومة OSS التي أنتجت صفوف المدفوع بالأحداث الأربعة — إنها حِمل العمل نفسه لكن منصة قياس مختلفة، ورقم Rust الأصلي تقريبي ~350، لا قياس محكم. والـ combos/s المطلقة خاصة بالعتاد؛ ما ينتقل هو الترتيب والنسب، وهي كبيرة بما يكفي (13,000× من الأعلى إلى الأسفل، 2.5–4.7× لانقلاب pandas-مقابل-الإطار) بحيث لا يقلبها أي فرق عتاد معقول.

دفاعا عن المحركات المدفوعة بالأحداث

المحرك المدفوع بالأحداث في دوره الصحيح: تشغيل تحقّق واحد دقيق عالي الأمانة لاستراتيجية ناجية واحدة، مصوَّرا كأداة دقيقة بلون الزمرد والسماوي تنمذج تنفيذا واقعيا، وتحترم الهامش ودورة حياة الأمر — آلية كل شمعة نفسها التي كانت هدرا محضا أثناء البحث معروضة هنا كحرفية قيّمة، بروفة أخيرة حريصة قبل النشر

من السهل قراءة هذا على أنه "أدوات الاختبار الرجعي المدفوعة بالأحداث سيئة". هذا هو الدرس الخاطئ، وهو غير عادل.

المحركات المدفوعة بالأحداث مبنية لمهمة مختلفة، وهي بارعة فيها. وسيط كل شمعة، ودورة حياة الأمر، ومنطق التنفيذ، والمحللات — تلك الآلية موجودة لجعل الاختبار الرجعي يشبه التداول الحي بأقصى قدر ممكن. حين يكون هدفك بروفة واحدة موثوقة لاستراتيجية أنت على وشك نشرها، فأنت تريد من المحرك أن يعرق على كل تنفيذ، وأن ينمذج التنفيذات الجزئية، وأن يحترم الهامش، وأن يرفض السماح لك بالتداول بأسعار لم تكن لتحصل عليها. تلك الأمانة هي المنتج. تكلفة تشغيلها هي ثمن الواقعية، ولتشغيل واحد يكون الثمن ضئيلا.

الفشل ليس في المحرك؛ بل في استخدامه للمرحلة الخاطئة. للبحث مرحلتان متمايزتان بمتطلبات متعاكسة:

  • البحث يريد إنتاجية. أنت تستكشف منظرا معظمه قمامة، وتحتاج إلى تقييم آلاف النقاط للعثور على القليل الذي يستحق نظرة ثانية. أمانة كل نقطة بالكاد تهمّ — أنت ترتّب، لا تنشر. هنا ضريبة الإطار هدر محض.
  • التحقق يريد أمانة. لديك حفنة من المرشحين وتحتاج أن تعرف، بأكبر دقة ممكنة، هل ينجون من تنفيذ واقعي، ورسوم، وانزلاق، وفخاخ النظر إلى الأمام التي تضخّم عوائد الورق. هنا يكسب المحرك المدفوع بالأحداث تكلفته.

الخطأ الذي تعاقب عليه ضريبة الإطار هو تشغيل بحثك على محرك التحقق لديك — دفع أجور ليموزين لاستكشاف منظر ستلقي 99% منه بعيدا.

قاعدة القرار

الخلاصة العملية تُختصر إلى قرار واحد:

ابحث على محرك متجهي/مُصرَّف. حقّق الناجين على محرك مدفوع بالأحداث.

على وجه التحديد:

  1. ابنِ أو استعِر نواة سريعة للمسح. vectorbt إن أردتها جاهزة؛ أو حلقة مُصرَّفة بـ numba إن كانت استراتيجيتك لا تتجه بنظافة (مُزيِّن @njit وحده اشترى نحو 140× هنا). شغّل فضاء المعاملات كاملا خلالها.
  2. لا تشغّل أبدا مسحا كبيرا على backtrader أو bt أو zipline أو أي إطار ثقيل مدفوع بالأحداث. إذا كان مسح على أحد هذه هو عنق الزجاجة لديك، فالحل ليس آلة أكبر — بل المحرك الخاطئ. حتى حلقة pandas الساذجة ستتفوق على اثنين منها.
  3. رقِّ قائمة قصيرة إلى المحرك المدفوع بالأحداث لفحص أمانة. خذ حفنة الناجين وأعِد تشغيلها على المحرك الواقعي، حيث يمكن لنموذج الوسيط ومنطق التنفيذ كشف مشكلات جرّدتها النواة السريعة.
  4. افرض التكافؤ بين الاثنين. على المحرك السريع ومحرك الأمانة أن يتفقا على عدد الصفقات والربح/الخسارة على تكوين ثابت (فحصنا لـ±1 صفقة عند L=104)، وإلا فالبحث والتحقق يقيسان استراتيجيتين مختلفتين وخط الأنابيب كله كذبة.

هذه هي بنية السرعتين نفسها التي تظهر كلما كان للبحث والتحقق ملفّا تكلفة متعاكسان، وهي السبب في أن مكدّسنا يحتفظ بمسار متجهي/مُصرَّف سريع لـبحث المعاملات ويحجز الآلية الثقيلة لـتقييم دالة الهدف النهائي وفحوص الهضبة.

الخلاصات

  1. الشائع ≠ السريع للمسوحات. على حِمل عمل واحد متطابق مثبّت التكافؤ (150k شمعة، 80 توليفة تقاطع HMA، 2,707 صفقة)، شغّل كلٌّ من backtrader (0.11 combos/s) وbt (0.06) أبطأ من حلقة pandas ساذجة (0.28). إطار ناضج حائز على نجوم كثيرة ليس تلقائيا الخيار السريع.
  2. ضريبة الإطار لكل شمعة، والمسح يضاعفها. اثنا عشر مليون تكرار شمعة لكل مسح، كل منها يحمل كائن حدث، واستدعاء راجعا، وذهاب وإياب وسيط. تكلفة غير مرئية في تشغيل واحد هي الفاتورة بأكملها عبر عشرة آلاف.
  3. النموذج يضع السقف. المحركات المتجهية/المُصرَّفة (vectorbt 56.9، numba 39.7، mp+numba 246، Rust أصلي ~350، MLX 779) تتفوق على المحركات المدفوعة بالأحداث بمرتبتين إلى أربع مراتب من حيث القدر — حتى نحو 13,000× من الأعلى إلى الأسفل. الخوارزمية نفسها، بمجرد تصريفها بـ JIT، مشت أسرع 140×.
  4. ليست كل المحركات المدفوعة بالأحداث متساوية. backtesting.py (1.42) وPyAlgoTrade (0.51) ما زالا يتفوقان على خط الأساس الساذج؛ الضريبة تتوسّع مع مدى ثقل آلية كل شمعة. التنفيذ يقرّر أين تحت السقف تحطّ.
  5. محركان، مرحلتان. ابحث على نواة متجهية/مُصرَّفة؛ حقّق الناجين على المحرك الواقعي المدفوع بالأحداث. افرض تكافؤ عدد الصفقات/الربح والخسارة بينهما لكي يقيس كلاهما الاستراتيجية نفسها.
  6. كن أمينا حول ما قسته. قِسنا ثمانية محركات تحت التكافؤ وسردنا التي لم نقسها (zipline، nautilus_trader، Lean، Jesse، QSTrader، وتثبيت fastquant الذي رفض العمل) بدلا من اختراع أرقام لها.

الخلاصة المزعجة: إذا كان مسح المعاملات هو عنق الزجاجة لديك، فالمشكلة على الأرجح ليست في آلتك ولا في استراتيجيتك. المشكلة أنك تشغّل بحثا على محرك بُني من أجل تشغيل واحد أمين — وكنتَ ستكون أسرع بحلقة pandas التي كنتَ محرجا جدا من الاحتفاظ بها.

المنظومة الكاملة، وتأكيدات التكافؤ، ونتائج JSON الخام لكل محرك تعيش في benchmarks/bench_oss_engines.py وbenchmarks/results_oss/ عند الالتزام 250dbb5. لطرف المُصرَّف/GPU من السلّم، انظر سلّم سرعة محركات الاختبار الرجعي وتحليل ضريبة IPC.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

ابقَ متقدماً على السوق

اشترك في نشرتنا الإخبارية للحصول على رؤى حصرية حول تداول الذكاء الاصطناعي وتحليلات السوق وتحديثات المنصة.

نحترم خصوصيتك. يمكنك إلغاء الاشتراك في أي وقت.