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

تطابق الاختبار الخلفي والتداول الحي: لماذا يتداول الروبوت الخاص بك بشكل مختلف عن الاختبار الخلفي

تطابق الاختبار الخلفي والتداول الحي: لماذا يتداول الروبوت الخاص بك بشكل مختلف عن الاختبار الخلفي
#algotrading
#backtest
#live trading
#backtest-live parity
#execution
#NautilusTrader

قمت بتشغيل استراتيجية عبر اختبار خلفي. Sharpe 2.1، MaxDD -8%، PnL +67%. أطلقت الروبوت. بعد شهر قارنت: نفس الإشارات، نفس الفترة — لكن PnL الحي أقل بنسبة 40%. التراجع أعمق بمرة ونصف. اثنان من كل عشر صفقات لم تُنفَّذ على الإطلاق.

هذا ليس خطأ برمجيًا. هذا هو التباين بين الاختبار الخلفي والتداول الحي — تباين منهجي بين نتائج الاختبار الخلفي والتداول الفعلي. الجميع لديه هذا. السؤال الوحيد هو هل تعرف عنه وهل يمكنك التحكم فيه.

تقدم هذه المقالة تصنيفًا شاملًا للتباينات، وأنماطًا معمارية لتقليلها، وقائمة مراجعة عملية لمراقبة التطابق في بيئة الإنتاج.

متلازمة "كان يعمل في الاختبار الخلفي"

تباين الاختبار الخلفي مقابل التداول الحي — منحنى الأسهم المثالي مقابل النتائج المتقلبة الفعلية

كل متداول خوارزمي يمر بهذه الدورة:

  1. كتب استراتيجية في Jupyter notebook
  2. شغّل اختبارًا خلفيًا على CSV تاريخي — النتائج ممتازة
  3. أعاد كتابة المنطق كروبوت (غالبًا بلغة أو إطار عمل مختلف)
  4. أطلقه — النتائج لا تتطابق
  5. بدأ البحث عن خطأ، لم يجده — "السوق تغيّر"

المشكلة ليست السوق. المشكلة هي أن الاختبار الخلفي والروبوت هما منتجان برمجيان مختلفان يحاكيان نفس الواقع بطرق مختلفة. التباينات حتمية، لكن يمكن تنظيمها وتقليلها.

تصنيف التباينات

تصنيف تباينات الاختبار الخلفي والتداول الحي

تندرج جميع مصادر التباين ضمن أربع فئات. لكل منها — تصنيف الخطورة (من 1 إلى 5) والمساهمة النموذجية في تباين PnL.

1. تباينات البيانات (الخطورة: 3/5)

البيانات التي يراها الاختبار الخلفي والبيانات التي يراها الروبوت في الوقت الفعلي ليست نفس الشيء.

الطوابع الزمنية. تسلّم البورصات الشموع بقواعد مختلفة لتعيين الطوابع الزمنية. بورصة واحدة تحدد الشمعة ببداية الفترة، وأخرى بنهايتها. قد تُرجع واجهة REST API شمعة بتأخير 1-3 ثوانٍ بعد الإغلاق الفعلي. يعمل الاختبار الخلفي بطوابع زمنية "مثالية" من الملف التاريخي.

تجميع OHLCV. غالبًا ما يتم تجميع البيانات التاريخية من قبل المزود بشكل مختلف عما تفعله البورصة في الوقت الفعلي. الفرق في الرقم الأخير — لكن مع إشارات العتبة (تقاطع MA، اختراق المستوى) هذا يحدد ما إذا كانت الاستراتيجية تدخل مركزًا أم لا.

الفجوات والبيانات المفقودة. عادةً ما تكون البيانات التاريخية نظيفة — يتم ملء الشموع المفقودة بالاستيفاء. في الوقت الفعلي، قد ينقطع WebSocket، ويفقد الروبوت 30 ثانية من البيانات.

