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

نمذجة فرق السعر بين العرض والطلب والتنبؤ به باستخدام التعلم الآلي

نمذجة فرق السعر بين العرض والطلب والتنبؤ به باستخدام التعلم الآلي
#microstructure
#spread
#market-making
#prediction
#machine-learning
#gradient-boosting
#deep-learning

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

لماذا تهم الفروق صانعي السوق

يعرض صانع السوق باستمرار سعر طلب PbP_b وسعر عرض PaP_a. الفرق المعروض هو:

S=PaPbS = P_a - P_b

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

Srealized=SeffectivePriceImpactS_{\text{realized}} = S_{\text{effective}} - \text{PriceImpact}

نقيس الكميات الثلاث جميعها على أساس الفرق الكامل (وليس النصف)، حتى تظل المعادلة متسقة بُعدياً. الفرق الفعّال لصفقة واحدة هو:

Seffective=2dt(PtMt)S_{\text{effective}} = 2 \cdot d_t \cdot (P_t - M_t)

هنا dt{+1,1}d_t \in \{+1, -1\} هو اتجاه الصفقة (شراء أو بيع من الآخذ)، وPtP_t هو سعر التنفيذ، وMtM_t هو نقطة منتصف أفضل طلب وأفضل عرض وقت الصفقة. ويُعرَّف حد أثر السعر تناظرياً على أفق ما بعد الصفقة τ\tau:

PriceImpact=2dt(Mt+τMt)\text{PriceImpact} = 2 \cdot d_t \cdot (M_{t+\tau} - M_t)

حيث Mt+τM_{t+\tau} هو المنتصف بعد أفق τ\tau من الصفقة. يجب ذكر الأفق صراحةً — والخيارات الشائعة هي 5 دقائق في الأسهم و30 ثانية في العملات المشفرة، حيث يُعاد تسعير الأسعار بوتيرة أسرع. وطرح الأثر من الفرق الفعّال يترك الفرق المحقق: ما يحتفظ به صانع السوق بعد أن تحرّك السوق.

صانع السوق القادر على التنبؤ بالفرق — وبمكوناته — خلال الثانية أو الخمس ثوانٍ أو الستين ثانية التالية يستطيع تعديل عروضه ديناميكياً لتعظيم الفرق المحقق مع الحفاظ على معدلات التنفيذ.

مكونات الفرق الثلاثة

فرق السعر بين العرض والطلب مفكَّكاً إلى طبقات تكلفة معالجة الأوامر والمخزون والاصطفاء العكسي

تفكّك أدبيات البنية الدقيقة للسوق (ستول 1978، غلوستن وميلغروم 1985، هوانغ وستول 1997) فرق السعر بين العرض والطلب إلى ثلاثة مكونات اقتصادية.

1. تكلفة معالجة الأوامر (α\alpha)

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

والفارق الجوهري هو مَن يدفع أيّ رسم. صانع السوق الذي يعرض بشكل سلبي يدفع رسم صانع السوق fmf_m على عمليات تنفيذه الخاصة — وفي كثير من المنصات يكون fmf_m حسماً مرتجعاً (rebate)، أي سالباً. وهو لا يدفع رسم الآخذ ftf_t على تلك التنفيذات السلبية؛ بل الطرف المقابل الذي يعبر الفرق هو من يدفع ftf_t. وعليه فإن تكلفة معالجة الأوامر لكل جانب لدى صانع السوق هي:

αfm+cinfra\alpha \approx f_m + c_{\text{infra}}

حيث fmf_m مُؤشَّر (الحسم المرتجع يخفّض α\alpha وقد يجعله سالباً) وcinfrac_{\text{infra}} يغطي الاتصال، والموقع المشترك (colocation)، والحوسبة. وفي الأسواق الإلكترونية الحديثة تقلّص هذا المكوّن تقلصاً كبيراً — إلى ما دون السنت في الأسهم، وبضع نقاط أساس أو حسماً مرتجعاً صافياً في العملات المشفرة.

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

2. تكلفة الاحتفاظ بالمخزون (β\beta)

عندما يراكم صانع السوق مركزاً اتجاهياً (طويلاً أو قصيراً)، فإنه يتحمل مخاطرة السعر. ويعوّض مكوّن المخزون عن هذه المخاطرة. وقد نمذجها ستول (1978) وأميهود وميندلسون (1980) بوصفها دالة في التقلب وفي مخزون صانع السوق الحالي:

βσQ\beta \propto \sigma \cdot |Q|

حيث σ\sigma هو تقلب الأصل وQQ هو مخزون صانع السوق الحالي. ومع نمو المخزون، يوسّع صانع السوق الفرق على الجانب المعرّض فيه ويضيّقه على الجانب الآخر، وهي تقنية تُسمى انحراف المخزون (inventory skewing).

3. تكلفة الاصطفاء العكسي (γ\gamma)

هذا أخطر المكونات. فالمتداولون المطّلعون — أولئك الذين يملكون معلومات متفوقة عن تحركات سعرية وشيكة — يقتنصون منهجياً العروض القديمة. وتساوي تكلفة الاصطفاء العكسي الخسارة المتوقعة لكل صفقة أمام الأطراف المقابلة المطّلعة. وقد نمذجها كوبلاند وغالاي (1983) بوصفها قيمة خيار مجاني يمنحه صانع السوق للمتداولين المطّلعين. وصاغها غلوستن وميلغروم (1985) صياغة رسمية بوصفها التنقيح البايزي في معتقدات صانع السوق بعد ملاحظة صفقة:

