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

تفكّك أدبيات البنية الدقيقة للسوق (ستول 1978، غلوستن وميلغروم 1985، هوانغ وستول 1997) فرق السعر بين العرض والطلب إلى ثلاثة مكونات اقتصادية.
1. تكلفة معالجة الأوامر ()
هذه هي تكلفة تقديم خدمة صناعة السوق لكل جانب مُنفَّذ: الرسم الذي يدفعه صانع السوق فعلياً، إضافة إلى البنية التحتية التقنية، والامتثال التنظيمي، وتكلفة الفرصة لرأس المال المنشور. وكان ديمستز (1968) وتينيتش (1972) أول من صاغ هذا المكوّن صياغة رسمية.
والفارق الجوهري هو مَن يدفع أيّ رسم. صانع السوق الذي يعرض بشكل سلبي يدفع رسم صانع السوق على عمليات تنفيذه الخاصة — وفي كثير من المنصات يكون حسماً مرتجعاً (rebate)، أي سالباً. وهو لا يدفع رسم الآخذ على تلك التنفيذات السلبية؛ بل الطرف المقابل الذي يعبر الفرق هو من يدفع . وعليه فإن تكلفة معالجة الأوامر لكل جانب لدى صانع السوق هي:
حيث مُؤشَّر (الحسم المرتجع يخفّض وقد يجعله سالباً) و يغطي الاتصال، والموقع المشترك (colocation)، والحوسبة. وفي الأسواق الإلكترونية الحديثة تقلّص هذا المكوّن تقلصاً كبيراً — إلى ما دون السنت في الأسهم، وبضع نقاط أساس أو حسماً مرتجعاً صافياً في العملات المشفرة.
ويهمّ رسم الآخذ لسبب مختلف: فهو يحدد حداً أدنى لمدى ضيق الفرق الكامل بشكل مربح، لأن الآخذ الذي يعبر يدفع فوق الفرق. وإذا أردت أرضية فرق تُبقي عروضك جذابة اقتصادياً مقارنةً بتكلفة الآخذ تلك، فحفّزها على حدة بدلاً من طيّ داخل تكلفة صانع السوق ذاته. فالخلط بينهما يحتسب رسم دورة ذهاب وإياب مرتين داخل نصف فرق واحد.
2. تكلفة الاحتفاظ بالمخزون ()
عندما يراكم صانع السوق مركزاً اتجاهياً (طويلاً أو قصيراً)، فإنه يتحمل مخاطرة السعر. ويعوّض مكوّن المخزون عن هذه المخاطرة. وقد نمذجها ستول (1978) وأميهود وميندلسون (1980) بوصفها دالة في التقلب وفي مخزون صانع السوق الحالي:
حيث هو تقلب الأصل و هو مخزون صانع السوق الحالي. ومع نمو المخزون، يوسّع صانع السوق الفرق على الجانب المعرّض فيه ويضيّقه على الجانب الآخر، وهي تقنية تُسمى انحراف المخزون (inventory skewing).
3. تكلفة الاصطفاء العكسي ()
هذا أخطر المكونات. فالمتداولون المطّلعون — أولئك الذين يملكون معلومات متفوقة عن تحركات سعرية وشيكة — يقتنصون منهجياً العروض القديمة. وتساوي تكلفة الاصطفاء العكسي الخسارة المتوقعة لكل صفقة أمام الأطراف المقابلة المطّلعة. وقد نمذجها كوبلاند وغالاي (1983) بوصفها قيمة خيار مجاني يمنحه صانع السوق للمتداولين المطّلعين. وصاغها غلوستن وميلغروم (1985) صياغة رسمية بوصفها التنقيح البايزي في معتقدات صانع السوق بعد ملاحظة صفقة:
حيث هي القيمة الأساسية الحقيقية. وفي الأسواق ذات السيولة العالية، قد يمثّل الاصطفاء العكسي ما بين 30 و60٪ من إجمالي الفرق.
التفكيك الكامل
يمكن كتابة نصف الفرق المعروض على النحو الآتي:
مع التعبير عن و و جميعها كتكاليف لكل جانب (نصف فرق) — وهذا ما يُبقي المحاسبة متسقة. وقد اقترح هوانغ وستول (1997) منهجاً اقتصادياً قياسياً لتقدير هذه المكونات من بيانات الصفقات والعروض. والبصيرة الأساسية: تكاليف معالجة الأوامر تنشئ أرضية فرق ثابتة، وتكاليف المخزون تنشئ فرقاً يتغير مع المركز والتقلب، وتكاليف الاصطفاء العكسي تنشئ فرقاً يتغير مع عدم تماثل المعلومات.
نموذج رول الضمني للفرق