المساهمة النموذجية في تباين PnL: 2-5% من PnL السنوي.

2. تباينات التنفيذ (الخطورة: 5/5)

تباينات تنفيذ الأوامر — انزلاق دفتر الأوامر، التأخير، والتنفيذ الجزئي

أخطر فئة من التباينات. يحاكي الاختبار الخلفي التنفيذ بشكل مثالي — الواقع بعيد عن المثالية.

الانزلاق السعري. يملأ الاختبار الخلفي الأمر بسعر الإغلاق (أو سعر الإشارة). في الواقع، يتم تنفيذ أمر السوق بأفضل سعر bid/ask بالإضافة إلى انزلاق يعتمد على الحجم والسيولة. لمركز بقيمة 10 آلاف دولار على عملة بديلة متوسطة السيولة، يمكن أن يكون الانزلاق 0.05-0.3%.

صيغة الانزلاق التراكمي عبر NN صفقة:

Slippagetotal=i=1Nsizei×si\text{Slippage}_{total} = \sum_{i=1}^{N} \text{size}_i \times s_i

حيث sis_i هو انزلاق الصفقة ii، ويعتمد على عمق دفتر الأوامر:

sisizeiLiquidity(ti)×ks_i \approx \frac{\text{size}_i}{\text{Liquidity}(t_i)} \times k

التأخير. من لحظة توليد الإشارة إلى تنفيذ الأمر، يمر وقت: حساب الإشارة (1-50 مللي ثانية)، إرسال الطلب (10-200 مللي ثانية)، المطابقة في البورصة (1-10 مللي ثانية). في الاختبار الخلفي، التأخير = 0. في الحي — يمكن أن يتحرك السعر.

التنفيذ الجزئي. يفترض الاختبار الخلفي أن 100% من الأمر يتم تنفيذه فورًا. في الواقع، قد يتم تنفيذ أمر محدد جزئيًا — أو لا يتم تنفيذه على الإطلاق إذا انعكس السعر. بالنسبة لأمر سوق في سوق غير سائل، "ينزلق" الأمر عبر مستويات متعددة في دفتر الأوامر.

أولوية الطابور. لن يتم تنفيذ أمر محدد موضوع بأفضل سعر bid فورًا — فهو يصطف خلف جميع الأوامر الموضوعة سابقًا عند هذا المستوى. الاختبار الخلفي الذي يعتبر "السعر لامس = الأمر مُلئ" يبالغ بشكل منهجي في معدل التنفيذ.

المساهمة النموذجية في تباين PnL: 10-30% من PnL السنوي.

3. تباينات المنطق (الخطورة: 4/5)

هذه تباينات في كود الاستراتيجية نفسه بين الاختبار الخلفي والروبوت.

قواعد كود منفصلة. النمط المضاد الكلاسيكي: backtests/strategy_a.py و bot/strategy_a.py — ملفان منفصلان "يفعلان نفس الشيء". بعد ثلاثة أشهر من التعديلات، يتباعدان حتمًا. أحدهم أضاف فلترًا في الاختبار الخلفي ونسي تكراره في الروبوت. أو العكس — تم إصلاح خطأ في الروبوت لكنه بقي في الاختبار الخلفي.

أطر عمل مختلفة. اختبار خلفي على pandas بعمليات متجهة، روبوت على asyncio بمنطق مدفوع بالأحداث. حتى مع استراتيجية متطابقة، يتم التعامل مع الحالات الحدية بشكل مختلف: التقريب، ترتيب فحص الشروط، معالجة NaN.

إدارة الحالة. عادةً ما يكون الاختبار الخلفي عديم الحالة — يكرر عبر مصفوفة بيانات. الروبوت ذو حالة — يخزن المراكز، الأرصدة، تاريخ الأوامر. إعادة تشغيل الروبوت، فقدان الحالة، عدم التزامن مع البورصة — كل هذه مصادر للتباين.