γ=E[Vtrade]Mt\gamma = E[V | \text{trade}] - M_t

حيث VV هي القيمة الأساسية الحقيقية. وفي الأسواق ذات السيولة العالية، قد يمثّل الاصطفاء العكسي ما بين 30 و60٪ من إجمالي الفرق.

التفكيك الكامل

يمكن كتابة نصف الفرق المعروض على النحو الآتي:

S2=α+β+γ\frac{S}{2} = \alpha + \beta + \gamma

مع التعبير عن α\alpha وβ\beta وγ\gamma جميعها كتكاليف لكل جانب (نصف فرق) — وهذا ما يُبقي المحاسبة متسقة. وقد اقترح هوانغ وستول (1997) منهجاً اقتصادياً قياسياً لتقدير هذه المكونات من بيانات الصفقات والعروض. والبصيرة الأساسية: تكاليف معالجة الأوامر تنشئ أرضية فرق ثابتة، وتكاليف المخزون تنشئ فرقاً يتغير مع المركز والتقلب، وتكاليف الاصطفاء العكسي تنشئ فرقاً يتغير مع عدم تماثل المعلومات.

نموذج رول الضمني للفرق

نموذج رول الضمني للفرق: أسعار التنفيذ ترتد بين الطلب والعرض، تاركةً بصمة تغاير تسلسلي سالب

قبل أن تصبح البيانات عالية التردد متاحة على نطاق واسع، اقترح ريتشارد رول (1984) منهجاً أنيقاً لتقدير الفرق الفعّال باستخدام أسعار التنفيذ وحدها. وبصيرته: في سوق كفؤ، يُحدث ارتداد العرض-الطلب تغايراً تسلسلياً سالباً في تغيرات السعر، حتى عندما لا توجد معلومات جديدة.

النموذج

افترض أن القيمة الأساسية VtV_t تتبع سيراً عشوائياً:

Vt=Vt1+ut,uti.i.d.(0,σu2)V_t = V_{t-1} + u_t, \quad u_t \sim \text{i.i.d.}(0, \sigma_u^2)

سعر التنفيذ الملحوظ يرتد بين الطلب والعرض:

Pt=Vt+S2dtP_t = V_t + \frac{S}{2} \cdot d_t

حيث dt{1,+1}d_t \in \{-1, +1\} باحتمال متساوٍ (أي إن عمليات الشراء والبيع متساوية الاحتمال). تغيُّر السعر هو:

ΔPt=ut+S2(dtdt1)\Delta P_t = u_t + \frac{S}{2}(d_t - d_{t-1})

وبحساب التغاير الذاتي من الرتبة الأولى:

Cov(ΔPt,ΔPt1)=S24\text{Cov}(\Delta P_t, \Delta P_{t-1}) = -\frac{S^2}{4}

النموذج مشتق بـوحدات السعر: فالفرق SS يخرج من التغاير الذاتي لـتغيرات السعر، لا للعوائد. وهذا التمييز هو أشيع خطأ تنفيذي على الإطلاق، ونُبقي الكود وفياً له في الأسفل.

مقدّر رول

بالحل من أجل SS:

S^Roll=2Cov(ΔPt,ΔPt1)\hat{S}_{\text{Roll}} = 2\sqrt{-\text{Cov}(\Delta P_t, \Delta P_{t-1})}

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

S^Roll=2sign(γ^1)γ^1\hat{S}_{\text{Roll}}^{*} = 2 \cdot \text{sign}(-\hat{\gamma}_1) \cdot \sqrt{|\hat{\gamma}_1|}

حيث γ^1\hat{\gamma}_1 هو التغاير الذاتي للعينة من الرتبة الأولى.

التنفيذ في بايثون

يعيد المقدّر فرقاً بـوحدات السعر. وللتعبير عنه بنقاط الأساس نقسّم على السعر المتوسط مرة واحدة — لأنه، على خلاف مقدّر فضاء العوائد، لم يُقسَّم بعد على السعر:

import numpy as np
import pandas as pd

def roll_spread(prices: pd.Series, window: int = 200) -> pd.Series:
    """
    Rolling Roll (1984) spread estimator, in PRICE units.

    The model is P_t = V_t + (S/2) d_t with Cov(ΔP_t, ΔP_{t-1}) = -S^2/4,
    so S is recovered from the autocovariance of price CHANGES (diff),
    not returns (pct_change). Using returns rescales the estimate by the
    price level and is wrong by roughly that factor.

    Parameters
    ----------
    prices : pd.Series
        Transaction prices.
    window : int
        Rolling window size (number of price changes).

    Returns
    -------
    pd.Series
        Estimated spread per window, in price units.
    """
    dprice = prices.diff().dropna()
    autocov = dprice.rolling(window).apply(
        lambda x: np.cov(x[:-1], x[1:])[0, 1], raw=True
    )
    return 2.0 * np.sqrt(np.maximum(-autocov, 0.0))


