تنكسر لوحات التداول بطرق لا تنكسر بها تطبيقات الويب العادية. تصل البيانات باستمرار لا عند الطلب، وتعتمد عشرات العناصر على شرائح متداخلة من الحالة نفسها، والإطار المُسقَط خلال دقيقة متقلبة كلفة حقيقية لمن يراقب الشاشة. يستعرض هذا المقال المعمارية التي استخدمناها لـ TradeBot، وهي لوحة لمشغّلين يشغّلون استراتيجيات آلية عبر عدة منصات، والقرارات التي أبقتها سريعة الاستجابة.
افصلوا أنواع الحالة الثلاثة
أول خطأ تقع فيه الفرق هو وضع كل شيء في مخزن واحد. لواجهة التداول ثلاثة أنواع من الحالة، بدورات حياة مختلفة:
- حالة الخادم — ما يقيم في قاعدة بيانات ويتغيّر عند الطلب: إعدادات الحساب، وتعريفات الاستراتيجية، والتنفيذات التاريخية، والتخطيطات المحفوظة. تُجلب، وتُخزَّن مؤقتاً، وتُبطَل.
- حالة البث — ما يصل باستمرار عبر المقبس: دفاتر الأوامر، وأشرطة الأسعار، والمراكز، والأوامر المفتوحة، وPnL. تُدفع، وتُوحَّد، وتُدمَج.
- حالة الواجهة — ما يهم داخل المتصفح فقط: السوق المحدد، والتبويب النشط، والنوافذ المفتوحة، وعروض الأعمدة.
نستخدم TanStack Query لحالة الخادم، ومخزن Zustand لحالة البث وحالة الواجهة، ولا ندع أحدهما يتسرب إلى الآخر. تحصل حالة الخادم مجاناً على إلغاء تكرار الطلبات، وإعادة الجلب في الخلفية، وإبطال الذاكرة المؤقتة. وتحصل حالة البث على شكل مسطّح موحَّد واشتراكات انتقائية. وتبقى حالة الواجهة محلية ورخيصة.
الفائدة أن كل نوع من الحالة يمكن تحسينه على حدة. إعدادات الذاكرة المؤقتة للاستعلامات لا علاقة لها بإنتاجية المقبس؛ وإنتاجية المقبس لا علاقة لها بوتيرة إعادة تصيير النافذة المنبثقة.
وحّدوا التدفق قبل أن يلمس React
تغذيات المنصات فوضوية. ترسل منصة لقطات كاملة لدفتر الأوامر كل ثانية؛ وترسل أخرى فروقاً بأرقام تسلسل؛ وترسل ثالثة المراكز بمخطط مختلف عن تنفيذاتها. إذا وصل هذا التنوع إلى مكوّناتكم، صار كل عنصر طبقة ترجمة.
قاعدتنا: طبقة تدفق واحدة، خارج React، تملك اتصالات المقبس وتحوّل كل رسالة إلى حدث من مجموعة صغيرة من الأحداث الداخلية — book.snapshot وbook.delta وticker وposition وorder وfill. يطبّق المخزن تلك الأحداث على بنية موحَّدة مفتاحها المنصة والسوق:
type Store = {
books: Record<MarketId, OrderBook>;
tickers: Record<MarketId, Ticker>;
positions: Record<PositionId, Position>;
orders: Record<OrderId, Order>;
connection: Record<Venue, "live" | "reconnecting" | "stale">;
};
لا ترى المكوّنات حمولات خاصة بمنصة. تشترك في books[marketId] أو positions ولا شيء سوى ذلك. حين تُضاف منصة جديدة، تكتسب طبقة التدفق محوّلاً، ولا يتغيّر شيء في شجرة المكوّنات.
صيّروا ما تغيّر فقط
يستطيع React التعامل براحة مع آلاف التحديثات في الثانية إذا أعاد كل تحديث تصيير شجرة فرعية صغيرة. ولا يستطيع إذا أعاد كل تحديث تصيير اللوحة. ثلاث تقنيات تؤدي معظم العمل:
اشتراكات انتقائية. يشترك كل عنصر في أضيق شريحة يحتاجها عبر محدّد، ولا يُخطر المخزن إلا المشتركين الذين تغيّر مرجع شريحتهم. عنصر دفتر أوامر لـ BTC/USDT لا يُعاد تصييره حين يتحرّك سعر ETH/USDT.
التجميع والخنق عند الحد. يمكن أن تصل فروق دفتر الأوامر أسرع مما تُحدّث الشاشة. تجمع طبقة التدفق الفروق وتفرغها إلى المخزن مرة واحدة على الأكثر في كل إطار عرض. لا يُفقد شيء مرئي — الدفتر صحيح بعد كل إفراغ — وينخفض عمل React بنحو عشرة أضعاف خلال الدفعات المفاجئة.
هويات مستقرة للصفوف. تستخدم جداول الأوامر والتنفيذات مفاتيح مستقرة ومكوّنات صفوف مُحفَّظة عبر memo، فيُلحِق التنفيذ الجديد صفاً واحداً بدلاً من إعادة مواءمة القائمة كلها. وتُبقي القوائم الافتراضية السجلات الطويلة رخيصة.
تتبع الرسوم المبدأ نفسه. نغذّي مكتبة الرسوم بتحديثات تدريجية (update(lastBar)) بدلاً من استبدال السلسلة، ونُبقي حالة الرسم خارج React تماماً، لأن المكتبة تدير عنصر canvas الخاص بها.
صمّموا للانقطاع
تسقط المقابس. تعيد المنصات التشغيل. تغفو الحواسيب المحمولة. لوحة تعرض بصمت مركز الأمس أسوأ من لوحة لا تعرض شيئاً. نعامل حالة الاتصال كبيانات من الدرجة الأولى:
- لكل منصة حالة —
liveأوreconnectingأوstale— مشتقة من نبضات الاتصال وطوابع آخر رسالة، وكل عنصر يعتمد على تلك المنصة يعرض مؤشراً لا لبس فيه حين لا يكون الاتصال مباشراً. - تستخدم إعادة الاتصال تراجعاً أسّياً مع تذبذب، وتعيد الاشتراك تلقائياً. بعد إعادة الاتصال، تطلب طبقة التدفق لقطة جديدة لكل دفتر مشترك، وتتجاهل الفروق التي تقل أرقام تسلسلها عن اللقطة.
- تُجلب من جديد استعلامات حالة الخادم التي تؤثر في القرارات (الأوامر المفتوحة، والمراكز) بعد إعادة الاتصال، لأن المقبس ربما فاته أحداث أثناء الانقطاع.
هذا عمل بلا بريق، وهو الفرق بين عرض تجريبي وأداة يأتمنها الناس على المال.
ضوابط المشغّل تحتاج احتكاكاً
TradeBot ليست عرضاً سلبياً. يوقف المشغّلون الاستراتيجيات مؤقتاً ويستأنفونها، ويعدّلون المعاملات، ويضعون أوامر يدوية. كل واحد من هذه الإجراءات يمكن أن يخسر مالاً إذا كان نقرة خاطئة، لذا الضوابط أبطأ من البيانات عمداً:
- الإجراءات التدميرية، أو التي تحرّك المال، تتطلب خطوة تأكيد تعيد ذكر ما سيحدث («إلغاء 12 أمراً مفتوحاً على Binance BTC/USDT؟»).
- تُجهَّز تغييرات المعاملات وتُطبَّق صراحة، لا مع كل ضغطة مفتاح.
- يُكتب كل إجراء في سجل إجراءات يبيّن من فعل، وماذا فعل، ومتى، واستجابة الخادم الناتجة، ويظهر هذا السجل داخل اللوحة نفسها.
ونفصل قرار التحديث التفاؤلي أيضاً لكل إجراء. إلغاء أمر يحدّث الواجهة فوراً، ثم تُعاد مواءمتها مع الخادم؛ ووضع أمر ينتظر الإقرار، لأن أمراً يظهر كأنه وُضع ولم يُوضع هو إخفاق أسوأ من تأخير قصير.
التخطيط والتخصيص
يراقب المشغّلون أموراً مختلفة. سلّمنا تخطيطات قابلة للتهيئة — شبكة عناصر لكل مستخدم، بأحجام ومواضع — وقوائم مراقبة لكل مستخدم، محفوظة كحالة خادم. التخطيطات وثائق JSON تُتحقق بمخطط، فيصمد التخطيط القديم أمام إصدار جديد من اللوحة. التخطيط الافتراضي هو الذي توقعنا أن يحتاجه معظم الناس؛ وعملياً غيّره الجميع تقريباً خلال يوم، وهذا هو المقصود.
ما نكرره، وما نغيّره
نكرره: فصل الحالات الثلاث، وطبقة التدفق الموحَّدة، وحالة الاتصال لكل منصة. هي الأساس الذي أتاح لنا إضافة منصات وعناصر من دون المساس بالكود القائم.
نغيّره: سمحنا في البداية للعناصر بالاشتراك في الدفاتر كاملة لا في أعلى N مستوى. ولدفتر من 200 مستوى كان ذلك عملاً ضائعاً؛ فمعظم العناصر تعرض 20 مستوى فقط. اعرضوا محدّدات محدودة العمق من البداية.
نغيّره أيضاً: استثمرنا أقل مما ينبغي في أداة إعادة تشغيل مبكراً. القدرة على تسجيل جلسة تدفق وإعادة تشغيلها على الواجهة تجعل عمل الأداء وإعادة إنتاج الأخطاء أسهل بكثير. ابنوها في الشهر الأول.
الخلاصة
لوحة التداول نظام بث ملحقة به واجهة React أمامية، لا تطبيق React يتفق أنه يستخدم WebSocket. أبقوا حالة البث خارج المكوّنات، ووحّدوا بقوة، وصيّروا انتقائياً، وعاملوا حالة الاتصال كبيانات، وأضيفوا احتكاكاً إلى الإجراءات التي تحرّك المال. افعلوا ذلك، فيصير اختيار الإطار أقل أهمية بكثير مما يفترض الناس.