قبل أن تصبح البيانات عالية التردد متاحة على نطاق واسع، اقترح ريتشارد رول (1984) منهجاً أنيقاً لتقدير الفرق الفعّال باستخدام أسعار التنفيذ وحدها. وبصيرته: في سوق كفؤ، يُحدث ارتداد العرض-الطلب تغايراً تسلسلياً سالباً في تغيرات السعر، حتى عندما لا توجد معلومات جديدة.
النموذج
افترض أن القيمة الأساسية تتبع سيراً عشوائياً:
سعر التنفيذ الملحوظ يرتد بين الطلب والعرض:
حيث باحتمال متساوٍ (أي إن عمليات الشراء والبيع متساوية الاحتمال). تغيُّر السعر هو:
وبحساب التغاير الذاتي من الرتبة الأولى:
النموذج مشتق بـوحدات السعر: فالفرق يخرج من التغاير الذاتي لـتغيرات السعر، لا للعوائد. وهذا التمييز هو أشيع خطأ تنفيذي على الإطلاق، ونُبقي الكود وفياً له في الأسفل.
مقدّر رول
بالحل من أجل :
عندما يكون التغاير الذاتي للعينة موجباً (وهو ما يحدث كثيراً في الممارسة بسبب الضجيج أو الزخم)، يصبح المقدّر غير معرَّف. والإصلاح الشائع هو ضبط التقدير على الصفر أو استخدام الجذر المُؤشَّر:
حيث هو التغاير الذاتي للعينة من الرتبة الأولى.
التنفيذ في بايثون
يعيد المقدّر فرقاً بـوحدات السعر. وللتعبير عنه بنقاط الأساس نقسّم على السعر المتوسط مرة واحدة — لأنه، على خلاف مقدّر فضاء العوائد، لم يُقسَّم بعد على السعر:
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 بفرق حقيقي قدره — يستعيد من تغيرات السعر. أما المتغير القائم على العوائد فسيعيد ، منحرفاً بمقدار مستوى السعر، ثم قسمة ذلك على السعر المتوسط مرة أخرى للحصول على نقاط الأساس يضاعف الخطأ. وإذا فضّلت مقدّراً في فضاء العوائد، فاشتق النموذج في فضاء لوغاريتم السعر واحذف القسمة الثانية على السعر المتوسط؛ اختر اصطلاحاً واحداً واجعل الكود مطابقاً للرياضيات.
قيود نموذج رول
يفترض نموذج رول: (1) كفاءة السوق، (2) غياب عدم تماثل المعلومات، (3) استقلال اتجاه الصفقة وتطابق توزيعه (i.i.d.)، (4) ثبات الفرق. وكل هذه الافتراضات تُنتهك في الممارسة العملية. وقد بيّن هاريس (1990) أن المقدّر منحاز انحيازاً شديداً بسبب متباينة ينسن عند تطبيقه على بيانات صاخبة. وعلى الرغم من هذه القيود، يبقى مقدّر رول مفيداً بوصفه خط أساس سريعاً، وهو مستخدم على نطاق واسع في البحوث المالية التطبيقية.
خصائص التعلم الآلي للتنبؤ بالفرق