trades = pd.read_parquet("trades.parquet")
trades["roll_spread"] = roll_spread(trades["price"], window=200)

trades["quoted_spread"] = trades["ask"] - trades["bid"]
trades["midprice"] = 0.5 * (trades["ask"] + trades["bid"])
trades["quoted_spread_bps"] = trades["quoted_spread"] / trades["midprice"] * 1e4
trades["roll_spread_bps"] = trades["roll_spread"] / trades["midprice"] * 1e4

فحصٌ سريع للسلامة على سلسلة محاكاة — سير عشوائي أساسي قرب سعر يساوي 100 بفرق حقيقي قدره S=0.10S = 0.10 — يستعيد 0.0999\approx 0.0999 من تغيرات السعر. أما المتغير القائم على العوائد فسيعيد 0.001\approx 0.001، منحرفاً بمقدار مستوى السعر، ثم قسمة ذلك على السعر المتوسط مرة أخرى للحصول على نقاط الأساس يضاعف الخطأ. وإذا فضّلت مقدّراً في فضاء العوائد، فاشتق النموذج في فضاء لوغاريتم السعر واحذف القسمة الثانية على السعر المتوسط؛ اختر اصطلاحاً واحداً واجعل الكود مطابقاً للرياضيات.

قيود نموذج رول

يفترض نموذج رول: (1) كفاءة السوق، (2) غياب عدم تماثل المعلومات، (3) استقلال اتجاه الصفقة وتطابق توزيعه (i.i.d.)، (4) ثبات الفرق. وكل هذه الافتراضات تُنتهك في الممارسة العملية. وقد بيّن هاريس (1990) أن المقدّر منحاز انحيازاً شديداً بسبب متباينة ينسن عند تطبيقه على بيانات صاخبة. وعلى الرغم من هذه القيود، يبقى مقدّر رول مفيداً بوصفه خط أساس سريعاً، وهو مستخدم على نطاق واسع في البحوث المالية التطبيقية.

خصائص التعلم الآلي للتنبؤ بالفرق

خصائص بنية دقيقة مهندسة — التقلب، واختلال توازن تدفق الأوامر، والعمق، وكثافة الصفقات — تغذي مُتنبئاً بالفرق

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

خصائص دفتر الأوامر (المخزون والاصطفاء العكسي)

الخاصية الصيغة تنوب عن
اختلال توازن الدفتر BI=Vb1Va1Vb1+Va1\text{BI} = \frac{V_b^1 - V_a^1}{V_b^1 + V_a^1} الضغط الاتجاهي
السعر المتوسط المرجّح Pw=PaVbVb+Va+PbVaVb+VaP_w = P_a \cdot \frac{V_b}{V_b + V_a} + P_b \cdot \frac{V_a}{V_b + V_a} القيمة العادلة قصيرة الأجل
نسبة العمق (المستويات 1-5) DR5=i=15Vbii=15Vai\text{DR}_5 = \frac{\sum_{i=1}^{5} V_b^i}{\sum_{i=1}^{5} V_a^i} العرض/الطلب متعدد المستويات
ضغط الدفتر BP=i=15Vbiwibi=15Vaiwia\text{BP} = \sum_{i=1}^{5} V_b^i\, w_i^b - \sum_{i=1}^{5} V_a^i\, w_i^a الضغط المرجّح بالمسافة
نسبة الفرق / التيك S/tickS / \text{tick} الضيق نسبةً إلى الحد الأدنى

يستخدم ضغط الدفتر هنا اضمحلالاً بحسب المسافة المطلقة إلى المنتصف، wi=eλPiMw_i = e^{-\lambda |P_i - M|}، بحيث يُحتسب الحجم القريب من نقطة التماس أكثر من الحجم العميق، ويُرجَّح الجانبان كلاهما بدالة موجبة متناقصة. وهذا يتفادى الانحياز البنيوي في الإشارة الناتج عن قسمة الحجم على المسافة المُؤشَّرة PiMP_i - M (التي تكون سالبة على جانب الطلب، وموجبة على جانب العرض، وتتفجر مع اقتراب أحد المستويات من المنتصف). اختر λ\lambda من عمق الدفتر النموذجي، أو استبدل الأسّي بأي وزن موجب w(PiM)w(|P_i - M|) يتناقص مع المسافة.

خصائص تدفق الصفقات (الاصطفاء العكسي)

الخاصية الصيغة تنوب عن
اختلال توازن الصفقات TIn=i=1ndivii=1nvi\text{TI}_{n} = \frac{\sum_{i=1}^{n} d_i \cdot v_i}{\sum_{i=1}^{n} v_i} صافي التدفق المطّلع
VPIN الاحتمال المتزامن مع الحجم للتداول المطّلع السُّمّية (toxicity)
لامبدا كايل انحدار ΔM\Delta M على الحجم المُؤشَّر أثر السعر لكل وحدة
تردد الصفقات الكبيرة عدد الصفقات >kmedian> k \cdot \text{median} في النافذة النشاط المؤسسي

خصائص التقلب (تكلفة المخزون)