المساهمة النموذجية في تباين PnL: 5-20% من PnL السنوي.

4. تباينات التكلفة (الخطورة: 3/5)

تباينات في نمذجة تكاليف التداول.

معدلات التمويل. معظم اختبارات العقود الدائمة الخلفية لا تأخذ معدلات التمويل في الاعتبار على الإطلاق. عند رافعة 10x ومعدل متوسط 0.01% لكل 8 ساعات، هذا يمثل 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% سنويًا — أكثر من PnL معظم الاستراتيجيات. التحليل المفصل في مقال معدلات التمويل تقتل الرافعة المالية.

العمولات. عادةً ما يتم نمذجة عمولات Maker/taker لكن غالبًا بالمعدل الخاطئ. مستويات VIP، خصومات BNB، الحسومات — كل هذا يؤثر على النتيجة النهائية.

الفارق السعري. الاختبار الخلفي المبني على الشموع لا يرى فارق bid-ask. على شمعة الدقيقة الواحدة، الإغلاق = 3000، لكن في الواقع bid = 2999.5 و ask = 3000.5. كل صفقة "تكلف" نصف الفارق.

المساهمة النموذجية في تباين PnL: 5-15% من PnL السنوي.

التأثير التراكمي

جميع الفئات الأربع تعمل بشكل متزامن وعادةً في اتجاه واحد — ضد المتداول:

PnLlivePnLbacktestΔdataΔexecutionΔlogicΔcosts\text{PnL}_{live} \approx \text{PnL}_{backtest} - \Delta_{data} - \Delta_{execution} - \Delta_{logic} - \Delta_{costs}

تباين إجمالي بنسبة 20-50% من PnL الاختبار الخلفي طبيعي لنظام غير مُحسَّن. مع الرافعة المالية، يتضاعف التأثير.

أنماط معمارية للتطابق

النمط 1: Shared Core (استخراج نواة مشتركة)

الفكرة: استخراج نواة الاستراتيجية — توليد الإشارات ومنطق التنفيذ — في وحدة منفصلة يستخدمها كل من الاختبار الخلفي والروبوت. الاختلاف الوحيد هو في البنية التحتية المحيطة: مصدر البيانات وآلية إرسال الأوامر.

┌─────────────────────────────────────┐
│         strategy_core.py            │
│  ┌─────────────┐ ┌───────────────┐  │
│  │ SignalEngine │ │ OrderManager  │  │
│  └──────┬──────┘ └──────┬────────┘  │
│         │               │           │
│    generate_signal()  create_order()│
└─────────┬───────────────┬───────────┘
          │               │
    ┌─────┴─────┐   ┌─────┴──────┐
    │ Backtest   │   │ Live       │
    │ DataFeed   │   │ DataFeed   │
    │ FillModel  │   │ Exchange   │
    └────────────┘   └────────────┘

from dataclasses import dataclass
from typing import Optional
import numpy as np

@dataclass
class Signal:
    side: str          # 'long' | 'short'
    entry_price: float
    sl_price: float
    tp_price: float
    size: float
    timestamp: int

@dataclass
class OrderRequest:
    side: str
    order_type: str    # 'market' | 'limit'
    price: float
    size: float

