الـ matching engine الفوري آلة حالة صغيرة تحيط بها قيود صارمة: price-time priority، وتنفيذات جزئية، وذيل latency لا يتحرك تحت أحمال الاندفاع. هكذا يُبنى الـ order book وحلقة المطابقة والسجل ومنصة إعادة التشغيل، وهنا يتوقف عون اللغة.
الـ matching engine في بورصة فورية مهمته ضيقة. يأخذ تدفقًا مرتبًا من الأوامر - أمر جديد، إلغاء، استبدال - ويطبّقها على order book وفق price-time priority، ثم يُصدر تدفقًا مرتبًا من الأحداث: صفقات، وتحديثات دفتر، ورفوض، وإقرارات.
وكل ما فيه من صعوبة يأتي من ثلاثة قيود مكدّسة فوق تلك المهمة: أن تكون النتيجة متطابقة في كل إعادة تشغيل للمدخل نفسه، وأن تحتفظ بقية الأمر المنفَّذ جزئيًا بمكانها في الطابور، وألا يتحرك ذيل الـ latency عند وصول اندفاع.
الدفتر جانبان، كل منهما مجموعة مستويات مرتبة بالسعر. والمستوى ليس رقمًا - إنه طابور أوامر راكدة عند ذلك السعر، بترتيب الوصول. والمطابقة تلمس أفضل مستوى باستمرار والمستويات العميقة نادرًا، فتُختار البنية لنمط الوصول هذا لا للأناقة.
نتيجة الطوابير التطفلية والمقابض الفهرسية أن الأمر الراكد لا يتحرك في الذاكرة أبدًا ما دام حيًا. وموضعه في الطابور خاصية لروابطه، لا لمكان جلوسه، وهذا ما يجعل التنفيذات الجزئية رخيصة لاحقًا.
الأمر المهاجم الوارد يمشي على الجانب المقابل من أفضل سعر إلى الداخل. وعند كل مستوى يمشي على طابور FIFO من مقدمته. ويتوقف حين لا يعود سعر المستوى مقبولًا للأمر الوارد أو حين تبلغ الكمية الواردة صفرًا.
الأمر الراكد المنفَّذ جزئيًا يحتفظ بمكانه. تبقى بقيته في مقدمة طابوره بتسلسل وصوله الأصلي، لأن التنفيذ يغيّر كمية فقط لا غير. أما الأمر المهاجم المنفَّذ جزئيًا إذا كان limit عاديًا فيصبح أمرًا راكدًا في مؤخرة مستوى سعره، بتسلسل وصول جديد - فقد وصل الآن، لا قبل ذلك.
دلالات أنواع الأوامر قرارات تُتخذ عند حدود هذه الحلقة لا داخلها. فـ immediate-or-cancel يُسقط البقية بدل إراحتها. وfill-or-kill يجري تمريرة جافة أولًا ثم ينفذ كاملًا أو يرفض. وpost-only يرفض إن كان الأمر سيتقاطع عند الوصول. وإبقاء هذه خارج الحلقة يعني أن الحلقة تبقى المكان الوحيد الذي تتغير فيه حالة الدفتر.
منع التداول الذاتي والكميات الدنيا والتحقق من الـ tick واللوت تنتمي كذلك إلى ما قبل الحلقة. فالأمر الذي يبلغ المطابقة قد ثبتت سلامة بنيته، فلا تحمل الحلقة فروع أخطاء تبطّئها أو يُختلف حولها.
نادرًا ما يكون متوسط الـ latency هو المشكلة. المشكلة أسوأ قياس أثناء الاندفاع، وهو الوقت الذي يهم فيه المحرك أكثر ما يهم والأرجح أن تقع فيه وقفة توقف العالم. وتحت بيئة تشغيل مُدارة فيها garbage collector، يجدول تلك الوقفة الجامعُ لا أنت، وتقع في منتصف الاندفاع الذي أنتج النفايات.
التخصيص اليدوي نسخة أصغر من المشكلة نفسها. فمخصِّص عام قد يمشي على قوائم حرة، أو يأخذ قفلًا، أو يطلب من النواة ذاكرة إضافية، والاستدعاء الذي يفعل ذلك هو الذي يظهر في الذيل. والعلاج واحد في الحالتين: لا تخصّص على المسار الساخن إطلاقًا.
يكون المحرك حتميًا حين ينتج تسلسل المدخلات نفسه تسلسل المخرجات نفسه، بايتًا ببايت، على جهاز آخر وبعد سنة. وأي شيء يقرأ ساعة الحائط أو جدولة الخيوط أو ترتيب تكرار الـ hash داخل مسار المطابقة يكسر هذه الخاصية.
لذلك فالطوابع الزمنية مدخل، لا شيء يقرأه المحرك بنفسه. فالمُسلسِل يختم الأمر حين يقبله، وحلقة المطابقة تعامل الختم كبيانات. والعشوائية، إن لزمت، تأتي من مولّد ببذرة تكون البذرة نفسها جزءًا من السجل.
الاسترداد ليس ميزة تُركَّب بعد أن تعمل المطابقة. يكتب المحرك سجلًا للإلحاق فقط بالأوامر المقبولة بترتيب التسلسل، والدفتر في الذاكرة ليس إلا نتيجة طيّ ذلك السجل. وإعادة البناء بعد عطل تعني إعادة تشغيله.
المحرك يكتب السجل، لكن الديمومة خاصية لمسار التخزين ولعدد الأجهزة التي تملك السجل قبل خروج الإقرار. وذلك قرار نسخ متماثل وعتاد، وهو حيث يُربح زمن الاسترداد أو يُخسر فعليًا.
الحتمية هي ما يجعل المحرك قابلًا للاختبار. فلأن المدخل نفسه يعطي المخرج نفسه، تصير الجلسة المسجّلة اختبار انحدار، ويمكن إعادة إنتاج أي عطل عُثر عليه مرة واحدة بدقة بدل مطاردته.
النموذج المرجعي أثمن مما يبدو. فتنفيذان مكتوبان من المواصفة نفسها يختلفان تحديدًا حيث كانت المواصفة غامضة، وقواعد المطابقة مليئة بالغموض عند الحواف - حدود متقاطعة، وبقايا صفرية، وإلغاءات تسابق التنفيذات.
تزيل Rust فئة من المشكلات أكثر مما تجعل الحلقة أسرع بذاتها. فلا يوجد garbage collector، فلا تُجدول وقفة من وراء ظهرك. والملكية تجعل انضباط الكاتب الواحد شيئًا يفرضه المترجم بدل شيء على مراجعة الكود أن تلحظه. ومقابض الـ slab والروابط التطفلية، وهي مظنة الخطأ في لغة بلا أعمار، قابلة للفحص هنا. وحالات panic عند تجاوز الأعداد الصحيحة في بنيات التصحيح تلتقط صنفًا من العلل يفسد الدفتر بصمت.
الحد الأمين هو أن معظم ما يحدد latency الذيل ليس اللغة:
لـ Rust كلفة أيضًا. فالـ borrow checker يبطّئ الأسابيع الأولى لتصميم ما زال يتحرك، والمنظومة الخاصة ببروتوكولات البورصات أرقّ منها في لغات أقدم، وكتل unsafe حول البنى اللاقفلية تحتاج انضباط المراجعة نفسه المطلوب لأي كود مكافئ في أي مكان. واختيارها قرار بشأن ذيل الـ latency وسلامة الذاكرة في نواة ذات كاتب واحد، لا قرار بشأن راحة المطور.
تبني amBrain بنية تداول تحتية في يريفان، أرمينيا منذ 2019، بمسارات ساخنة بلغة Rust - بيانات سوق تصل في أقل من 5 ms، وفحوص مخاطر قبل التداول في أقل من 1 ms. إن كنت تصمم matching engine وتريد استعراض بنية الدفتر أو صيغة السجل أو منصة إعادة التشغيل، فهذا حديث يستحق أن يجري قبل كتابة أول سطر في المسار الساخن.
فريقنا الهندسي متخصص في حلول FinTech. لنناقش كيف يمكننا تنفيذ مشروعك.