الخاصية الصيغة تنوب عن
التقلب المحقق σrv=(ΔlogM)2\sigma_{\text{rv}} = \sqrt{\sum (\Delta \log M)^2} المخاطرة قصيرة الأجل
تقلب غارمان-كلاس 12(logH/L)2(2ln21)(logC/O)2\frac{1}{2}(\log H/L)^2 - (2\ln 2 - 1)(\log C/O)^2 التقلب القائم على النطاق
تقلب التقلب الانحراف المعياري المتدحرج لـσrv\sigma_{\text{rv}} عدم يقين النظام
الارتباط الذاتي للعوائد ρ1(ΔM)\rho_1(\Delta M) الزخم / الارتداد إلى المتوسط

خصائص نظام السوق

الخاصية الوصف تنوب عن
ترميز وقت اليوم sin(2πt/T),cos(2πt/T)\sin(2\pi t / T), \cos(2\pi t / T) الموسمية خلال اليوم
الثواني منذ آخر صفقة الفجوة الزمنية مستوى النشاط
الارتباط بين الأصول الارتباط المتدحرج مع المؤشر/البيتكوين المخاطرة النظامية
معدل التمويل (العملات المشفرة) معدل تمويل العقود الدائمة التمركز بالرافعة المالية

التعزيز التدرجي للتنبؤ بالفرق

تُعدّ الأشجار المعزَّزة تدرجياً (XGBoost وLightGBM وCatBoost) عماد التنبؤ الجدولي في التمويل الكمّي. فهي تتعامل مع أنواع الخصائص المختلطة، وتلتقط التفاعلات غير الخطية، وتتطلب أدنى قدر من المعالجة المسبقة، وتتدرب بسرعة على ملايين الصفوف — شريطة أن يكون بناء الخصائص نفسه مُمتجهاً (vectorized) (انظر ملاحظة الارتباط الذاتي أدناه).

صياغة المشكلة

نصوغ التنبؤ بالفرق بوصفه مهمة انحدار. الهدف هو متوسط الفرق المعروض المرجّح زمنياً خلال الثواني τ\tau التالية:

yt=1τtt+τS(u)duy_t = \frac{1}{\tau} \int_{t}^{t+\tau} S(u) \, du

وفي الممارسة العملية، نقرّب ذلك بمتوسط الفرق المرجّح بالحجم خلال اللقطات NN التالية:

yt=i=1NSt+iVt+ii=1NVt+iy_t = \frac{\sum_{i=1}^{N} S_{t+i} \cdot V_{t+i}}{\sum_{i=1}^{N} V_{t+i}}

هذا الهدف هو نافذة أمامية بطول NN (أو horizon)، ما يعني أن الصفوف المتجاورة تتقاسم نوافذ مستقبلية متداخلة. وهذا التداخل يُسرّب المعلومات عبر تقسيم تدريب/تحقق ساذج — ونعالجه صراحةً في كود التدريب أدناه.

المسار الكامل

import lightgbm as lgb
import numpy as np
import pandas as pd
from sklearn.metrics import mean_absolute_error, r2_score

def build_features(df: pd.DataFrame) -> pd.DataFrame:
    """Build spread-prediction features from L2 order book snapshots."""
    f = pd.DataFrame(index=df.index)

    f["imb1"] = (df["bid_vol_1"] - df["ask_vol_1"]) / (
        df["bid_vol_1"] + df["ask_vol_1"] + 1e-9
    )

    bid_depth = df[[f"bid_vol_{i}" for i in range(1, 6)]].sum(axis=1)
    ask_depth = df[[f"ask_vol_{i}" for i in range(1, 6)]].sum(axis=1)
    f["depth_imb5"] = (bid_depth - ask_depth) / (bid_depth + ask_depth + 1e-9)

    mid = 0.5 * (df["ask_1"] + df["bid_1"])
    f["spread_bps"] = (df["ask_1"] - df["bid_1"]) / mid * 1e4
    f["log_spread"] = np.log1p(df["ask_1"] - df["bid_1"])

    log_ret = np.log(mid / mid.shift(1))
    f["rvol_50"] = log_ret.rolling(50).std()
    f["rvol_200"] = log_ret.rolling(200).std()

    if "trade_sign" in df.columns and "trade_vol" in df.columns:
        signed_vol = df["trade_sign"] * df["trade_vol"]
        total_vol = df["trade_vol"].rolling(50).sum()
        f["tfi_50"] = signed_vol.rolling(50).sum() / (total_vol + 1e-9)

    lag1 = log_ret.shift(1)
    f["ret_autocorr"] = log_ret.rolling(100).corr(lag1)

    if isinstance(df.index, pd.DatetimeIndex):
        seconds = df.index.hour * 3600 + df.index.minute * 60 + df.index.second
        f["tod_sin"] = np.sin(2 * np.pi * seconds / 86400)
        f["tod_cos"] = np.cos(2 * np.pi * seconds / 86400)

    for lag in [1, 5, 10, 50]:
        f[f"spread_lag_{lag}"] = f["spread_bps"].shift(lag)

    return f.dropna()