class StrategyCore:
    """
    نواة الاستراتيجية. كود متطابق للاختبار الخلفي والتداول الحي.
    يعتمد على البيانات فقط، وليس على البنية التحتية.
    """
    def __init__(self, params: dict):
        self.fast_period = params.get('fast_ma', 20)
        self.slow_period = params.get('slow_ma', 50)
        self.sl_pct = params.get('sl_pct', 0.02)
        self.tp_pct = params.get('tp_pct', 0.04)
        self.position: Optional[Signal] = None
        self._closes: list[float] = []

    def on_candle(self, timestamp: int, o: float, h: float,
                  l: float, c: float, v: float) -> Optional[OrderRequest]:
        """
        معالجة شمعة جديدة. يُرجع OrderRequest أو None.
        يتم استدعاء هذه الطريقة بشكل متطابق من الاختبار الخلفي والروبوت.
        """
        self._closes.append(c)

        if len(self._closes) < self.slow_period:
            return None

        fast_ma = np.mean(self._closes[-self.fast_period:])
        slow_ma = np.mean(self._closes[-self.slow_period:])

        if self.position is not None:
            exit_order = self._check_exit(h, l, c)
            if exit_order:
                self.position = None
                return exit_order

        if self.position is None:
            if fast_ma > slow_ma and self._prev_fast_ma <= self._prev_slow_ma:
                self.position = Signal(
                    side='long', entry_price=c,
                    sl_price=c * (1 - self.sl_pct),
                    tp_price=c * (1 + self.tp_pct),
                    size=1.0, timestamp=timestamp,
                )
                return OrderRequest('buy', 'market', c, 1.0)

        self._prev_fast_ma = fast_ma
        self._prev_slow_ma = slow_ma
        return None

    def _check_exit(self, high: float, low: float,
                    close: float) -> Optional[OrderRequest]:
        pos = self.position
        if pos.side == 'long':
            if low <= pos.sl_price:
                return OrderRequest('sell', 'market', pos.sl_price, pos.size)
            if high >= pos.tp_price:
                return OrderRequest('sell', 'market', pos.tp_price, pos.size)
        return None

الآن يستخدم كل من الاختبار الخلفي والروبوت نفس StrategyCore:


from strategy_core import StrategyCore

def run_backtest(candles, params, fill_model):
    core = StrategyCore(params)
    trades = []

    for candle in candles:
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            fill_price = fill_model.simulate_fill(order, candle)
            trades.append({'price': fill_price, 'side': order.side})

    return trades

from strategy_core import StrategyCore

async def run_live(exchange, symbol, params):
    core = StrategyCore(params)

    async for candle in exchange.stream_candles(symbol, '1m'):
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            await exchange.place_order(symbol, order.side,
                                       order.order_type, order.size)

القاعدة الأساسية: StrategyCore لا يعرف من أين تأتي البيانات أو إلى أين تُرسل الأوامر. يستقبل OHLCV ويُرجع OrderRequest. كل شيء آخر هو مسؤولية طبقة البنية التحتية.

النمط 2: التوحيد المدفوع بالأحداث (نهج NautilusTrader)

يحقق NautilusTrader التطابق من خلال NautilusKernel موحد — محرك Rust أصلي بنواة حتمية مدفوعة بالأحداث ودقة نانو ثانية. نفس تنفيذ الاستراتيجية يعمل في كل من الاختبار الخلفي والتداول الحي.

البنية مبنية على نمط المنافذ والمحولات (البنية السداسية):

┌──────────────────────────────────┐
│        NautilusKernel            │
│  ┌───────────┐  ┌─────────────┐  │
│  │ Strategy   │  │ RiskEngine  │  │
│  │ (Python)   │  │ (Rust)      │  │
│  └─────┬─────┘  └──────┬──────┘  │
│        │               │         │
│  ┌─────┴───────────────┴──────┐  │
│  │      Message Bus (Rust)    │  │
│  └─────┬───────────────┬──────┘  │
└────────┼───────────────┼─────────┘
         │               │
   ┌─────┴─────┐   ┌─────┴──────┐
   │ Backtest   │   │ Live       │
   │ Adapter    │   │ Adapter    │
   │ FillModel  │   │ Exchange   │
   │ (L2 book)  │   │ Gateway    │
   └────────────┘   └────────────┘