لتجاوز النماذج الساكنة، نحتاج إلى خصائص تلتقط المحركات الديناميكية لتباين الفرق. وفيما يلي تصنيف للخصائص منظَّم بحسب مكوّن الفرق الذي تنوب عنه.
خصائص دفتر الأوامر (المخزون والاصطفاء العكسي)
| الخاصية | الصيغة | تنوب عن |
|---|---|---|
| اختلال توازن الدفتر | الضغط الاتجاهي | |
| السعر المتوسط المرجّح | القيمة العادلة قصيرة الأجل | |
| نسبة العمق (المستويات 1-5) | العرض/الطلب متعدد المستويات | |
| ضغط الدفتر | الضغط المرجّح بالمسافة | |
| نسبة الفرق / التيك | الضيق نسبةً إلى الحد الأدنى |
يستخدم ضغط الدفتر هنا اضمحلالاً بحسب المسافة المطلقة إلى المنتصف، ، بحيث يُحتسب الحجم القريب من نقطة التماس أكثر من الحجم العميق، ويُرجَّح الجانبان كلاهما بدالة موجبة متناقصة. وهذا يتفادى الانحياز البنيوي في الإشارة الناتج عن قسمة الحجم على المسافة المُؤشَّرة (التي تكون سالبة على جانب الطلب، وموجبة على جانب العرض، وتتفجر مع اقتراب أحد المستويات من المنتصف). اختر من عمق الدفتر النموذجي، أو استبدل الأسّي بأي وزن موجب يتناقص مع المسافة.
خصائص تدفق الصفقات (الاصطفاء العكسي)
| الخاصية | الصيغة | تنوب عن |
|---|---|---|
| اختلال توازن الصفقات | صافي التدفق المطّلع | |
| VPIN | الاحتمال المتزامن مع الحجم للتداول المطّلع | السُّمّية (toxicity) |
| لامبدا كايل | انحدار على الحجم المُؤشَّر | أثر السعر لكل وحدة |
| تردد الصفقات الكبيرة | عدد الصفقات في النافذة | النشاط المؤسسي |
خصائص التقلب (تكلفة المخزون)
| الخاصية | الصيغة | تنوب عن |
|---|---|---|
| التقلب المحقق | المخاطرة قصيرة الأجل | |
| تقلب غارمان-كلاس | التقلب القائم على النطاق | |
| تقلب التقلب | الانحراف المعياري المتدحرج لـ | عدم يقين النظام |
| الارتباط الذاتي للعوائد | الزخم / الارتداد إلى المتوسط |
خصائص نظام السوق
| الخاصية | الوصف | تنوب عن |
|---|---|---|
| ترميز وقت اليوم | الموسمية خلال اليوم | |
| الثواني منذ آخر صفقة | الفجوة الزمنية | مستوى النشاط |
| الارتباط بين الأصول | الارتباط المتدحرج مع المؤشر/البيتكوين | المخاطرة النظامية |
| معدل التمويل (العملات المشفرة) | معدل تمويل العقود الدائمة | التمركز بالرافعة المالية |
التعزيز التدرجي للتنبؤ بالفرق
تُعدّ الأشجار المعزَّزة تدرجياً (XGBoost وLightGBM وCatBoost) عماد التنبؤ الجدولي في التمويل الكمّي. فهي تتعامل مع أنواع الخصائص المختلطة، وتلتقط التفاعلات غير الخطية، وتتطلب أدنى قدر من المعالجة المسبقة، وتتدرب بسرعة على ملايين الصفوف — شريطة أن يكون بناء الخصائص نفسه مُمتجهاً (vectorized) (انظر ملاحظة الارتباط الذاتي أدناه).
صياغة المشكلة
نصوغ التنبؤ بالفرق بوصفه مهمة انحدار. الهدف هو متوسط الفرق المعروض المرجّح زمنياً خلال الثواني التالية:
وفي الممارسة العملية، نقرّب ذلك بمتوسط الفرق المرجّح بالحجم خلال اللقطات التالية:
هذا الهدف هو نافذة أمامية بطول (أو 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)
نتائج نموذجية عبر فئات الأصول:
- الفرق المتأخر () هو دائماً تقريباً أهم خاصية — فالفروق مرتبطة ذاتياً ارتباطاً عالياً. وهذا أيضاً سبب ظهور قيمة R² الرئيسية مرتفعة: فالكثير من النتيجة ليس سوى استمرارية، لذا قايس دائماً مقابل خط أساس AR/EWMA (مزيد عن ذلك أدناه).
- التقلب المحقق هو ثاني أهم خاصية — فتقلب اليوم والفروق مرتبطان إيجابياً ارتباطاً قوياً، آنياً وديناميكياً معاً.
- اختلال توازن الدفتر يهمّ أكثر خلال الفترات المتقلبة — فهو يشير إلى تحركات اتجاهية وشيكة.
- اختلال توازن تدفق الصفقات يلتقط الاصطفاء العكسي قصير الأجل — فاندفاع تدفق أحادي الجانب يتنبأ باتساع الفرق.
- وقت اليوم يلتقط النمط حرف U خلال اليوم (أوسع عند الفتح/الإغلاق، أضيق في منتصف اليوم).
اعتبارات المعاملات الفائقة
للتنبؤ بالفرق تحديداً:
- استخدم خسارة MAE أو خسارة هوبر بدلاً من MSE. فتوزيعات الفروق ملتوية نحو اليمين مع قيم متطرفة عرضية (خلال أحداث الأخبار). وMAE أكثر متانة.
- اضبط
min_child_samplesعلى قيمة عالية (100 فأكثر) لمنع النموذج من ملاءمة ضجيج البنية الدقيقة في اللقطات المنفردة. - استخدم
subsample < 1.0لإزالة الارتباط بين الأشجار وتحسين التعميم عبر أنظمة التقلب المختلفة.
مقاربات التعلم العميق
في حين يتفوق التعزيز التدرجي على الخصائص الجدولية، يستطيع التعلم العميق أن يتعلم التمثيلات مباشرةً من بيانات دفتر الأوامر الخام. وقد أثبتت بنيتان فاعليتهما في مهام التنبؤ المتعلقة بالفرق.
البنية 1: CNN-LSTM للقطات دفتر الأوامر
تستخدم بنية DeepLOB (تشانغ وآخرون 2019) التفافات مكدّسة صغيرة النواة — ووحدة Inception — لاستخراج الأنماط المكانية عبر مستويات دفتر الأوامر مع الحفاظ على تلك البنية المكانية، تتبعها طبقات LSTM لنمذجة التبعيات الزمنية. والخيار التصميمي المهم هو عدم تجميع محور المستويات عالمياً (global-pool) قبل الطبقة المتكررة: فعل ذلك يطمس البنية بالضبط عبر المستويات التي يُفترض أن تلتقطها الالتفافات.
للتنبؤ بالفرق، المدخل تنسور بشكل :
- = عدد الخطوات الزمنية (مثلاً 100 لقطة)
- = عدد مستويات السعر (مثلاً 10 طلب + 10 عرض = 20)
- = الخصائص لكل مستوى (السعر، الحجم، عدد الأوامر)
النموذج أدناه يبقي خريطة الخصائص الالتفافية عبر المستويات ويفلطحها (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, :])
اعتبارات التدريب
-
التطبيع (Normalization): طبّع الأسعار بوصفها إزاحات عن السعر المتوسط (بالتيكات أو بنقاط الأساس). وطبّع الأحجام بمتوسطها المتدحرج. فالأسعار والأحجام الخام تسبب عدم استقرار في التدريب.
-
دالة الخسارة: استخدم خسارة هوبر () للتعامل مع قفزات الفرق:
-
أخذ عينات النوافذ والتسرب: استخدم نوافذ غير متداخلة للتدريب، و — تماماً كما في مسار التعزيز التدرجي — طهّر/احظر فجوة لا تقل عن
horizonلقطة بين التدريب والتحقق. فكل من هدف المتوسط الأمامي ونوافذ المدخلات المتداخلة يسرّب معلومات مستقبلية عبر حدود التقسيم ويضخّم الأداء الظاهري. -
التكيف عبر الإنترنت (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
عندما يكون المخزون طويلاً ()، يخفّض الانحراف كلاً من الطلب والعرض. ومن منظور واحد متسق: العرض الأدنى يجعل الشراء منا أرخص للآخذين، ما يفرّغ مخزوننا الطويل؛ والطلب الأدنى يقلّل احتمال أن يصيبنا البائعون، فيبطئ مزيداً من التراكم. ويتحكم الفرق المتنبَّأ به في العرض الكلي — متسعاً عندما يتوقع النموذج تقلباً أو اصطفاءً عكسياً، ومتضيقاً عندما تكون الظروف هادئة.
التقييم واختبار الأداء بالبيانات التاريخية

مقاييس التنبؤ بالفرق
إلى جانب مقاييس الانحدار القياسية (MAE و)، قيّم تنبؤات الفرق بمقاييس مهمة لصناعة السوق:
- المهارة فوق خط أساس: أبلغ دائماً عن MAE/R² نسبةً إلى تنبؤ AR(1) أو EWMA للفروق الأخيرة. ولأن الفروق شديدة الاستمرارية، تهيمن قيمة الارتباط الذاتي على النتيجة المطلقة؛ والتحسن فوق خط أساس تافه وحده هو ما يعكس محتوى تنبؤياً حقيقياً.
- دقة الاتجاه: هل يتنبأ النموذج بشكل صحيح بما إذا كان الفرق سيتسع أم سيضيق؟ نموذج بقيمة MAE متوسطة لكن بدقة اتجاه عالية قد يظل مربحاً.
- تغطية الذيول: هل يتنبأ النموذج بقفزات الفرق؟ احسب MAE على حدة لأعلى 5٪ من قيم الفرق — فهنا تتركّز خسائر الاصطفاء العكسي.
- المعايرة (Calibration): ارسم كميات الفرق المتنبَّأ بها مقابل المحققة. فتنبؤ النموذج جيد المعايرة عند المئين التسعين ينبغي أن يطابق المئين التسعين للفروق المحققة.
التقييم القائم على الأرباح والخسائر
في نهاية المطاف، المقياس الوحيد الذي يهمّ هو الأرباح والخسائر المحققة. اختبر الحلقة الكاملة بالبيانات التاريخية:
- عند كل طابع زمني، تنبّأ بالفرق
- احسب العروض باستخدام الفرق المتنبَّأ به + انحراف المخزون
- حاكِ التنفيذات مقابل الصفقات التاريخية
- تتبّع المخزون والأرباح والخسائر المحققة ونسبة شارب
قارن مقابل خطوط الأساس: (أ) فرق ثابت (وسيط السلسلة الزمنية)، (ب) EWMA للفروق الأخيرة، و(ج) مقدّر رول.
الخلاصة
تقع نمذجة الفرق عند تقاطع النظرية المالية والتعلم الآلي التطبيقي. ويوفر التفكيك الكلاسيكي إلى تكاليف معالجة الأوامر والمخزون والاصطفاء العكسي الحدسَ الاقتصادي لـسبب تباين الفروق. ويقدّم نموذج رول مقدّر خط أساس أنيقاً انطلاقاً من أدنى قدر من البيانات — ما دمت تحسبه بوحدات السعر. وتحوّل نماذج التعزيز التدرجي خصائص البنية الدقيقة إلى تنبؤات دقيقة قصيرة الأفق للفرق باستدلال منخفض زمن الاستجابة. وتتعلم بُنى التعلم العميق مباشرةً من بيانات دفتر الأوامر الخام، ملتقطةً أنماطاً قد تفوتها الخصائص المصنوعة يدوياً — شريطة أن تحافظ البنية على البنية عبر المستويات بدلاً من تجميعها بعيداً.
بالنسبة لنظام صناعة سوق إنتاجي، التوصية العملية متعددة الطبقات:
- استخدم تفكيك هوانغ-ستول بدون اتصال لفهم مكونات فرقك ومعايرة حدود المخاطرة
- استخدم مقدّر رول بوصفه فحص سلامة وللأدوات التي تفتقر فيها إلى بيانات دفتر الأوامر
- انشر نموذج LightGBM للتنبؤ بالفرق في الوقت الفعلي — فهو سريع، وقابل للتفسير، ومتين — مع تحقق متدرج للأمام مُطهَّر ومقايسة AR/EWMA
- شغّل نموذج CNN-LSTM أو محوِّل في حلقة ثانوية لكشف تغيرات النظام وتعديل النموذج الأساسي
الفرق ليس رقماً — إنه إشارة. وكلما نمذجته بشكل أفضل (وكلما قِست ذلك النموذج بأمانة أكبر)، أمكنك تسعير توفير السيولة بدقة أكبر.
هذا المقال جزء من سلسلة marketmaker.cc حول صناعة السوق الخوارزمية والبنية الدقيقة.
Authors
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.