def build_target(df: pd.DataFrame, horizon: int = 10) -> pd.Series:
    """Forward mean spread over the next `horizon` snapshots (in bps).

    target[t] = mean(spread_bps[t+1 .. t+horizon]). Note that consecutive
    targets share an overlapping forward window of length `horizon`, which
    is why the CV below purges a gap of `horizon` rows around each fold.
    """
    mid = 0.5 * (df["ask_1"] + df["bid_1"])
    spread_bps = (df["ask_1"] - df["bid_1"]) / mid * 1e4
    fwd = spread_bps.shift(-1).rolling(horizon).mean().shift(-(horizon - 1))
    return fwd


def purged_walk_forward(n: int, n_splits: int, horizon: int):
    """Expanding-window splits with a purge/embargo gap of `horizon` rows.

    Because each target spans `horizon` future snapshots, rows straddling a
    train/val boundary share overlapping target windows. Dropping a gap of
    `horizon` rows between train and validation removes that leakage
    (Lopez de Prado-style purging). Without it, validation R²/MAE are
    optimistically biased by the target overlap.
    """
    fold_size = n // (n_splits + 1)
    for k in range(1, n_splits + 1):
        train_end = fold_size * k
        val_start = train_end + horizon       # embargo gap
        val_end = val_start + fold_size
        if val_end > n:
            break
        train_idx = np.arange(0, train_end - horizon)   # purge gap
        val_idx = np.arange(val_start, val_end)
        yield train_idx, val_idx


def train_spread_model(features: pd.DataFrame, target: pd.Series, horizon: int = 10):
    """Train LightGBM with purged, embargoed walk-forward validation."""
    common = features.index.intersection(target.dropna().index)
    X = features.loc[common].reset_index(drop=True)
    y = target.loc[common].reset_index(drop=True)

    models, scores = [], []
    params = {
        "objective": "mae",
        "learning_rate": 0.05,
        "num_leaves": 63,
        "min_child_samples": 100,
        "subsample": 0.8,
        "colsample_bytree": 0.8,
        "reg_alpha": 0.1,
        "reg_lambda": 1.0,
        "verbose": -1,
    }

    for fold, (train_idx, val_idx) in enumerate(
        purged_walk_forward(len(X), n_splits=5, horizon=horizon)
    ):
        X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
        y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx]

        ds_tr = lgb.Dataset(X_tr, y_tr)
        ds_val = lgb.Dataset(X_val, y_val, reference=ds_tr)

        model = lgb.train(
            params,
            ds_tr,
            num_boost_round=2000,
            valid_sets=[ds_val],
            callbacks=[lgb.early_stopping(50), lgb.log_evaluation(200)],
        )
        preds = model.predict(X_val)
        mae = mean_absolute_error(y_val, preds)
        r2 = r2_score(y_val, preds)
        print(f"Fold {fold}: MAE={mae:.4f} bps, R²={r2:.4f}")
        models.append(model)
        scores.append({"mae": mae, "r2": r2})

    return models[-1], scores

التفصيل الحاسم هو فجوة التطهير/الحظر (purge/embargo gap). فهدف المتوسط الأمامي يعني أن الصفوف المتتالية تتداخل بما يصل إلى horizon لقطة، فيتيح تقسيم TimeSeriesSplit البسيط لصفوف التحقق أن تتقاسم نوافذ مستقبلية مع صفوف التدريب — مسرّباً الإجابة ومضخّماً قيمة R² في التحقق. وإسقاط فجوة لا تقل عن horizon صف على جانبي كل حدّ فاصل بين الطيّات (تطهير k-طيّة على نمط لوبيز دي برادو) يزيل ذلك الانحياز. وهذا ينطبق على مسار التعزيز التدرجي تماماً كما ينطبق على التعلم العميق، رغم أن التسرب يُناقَش غالباً أكثر في نماذج التسلسل.

تحليل أهمية الخصائص

من المزايا الرئيسية للنماذج القائمة على الأشجار قابليتُها للتفسير. وبعد التدريب، افحص قيم SHAP لفهم الخصائص التي تقود التنبؤ بالفرق:

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val)
shap.summary_plot(shap_values, X_val, max_display=15)

نتائج نموذجية عبر فئات الأصول:

  1. الفرق المتأخر (spread_lag_1\text{spread\_lag\_1}) هو دائماً تقريباً أهم خاصية — فالفروق مرتبطة ذاتياً ارتباطاً عالياً. وهذا أيضاً سبب ظهور قيمة R² الرئيسية مرتفعة: فالكثير من النتيجة ليس سوى استمرارية، لذا قايس دائماً مقابل خط أساس AR/EWMA (مزيد عن ذلك أدناه).
  2. التقلب المحقق هو ثاني أهم خاصية — فتقلب اليوم والفروق مرتبطان إيجابياً ارتباطاً قوياً، آنياً وديناميكياً معاً.
  3. اختلال توازن الدفتر يهمّ أكثر خلال الفترات المتقلبة — فهو يشير إلى تحركات اتجاهية وشيكة.
  4. اختلال توازن تدفق الصفقات يلتقط الاصطفاء العكسي قصير الأجل — فاندفاع تدفق أحادي الجانب يتنبأ باتساع الفرق.
  5. وقت اليوم يلتقط النمط حرف U خلال اليوم (أوسع عند الفتح/الإغلاق، أضيق في منتصف اليوم).

اعتبارات المعاملات الفائقة