المزايا:

  • إعادة تشغيل حتمية. تُعالج الأحداث بترتيب محدد بدقة — نتيجة الاختبار الخلفي قابلة للتكرار على مستوى البت.
  • FillModel مخصص. محاكاة دفتر أوامر L2 لكل تنفيذ — يتم محاكاة الانزلاق بناءً على عمق دفتر الأوامر الحقيقي.
  • الأداء. حتى 5 ملايين صف/ثانية، معالجة بيانات لا تتسع في RAM.
  • Redis + PostgreSQL. ذاكرة مؤقتة وناقل رسائل عبر Redis، استمرارية عبر PostgreSQL — بنية تحتية متطابقة للاختبار الخلفي والتداول الحي.

النمط 3: Strategy Interface (نهج Freqtrade)

يستخدم Freqtrade واجهة IStrategy موحدة: نفس فئة الاستراتيجية تعمل في كل من الاختبار الخلفي والتداول الحي. الاختلاف الوحيد هو طبقة الاستمرارية.


class IStrategy:
    """واجهة موحدة — التنفيذ لا يعرف إن كان هذا اختبارًا خلفيًا أم تداولًا حيًا."""

    def populate_indicators(self, dataframe, metadata):
        """حساب المؤشرات."""
        dataframe['fast_ma'] = dataframe['close'].rolling(20).mean()
        dataframe['slow_ma'] = dataframe['close'].rolling(50).mean()
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        """تحديد إشارات الدخول."""
        dataframe.loc[
            (dataframe['fast_ma'] > dataframe['slow_ma']) &
            (dataframe['fast_ma'].shift(1) <= dataframe['slow_ma'].shift(1)),
            'enter_long'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        """تحديد إشارات الخروج."""
        dataframe.loc[
            (dataframe['fast_ma'] < dataframe['slow_ma']),
            'exit_long'
        ] = 1
        return dataframe

يوفر Freqtrade بالإضافة إلى ذلك:

  • Hyperopt عبر Optuna — تحسين معلمات الاستراتيجية
  • --timeframe-detail — التعمق في إطار زمني أدق لتحسين دقة التنفيذ (مشابه لـ التعمق التكيفي)

مقارنة الأنماط

Shared Core مدفوع بالأحداث (NautilusTrader) Strategy Interface (Freqtrade)
تعقيد التنفيذ منخفض عالي متوسط
مستوى التطابق متوسط أقصى عالي
محاكاة التنفيذ FillModel منفصل دفتر أوامر L2 --timeframe-detail
لغة النواة Python Rust + Python Python
مناسب لـ محركات مخصصة التداول المؤسسي البداية السريعة

دقة محاكاة التنفيذ

مستويات دقة محاكاة التنفيذ

محاكاة التنفيذ هي المصدر الرئيسي لتباين التنفيذ. ثلاثة مستويات من الدقة:

المستوى 1: ساذج (التنفيذ بسعر الإغلاق)

fill_price = candle['close']

خطأ: لا يأخذ في الاعتبار الانزلاق والفارق السعري والتنفيذ الجزئي. يبالغ بشكل منهجي في تقدير PnL.

المستوى 2: نموذج الانزلاق

def simulate_fill(order, candle, slippage_bps=5):
    """التنفيذ مع الانزلاق."""
    base_price = candle['close']
    slip = base_price * slippage_bps / 10000

    if order.side == 'buy':
        return base_price + slip  # Buy at a higher price
    else:
        return base_price - slip  # Sell at a lower price

خطأ: الانزلاق الثابت لا يأخذ في الاعتبار السيولة وحجم الأمر. أفضل من الساذج، لكنه لا يزال نموذجًا خامًا.

المستوى 3: التعمق التكيفي مع بيانات 1s/100ms

الخيار الأفضل: استخدام بيانات حقيقية دقيقة لتحديد ترتيب تنفيذ SL/TP بدقة. موصوف بالتفصيل في مقال التعمق التكيفي: الاختبار الخلفي بتفصيل متغير.

class RealisticFillModel:
    """
    نموذج تنفيذ مركب: انزلاق + فارق سعري + تأثير الحجم.
    """
    def __init__(self, avg_spread_bps=3, impact_coeff=0.1):
        self.avg_spread_bps = avg_spread_bps
        self.impact_coeff = impact_coeff

    def simulate_fill(self, order, candle, order_size_usd):
        base_price = candle['close']

        spread_cost = base_price * self.avg_spread_bps / 20000

        candle_volume_usd = candle['volume'] * candle['close']
        participation_rate = order_size_usd / max(candle_volume_usd, 1)
        impact = base_price * self.impact_coeff * np.sqrt(participation_rate)

        if order.side == 'buy':
            return base_price + spread_cost + impact
        else:
            return base_price - spread_cost - impact

صيغة التأثير السوقي (نموذج Almgren-Chriss المبسط):

Δp=σkVorderVmarket\Delta p = \sigma \cdot k \cdot \sqrt{\frac{V_{order}}{V_{market}}}

حيث σ\sigma هي التقلبية، kk هو معامل التأثير، VorderV_{order} هو حجم الأمر، و VmarketV_{market} هو حجم السوق للفترة.

قائمة مراجعة عملية للتطابق

قبل إطلاق الروبوت في التداول الحي، تحقق من كل عنصر:

الكود:

  • الاستراتيجية تستخدم نواة مشتركة (وحدة واحدة للاختبار الخلفي والتداول الحي)
  • لا يوجد تكرار لمنطق الإشارة في مكانين
  • اختبارات الوحدة تتحقق من مخرجات نواة متطابقة لمدخلات متطابقة
  • ترتيب فحص الشروط متطابق (SL قبل TP؟ TP قبل SL؟)

البيانات:

  • تنسيق الطوابع الزمنية متطابق (UTC، نفس المزود)
  • تجميع OHLCV يستخدم نفس القواعد
  • معالجة الشموع المفقودة متطابقة
  • لا يوجد look-ahead bias — الاختبار الخلفي لا يطّلع على المستقبل

التنفيذ:

  • نموذج الانزلاق مُعايَر على بيانات حقيقية
  • التنفيذ الجزئي مُنمذج (أو على الأقل مُقدَّر بشكل متشائم)
  • أوامر الحد لديها نموذج أولوية الطابور
  • التأخير مأخوذ في الاعتبار (تأخير 100-500 مللي ثانية من الإشارة إلى التنفيذ)

التكاليف:

  • عمولات Maker/taker مُدرجة بالمعدل الحالي
  • معدلات التمويل مأخوذة في الاعتبار مع العقود الدائمة
  • الفارق السعري مُنمذج (على الأقل المتوسط)

البنية التحتية:

  • استمرارية الحالة: الروبوت يستعيد المراكز بعد إعادة التشغيل
  • منطق إعادة الاتصال: WebSocket يعيد الاتصال بدون فقدان بيانات
  • التسجيل: جميع الأوامر والتنفيذات مسجلة للتحليل اللاحق

مراقبة التباين في بيئة الإنتاج

التطابق ليس فحصًا لمرة واحدة بل عملية مستمرة. بعد إطلاق الروبوت، يجب تتبع التباينات في الوقت الفعلي.

الوضع الظلي (التداول الورقي)

شغّل الروبوت بالتوازي مع الاختبار الخلفي على نفس البيانات. يولّد الروبوت إشارات لكنه لا يرسل أوامر — يسجّل فقط. في نفس الوقت، يعالج الاختبار الخلفي نفس البيانات. قارن:

class DivergenceMonitor:
    """
    يقارن إشارات الاختبار الخلفي والروبوت الحي في الوقت الفعلي.
    """
    def __init__(self, tolerance_pct=0.5):
        self.tolerance = tolerance_pct / 100
        self.divergences = []

    def compare_signal(self, backtest_signal, live_signal, timestamp):
        """مقارنة إشارات الاختبار الخلفي والتداول الحي."""
        if backtest_signal is None and live_signal is None:
            return  # Both silent — OK

        if (backtest_signal is None) != (live_signal is None):
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'signal_mismatch',
                'backtest': backtest_signal,
                'live': live_signal,
                'severity': 'HIGH',
            })
            return

        price_diff = abs(
            backtest_signal.entry_price - live_signal.entry_price
        ) / backtest_signal.entry_price

        if price_diff > self.tolerance:
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'price_divergence',
                'diff_pct': price_diff * 100,
                'severity': 'MEDIUM',
            })

    def compare_fill(self, backtest_fill, live_fill, timestamp):
        """مقارنة التنفيذ."""
        if backtest_fill and live_fill:
            slippage = (live_fill['price'] - backtest_fill['price']
                        ) / backtest_fill['price']
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'fill_divergence',
                'slippage_bps': slippage * 10000,
                'severity': 'LOW' if abs(slippage) < 0.001 else 'MEDIUM',
            })

    def report(self):
        """تقرير التباين الأسبوعي."""
        from collections import Counter
        severity_counts = Counter(d['severity'] for d in self.divergences)
        return {
            'total_divergences': len(self.divergences),
            'by_severity': dict(severity_counts),
            'avg_slippage_bps': np.mean([
                d['slippage_bps'] for d in self.divergences
                if d['type'] == 'fill_divergence'
            ]) if any(d['type'] == 'fill_divergence'
                      for d in self.divergences) else 0,
        }