للتنبؤ بالفرق تحديداً:

  • استخدم خسارة MAE أو خسارة هوبر بدلاً من MSE. فتوزيعات الفروق ملتوية نحو اليمين مع قيم متطرفة عرضية (خلال أحداث الأخبار). وMAE أكثر متانة.
  • اضبط min_child_samples على قيمة عالية (100 فأكثر) لمنع النموذج من ملاءمة ضجيج البنية الدقيقة في اللقطات المنفردة.
  • استخدم subsample < 1.0 لإزالة الارتباط بين الأشجار وتحسين التعميم عبر أنظمة التقلب المختلفة.

مقاربات التعلم العميق

في حين يتفوق التعزيز التدرجي على الخصائص الجدولية، يستطيع التعلم العميق أن يتعلم التمثيلات مباشرةً من بيانات دفتر الأوامر الخام. وقد أثبتت بنيتان فاعليتهما في مهام التنبؤ المتعلقة بالفرق.

البنية 1: CNN-LSTM للقطات دفتر الأوامر

تستخدم بنية DeepLOB (تشانغ وآخرون 2019) التفافات مكدّسة صغيرة النواة — ووحدة Inception — لاستخراج الأنماط المكانية عبر مستويات دفتر الأوامر مع الحفاظ على تلك البنية المكانية، تتبعها طبقات LSTM لنمذجة التبعيات الزمنية. والخيار التصميمي المهم هو عدم تجميع محور المستويات عالمياً (global-pool) قبل الطبقة المتكررة: فعل ذلك يطمس البنية بالضبط عبر المستويات التي يُفترض أن تلتقطها الالتفافات.

للتنبؤ بالفرق، المدخل تنسور بشكل (T,L,F)(T, L, F):

  • TT = عدد الخطوات الزمنية (مثلاً 100 لقطة)
  • LL = عدد مستويات السعر (مثلاً 10 طلب + 10 عرض = 20)
  • FF = الخصائص لكل مستوى (السعر، الحجم، عدد الأوامر)

النموذج أدناه يبقي خريطة الخصائص الالتفافية عبر المستويات ويفلطحها (flattens) في مدخل LSTM (input_size = 16 * L)، بدلاً من حساب متوسط بُعد المستويات في 16 متوسطاً للقنوات:

import torch
import torch.nn as nn