مقاييس لوحة المعلومات

المقياس الصيغة عتبة التنبيه
معدل تطابق الإشارات matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95%
متوسط الانزلاق 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
معدل التنفيذ filledsent\frac{\text{filled}}{\text{sent}} < 90%
تباين PnL PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20%
التأخير p99 النسبة المئوية 99 من الإشارة إلى التنفيذ > 500 ms

معايرة نموذج الانزلاق

بعد تراكم بيانات لمدة 2-4 أسابيع، يمكنك معايرة نموذج انزلاق الاختبار الخلفي على بيانات حقيقية:

def calibrate_slippage(live_fills: list[dict]) -> dict:
    """
    معايرة نموذج الانزلاق باستخدام تنفيذات حقيقية.

    live_fills: [{'expected_price': ..., 'actual_price': ..., 'size_usd': ..., 'volume_usd': ...}]
    """
    slippages = []
    participation_rates = []

    for fill in live_fills:
        slip = abs(fill['actual_price'] - fill['expected_price']
                   ) / fill['expected_price']
        part = fill['size_usd'] / max(fill['volume_usd'], 1)
        slippages.append(slip)
        participation_rates.append(part)

    slippages = np.array(slippages)
    participation_rates = np.array(participation_rates)

    from scipy.optimize import curve_fit

    def model(x, k, base):
        return k * np.sqrt(x) + base

    popt, _ = curve_fit(model, participation_rates, slippages,
                        p0=[0.1, 0.0001])

    return {
        'impact_coeff': popt[0],
        'base_slippage': popt[1],
        'mean_slippage_bps': np.mean(slippages) * 10000,
        'p95_slippage_bps': np.percentile(slippages, 95) * 10000,
    }

الروابط مع الأدوات الأخرى

تطابق الاختبار الخلفي والتداول الحي ليس مهمة معزولة. يتقاطع مع أدوات أخرى من سلسلة "اختبارات خلفية بدون أوهام":

  • التعمق التكيفي — يحسن دقة محاكاة التنفيذ، وهو مكون أساسي لتطابق التنفيذ.
  • معدلات التمويل — إذا لم يُنمذج الاختبار الخلفي التمويل، فالتطابق مستحيل عند رافعة > 3x.
  • ذاكرة Parquet المؤقتة — الأطر الزمنية والمؤشرات المحسوبة مسبقًا تضمن أن الاختبار الخلفي يرى نفس البيانات التي يراها الروبوت. محاكاة RunningCandleBuffer = تحديث في الوقت الفعلي.
  • Polars مقابل Pandas — عند التبديل من pandas (اختبار خلفي) إلى Polars (تداول حي)، تحتاج للتأكد من تطابق النتائج العددية.
  • التحسين المتقدم — التحسين المتقدم على بيانات خارج العينة يُظهر كيف تتدهور الاستراتيجية — وهذا أقرب للتداول الحي من اختبار خلفي داخل العينة.

التوصيات

  1. النواة المشتركة إلزامية. قاعدة كود واحدة لتوليد الإشارات هي الحد الأدنى للتطابق. ملفان بمنطق متطابق يضمنان التباعد خلال شهر.

  2. عايِر نموذج التنفيذ. انزلاق ثابت بمقدار 5 bps أفضل من لا شيء. نموذج انزلاق مُعايَر على بيانات حقيقية أفضل بكثير.

  3. استخدم الوضع الظلي أول 2-4 أسابيع. لا تتداول بأموال حقيقية حتى يصل معدل تطابق الإشارات إلى 95%+.

  4. نمذج معدلات التمويل. للعقود الدائمة، هذا ليس اختياريًا — إنه إلزامي. يمكن للتمويل أن يستهلك كل PnL عند رافعة > 5x.

  5. سجّل كل شيء. كل إشارة، كل أمر، كل تنفيذ — مع طوابع زمنية. بدون سجلات، التحليل اللاحق مستحيل.

  6. أتمت المقارنة. تقرير DivergenceMonitor الأسبوعي يجب أن يصل تلقائيًا. لا تنتظر حتى يصبح PnL سالبًا.

  7. اختبار خلفي متشائم بشكل افتراضي. من الأفضل التقليل من التوقعات في الاختبار الخلفي والتفاجؤ بشكل إيجابي في التداول الحي من العكس. يجب أن يكون نموذج الانزلاق متحفظًا.