class SpreadPredictor(nn.Module):
    """
    CNN-LSTM model for bid-ask spread prediction from L2 order book.

    Input: (batch, seq_len, n_levels, n_features)
    Output: (batch, 1) — predicted spread in bps
    """

    def __init__(
        self,
        n_levels: int = 20,
        n_features: int = 3,
        seq_len: int = 100,
        hidden_dim: int = 64,
        n_lstm_layers: int = 2,
        dropout: float = 0.2,
    ):
        super().__init__()
        self.seq_len = seq_len
        self.n_levels = n_levels

        self.conv = nn.Sequential(
            nn.Conv1d(n_features, 32, kernel_size=3, padding=1),
            nn.BatchNorm1d(32),
            nn.LeakyReLU(0.1),
            nn.Conv1d(32, 16, kernel_size=3, padding=1),
            nn.BatchNorm1d(16),
            nn.LeakyReLU(0.1),
        )
        conv_out_dim = 16 * n_levels  # flattened (channels × levels)

        self.lstm = nn.LSTM(
            input_size=conv_out_dim,
            hidden_size=hidden_dim,
            num_layers=n_lstm_layers,
            batch_first=True,
            dropout=dropout,
        )

        self.head = nn.Sequential(
            nn.Linear(hidden_dim, 32),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(32, 1),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Parameters
        ----------
        x : Tensor of shape (batch, seq_len, n_levels, n_features)

        Returns
        -------
        Tensor of shape (batch, 1) — predicted spread
        """
        batch, T, L, F = x.shape

        x = x.reshape(batch * T, L, F).permute(0, 2, 1)
        x = self.conv(x)                  # (batch * T, 16, L)
        x = x.reshape(batch, T, 16 * L)   # (batch, T, 16 * L) — keep levels

        lstm_out, _ = self.lstm(x)        # (batch, T, hidden_dim)
        last_hidden = lstm_out[:, -1, :]  # (batch, hidden_dim)

        return self.head(last_hidden)     # (batch, 1)

وإذا أردت فعلاً تجميعاً على محور المستويات للتحكم في عدد المعاملات، فاستخدم تجميعاً بخطوة (strided) أو تجميعاً متعلَّماً يحتفظ بأكثر من موضع واحد — لا AdaptiveAvgPool1d(1) الذي يحسب متوسط كل مستوى في رقم واحد ويُلقي بالإشارة المكانية بعيداً.

البنية 2: مُرمِّز المُحوِّل (Transformer Encoder)

تستطيع المحوِّلات (Transformers) التقاط التبعيات بعيدة المدى في تسلسلات دفتر الأوامر دون عنق الزجاجة التسلسلي لـLSTM. وللتنبؤ بالفرق، يعمل مُرمِّز محوِّل خفيف الوزن جيداً:

class TransformerSpreadPredictor(nn.Module):
    """Transformer encoder for spread prediction from order book sequences."""

    def __init__(
        self,
        input_dim: int = 40,   # 20 levels * 2 features (price_offset, volume)
        d_model: int = 64,
        nhead: int = 4,
        n_layers: int = 3,
        seq_len: int = 100,
        dropout: float = 0.1,
    ):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, d_model)
        self.pos_encoding = nn.Parameter(torch.randn(1, seq_len, d_model) * 0.02)

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=nhead,
            dim_feedforward=d_model * 4,
            dropout=dropout,
            batch_first=True,
            activation="gelu",
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)
        self.head = nn.Sequential(
            nn.LayerNorm(d_model),
            nn.Linear(d_model, 1),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        x: (batch, seq_len, input_dim) — flattened order book snapshots
        """
        x = self.input_proj(x) + self.pos_encoding[:, : x.size(1), :]
        x = self.encoder(x)
        return self.head(x[:, -1, :])

اعتبارات التدريب

  1. التطبيع (Normalization): طبّع الأسعار بوصفها إزاحات عن السعر المتوسط (بالتيكات أو بنقاط الأساس). وطبّع الأحجام بمتوسطها المتدحرج. فالأسعار والأحجام الخام تسبب عدم استقرار في التدريب.

  2. دالة الخسارة: استخدم خسارة هوبر (δ=1.0\delta = 1.0) للتعامل مع قفزات الفرق:

Lδ(y,y^)={12(yy^)2if yy^δδyy^12δ2otherwiseL_\delta(y, \hat{y}) = \begin{cases} \frac{1}{2}(y - \hat{y})^2 & \text{if } |y - \hat{y}| \le \delta \\ \delta |y - \hat{y}| - \frac{1}{2}\delta^2 & \text{otherwise} \end{cases}

  1. أخذ عينات النوافذ والتسرب: استخدم نوافذ غير متداخلة للتدريب، و — تماماً كما في مسار التعزيز التدرجي — طهّر/احظر فجوة لا تقل عن horizon لقطة بين التدريب والتحقق. فكل من هدف المتوسط الأمامي ونوافذ المدخلات المتداخلة يسرّب معلومات مستقبلية عبر حدود التقسيم ويضخّم الأداء الظاهري.

  2. التكيف عبر الإنترنت (Online adaptation): في الإنتاج، اضبط النموذج بدقة دورياً على بيانات حديثة (آخر ساعة إلى ساعتين) بمعدل تعلم صغير. فبنية السوق الدقيقة تتغير خلال اليوم، وقد يؤدي نموذجٌ مُدرَّب على بيانات الصباح أداءً دون المستوى في الظهيرة.

متى تستخدم التعلم العميق مقابل التعزيز التدرجي

المعيار التعزيز التدرجي التعلم العميق
نوع المدخلات خصائص جدولية تسلسلات دفتر أوامر خام
حجم بيانات التدريب يعمل مع 100 ألف صف فأكثر يحتاج إلى مليون صف فأكثر
هندسة الخصائص يدوية (جهد عالٍ، تحكم عالٍ) متعلَّمة (جهد أقل، أقل قابلية للتفسير)
زمن استدلال أحادي الرقم ميكروثوانٍ أحادية الرقم مع مُتنبئ مُترجَم؛ عشرات الميكروثواني من بايثون مئات الميكروثواني على GPU
قابلية التفسير عالية (SHAP) منخفضة (خرائط الانتباه)
التكيف مع النظام إعادة تدريب / تحديث عبر الإنترنت ضبط دقيق على بيانات حديثة
مهارة الفرق قصير الأفق مماثلة على نطاق واسع للتعلم العميق تنمو الأفضلية على الآفاق الأطول / البيانات الأكبر

نتجنب عمداً ذكر أرقام R² محددة: فدقة التنبؤ بالفرق تعتمد اعتماداً كبيراً على الأفق، والأصل، ومقدار النتيجة الذي هو ببساطة ارتباط ذاتي للفرق. فقد يسجّل نموذج قيمة R² خام مبهرة في حين لا يضيف شيئاً يُذكر فوق سطر EWMA واحد. أبلغ عن المهارة فوق خط أساس AR/EWMA على البيانات نفسها، مع ذكر الأفق، بدلاً من قيمة R² رئيسية. وبالمثل، تعامل مع أرقام زمن الاستجابة بوصفها معتمدة على التنفيذ: فنموذج LightGBM بـ2000 جولة و63 ورقة يتنبأ بصف واحد في عشرات الميكروثواني من بايثون، ولا يصل إلى بضعة ميكروثوانٍ إلا مع مُتنبئ مُترجَم/C++.

في الممارسة العملية، تستخدم كثير من أنظمة الإنتاج مقاربة من مرحلتين: نموذج تعزيز تدرجي سريع للعرض في الوقت الفعلي (حسّاس لزمن الاستجابة)، ونموذج تعلم عميق يعمل بشكل غير متزامن لتعديل معاملات نموذج التعزيز أو لتوفير إشارة ثانوية.

من التنبؤ إلى العرض

تحويل فرق متنبَّأ به إلى عروض طلب/عرض حيّة، مُنحرفة بحسب المخزون حول سعر منتصف عادل

لا تكون قيمة التنبؤ بالفرق إلا إذا تُرجم إلى عروض أفضل. وفيما يلي قاعدة عرض مبسّطة تستخدم الفرق المتنبَّأ به:

def compute_quotes(
    mid: float,
    predicted_spread_bps: float,
    inventory: float,
    max_inventory: float,
    skew_factor: float = 0.5,
    min_spread_bps: float = 1.0,
) -> tuple[float, float]:
    """
    Compute bid/ask quotes from predicted spread and inventory.

    Parameters
    ----------
    mid : float
        Current midprice.
    predicted_spread_bps : float
        Model-predicted spread in basis points.
    inventory : float
        Current inventory (positive = long).
    max_inventory : float
        Maximum allowed inventory.
    skew_factor : float
        How aggressively to skew quotes toward inventory neutrality.
    min_spread_bps : float
        Minimum spread floor (covers order processing costs).

    Returns
    -------
    (bid, ask) : tuple[float, float]
    """
    spread_bps = max(predicted_spread_bps, min_spread_bps)
    half_spread = mid * spread_bps / 2e4

    inv_ratio = inventory / max_inventory  # in [-1, 1]
    skew = skew_factor * inv_ratio * half_spread

    bid = mid - half_spread - skew
    ask = mid + half_spread - skew

    return bid, ask

عندما يكون المخزون طويلاً (Q>0Q > 0)، يخفّض الانحراف كلاً من الطلب والعرض. ومن منظور واحد متسق: العرض الأدنى يجعل الشراء منا أرخص للآخذين، ما يفرّغ مخزوننا الطويل؛ والطلب الأدنى يقلّل احتمال أن يصيبنا البائعون، فيبطئ مزيداً من التراكم. ويتحكم الفرق المتنبَّأ به في العرض الكلي — متسعاً عندما يتوقع النموذج تقلباً أو اصطفاءً عكسياً، ومتضيقاً عندما تكون الظروف هادئة.

التقييم واختبار الأداء بالبيانات التاريخية

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

مقاييس التنبؤ بالفرق

إلى جانب مقاييس الانحدار القياسية (MAE وR2R^2)، قيّم تنبؤات الفرق بمقاييس مهمة لصناعة السوق:

  • المهارة فوق خط أساس: أبلغ دائماً عن MAE/R² نسبةً إلى تنبؤ AR(1) أو EWMA للفروق الأخيرة. ولأن الفروق شديدة الاستمرارية، تهيمن قيمة الارتباط الذاتي على النتيجة المطلقة؛ والتحسن فوق خط أساس تافه وحده هو ما يعكس محتوى تنبؤياً حقيقياً.
  • دقة الاتجاه: هل يتنبأ النموذج بشكل صحيح بما إذا كان الفرق سيتسع أم سيضيق؟ نموذج بقيمة MAE متوسطة لكن بدقة اتجاه عالية قد يظل مربحاً.
  • تغطية الذيول: هل يتنبأ النموذج بقفزات الفرق؟ احسب MAE على حدة لأعلى 5٪ من قيم الفرق — فهنا تتركّز خسائر الاصطفاء العكسي.
  • المعايرة (Calibration): ارسم كميات الفرق المتنبَّأ بها مقابل المحققة. فتنبؤ النموذج جيد المعايرة عند المئين التسعين ينبغي أن يطابق المئين التسعين للفروق المحققة.

التقييم القائم على الأرباح والخسائر

في نهاية المطاف، المقياس الوحيد الذي يهمّ هو الأرباح والخسائر المحققة. اختبر الحلقة الكاملة بالبيانات التاريخية:

  1. عند كل طابع زمني، تنبّأ بالفرق
  2. احسب العروض باستخدام الفرق المتنبَّأ به + انحراف المخزون
  3. حاكِ التنفيذات مقابل الصفقات التاريخية
  4. تتبّع المخزون والأرباح والخسائر المحققة ونسبة شارب

قارن مقابل خطوط الأساس: (أ) فرق ثابت (وسيط السلسلة الزمنية)، (ب) EWMA للفروق الأخيرة، و(ج) مقدّر رول.

الخلاصة

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

بالنسبة لنظام صناعة سوق إنتاجي، التوصية العملية متعددة الطبقات:

  1. استخدم تفكيك هوانغ-ستول بدون اتصال لفهم مكونات فرقك ومعايرة حدود المخاطرة
  2. استخدم مقدّر رول بوصفه فحص سلامة وللأدوات التي تفتقر فيها إلى بيانات دفتر الأوامر
  3. انشر نموذج LightGBM للتنبؤ بالفرق في الوقت الفعلي — فهو سريع، وقابل للتفسير، ومتين — مع تحقق متدرج للأمام مُطهَّر ومقايسة AR/EWMA
  4. شغّل نموذج CNN-LSTM أو محوِّل في حلقة ثانوية لكشف تغيرات النظام وتعديل النموذج الأساسي

الفرق ليس رقماً — إنه إشارة. وكلما نمذجته بشكل أفضل (وكلما قِست ذلك النموذج بأمانة أكبر)، أمكنك تسعير توفير السيولة بدقة أكبر.


هذا المقال جزء من سلسلة marketmaker.cc حول صناعة السوق الخوارزمية والبنية الدقيقة.

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

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

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

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