الخاتمة

تطابق الاختبار الخلفي والتداول الحي ليس خاصية نظام بل عملية. التطابق المثالي غير موجود: الاختبار الخلفي بحكم التعريف نموذج للواقع، والنموذج دائمًا يُبسّط. لكن الفرق بين "النموذج يختلف بنسبة 5%" و"النموذج يختلف بنسبة 50%" يحدده المعمار.

ثلاثة مستويات من النضج:

  1. أساسي. نواة مشتركة، انزلاق ثابت، عمولات. التباين: 10-20%.
  2. متقدم. بنية مدفوعة بالأحداث، تعمق تكيفي، نموذج تمويل، وضع ظلي. التباين: 5-10%.
  3. مؤسسي. محاكاة دفتر أوامر L2، نموذج تأثير مُعايَر، مراقبة التباين في الوقت الفعلي. التباين: 2-5%.

مهمتك هي تحديد المستوى الذي أنت فيه وفهم ما مقدار التباين الذي تعتبره مقبولًا لحجم مركزك ورافعتك المالية.


روابط مفيدة

  1. NautilusTrader — High-Performance Algorithmic Trading Platform
  2. Freqtrade — Free, open source crypto trading bot
  3. Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
  4. Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  5. Ernest Chan — Quantitative Trading: How to Build Your Own Algorithmic Trading Business
  6. Hexagonal Architecture (Ports and Adapters) — Alistair Cockburn
  7. Optuna — Hyperparameter Optimization Framework

Citation

@article{soloviov2026backtestliveparity,
  author = {Soloviov, Eugen},
  title = {Backtest-live parity: why your bot trades differently from the backtest},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/backtest-live-parity},
  description = {Complete taxonomy of divergences between backtesting and live trading: from slippage and partial fills to codebase desynchronization. Architectural patterns for achieving parity and a production monitoring checklist.}
}
blog.disclaimer

MarketMaker.cc Team

البحوث والاستراتيجيات الكمية

ناقش في تلغرام
Newsletter

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

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

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