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

تحسين المشي للأمام: الاختبار الصادق الوحيد للاستراتيجية

تحسين المشي للأمام: الاختبار الصادق الوحيد للاستراتيجية
#algotrading
#backtest
#walk-forward
#overfitting
#validation
#optimization

لقد قمت بتحسين استراتيجية. 12 معلمة فصل، 9 معلمات وصفية — 21 إجمالاً. اختبار رجعي على مدى 25 شهراً على زوج واحد يُظهر PnL بنسبة +3342% عند أقصى رافعة مالية. منحنى حقوق الملكية يرتفع بدون تراجعات تقريباً. Sharpe أعلى من 3. كل شيء يبدو مثالياً.

تُطلق البوت. بعد أسبوعين، تخسر الاستراتيجية 18% من رأس المال. بعد شهر — 34%. المعلمات التي "نجحت" على البيانات التاريخية تبين أنها كانت مُلائمة لتسلسل محدد من أحداث السوق. لم تجد نمطاً — بل حفظت الضوضاء.

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

فخ التقسيم الواحد للتدريب/الاختبار

تصور فخ تقسيم التدريب/الاختبار

النهج القياسي: تقسيم البيانات إلى 70% تدريب و30% اختبار. التحسين على التدريب، والتحقق على الاختبار. إذا كانت النتيجة إيجابية — الإطلاق.

المشكلة: هذا اختبار واحد على تقسيم واحد. النتيجة تعتمد على أين ترسم الحد. حرّك الحد بشهر واحد — ويمكن أن يتغير PnL خارج العينة من +40% إلى -15%.

Data:    |===== Train (70%) =====|== Test (30%) ==|
Split 1: |===2024-01..2025-09====|==2025-10..26-01==|   OOS PnL: +38%
Split 2: |===2024-01..2025-06====|==2025-07..26-01==|   OOS PnL: -12%
Split 3: |===2024-04..2025-12====|==2026-01..26-04==|   OOS PnL: +7%

ثلاثة تقسيمات مختلفة — ثلاثة استنتاجات مختلفة. أيها يمكن الوثوق به؟ لا شيء منها. التقسيم الواحد للتدريب/الاختبار هو نفس التقدير النقطي الواحد الذي وصفنا مشاكله في مونت كارلو بوتستراب. تحتاج ليس إلى فحص واحد، بل إلى سلسلة منهجية من الفحوصات على شرائح بيانات متتالية.

هذا بالضبط ما وُجد تحسين المشي للأمام من أجله.

ما هو تحسين المشي للأمام

مخطط نوافذ المشي للأمام المتدحرجة

WFO هو إجراء التحسين والتحقق المتسلسل لاستراتيجية على نوافذ بيانات منزلقة (أو متوسعة). الفكرة: محاكاة عملية التداول الحقيقية حيث تقوم دورياً بإعادة تحسين المعلمات على البيانات المتاحة ثم التداول حتى إعادة التحسين التالية.

تتكون كل "نافذة" من جزأين:

  • داخل العينة (IS) — الفترة التي يتم فيها تحسين المعلمات
  • خارج العينة (OOS) — الفترة التي يتم فيها اختبار المعلمات المُكتشفة بدون ملاءمة

الخاصية الرئيسية: فترات OOS لا تتداخل وتغطي معاً جزءاً كبيراً من البيانات. منحنى حقوق الملكية الناتج يُبنى فقط من شرائح OOS — هذا هو التقييم الصادق للاستراتيجية.

WFO المُرتكز (النافذة المتوسعة)

تصور نافذة WFO المُرتكز المتوسعة

في WFO المُرتكز، بداية فترة التدريب ثابتة، ونهايتها تتوسع مع كل نافذة:

Window 1: Train [2024-01]         Test [2024-04]
Window 2: Train [2024-01..04]     Test [2024-07]    (growing train)
Window 3: Train [2024-01..07]     Test [2024-10]
Window 4: Train [2024-01..10]     Test [2025-01]
Window 5: Train [2024-01..2025-01]  Test [2025-04]

المزايا:

  • كل فترة تدريب لاحقة تحتوي على المزيد من البيانات — التحسين أكثر استقراراً
  • الأنماط المبكرة لا تُفقد — دائماً في مجموعة التدريب
  • أسهل في التنفيذ

العيوب:

  • البيانات القديمة قد "تُخفف" الأنماط الحالية
  • إذا تغير السوق هيكلياً — البيانات القديمة ضارة
  • فترة التدريب تنمو بلا حدود، مما يزيد وقت التحسين

WFO المتدحرج (النافذة المنزلقة)

في WFO المتدحرج، فترة تدريب بطول ثابت "تنزلق" عبر البيانات:

Window 1: Train [2024-01..06]  Test [2024-07..09]
Window 2: Train [2024-04..09]  Test [2024-10..12]
Window 3: Train [2024-07..12]  Test [2025-01..03]
Window 4: Train [2024-10..2025-03]  Test [2025-04..06]
Window 5: Train [2025-01..06]  Test [2025-07..09]

المزايا:

  • يتكيف مع نظام السوق الحالي
  • وقت تحسين ثابت
  • البيانات القديمة غير ذات الصلة لا تؤثر على النتائج

العيوب:

  • بيانات أقل للتدريب — تباين أعلى للمعلمات المثلى
  • حساس لاختيار طول النافذة
  • قد "ينسى" أحداثاً نادرة لكنها مهمة (الانهيارات المفاجئة)

التحقق المتقاطع التوليفي المُنقّى (CPCV)

تصور التحقق المتقاطع التوليفي المُنقّى

طريقة متقدمة اقترحها Marcos Lopez de Prado. تُقسم البيانات إلى NN مجموعة، يُختار منها kk للاختبار. الفرق الرئيسي عن التحقق المتقاطع القياسي هو التنقية (إزالة البيانات عند حد التدريب/الاختبار) والحظر (فجوة إضافية لمنع تسرب البيانات):

عدد التوليفات=(Nk)\text{عدد التوليفات} = \binom{N}{k}

مع N=10,k=2N = 10, k = 2: 45 توليفة تدريب/اختبار. كل توليفة تنتج نتيجة OOS، والتقدير النهائي هو المتوسط عبر جميع التوليفات.

from itertools import combinations
import numpy as np

def cpcv_splits(n_groups: int, k_test: int, purge_pct: float = 0.01):
    """
    Generate CPCV splits with purging.

    Args:
        n_groups: number of groups
        k_test: number of test groups in each split
        purge_pct: fraction of data for purging (at the train/test boundary)
    """
    groups = list(range(n_groups))
    splits = []

    for test_groups in combinations(groups, k_test):
        train_groups = [g for g in groups if g not in test_groups]
        splits.append({
            "train": train_groups,
            "test": list(test_groups),
            "purge_groups": _get_purge_groups(train_groups, test_groups),
        })

    return splits

def _get_purge_groups(train, test):
    """Groups at the train/test boundary for purging."""
    purge = set()
    for t in test:
        if t - 1 in train:
            purge.add(t - 1)
        if t + 1 in train:
            purge.add(t + 1)
    return list(purge)

CPCV أفضل من WFO المتدحرج عندما تكون البيانات شحيحة، لكنه أكثر تكلفة حسابياً. لاستراتيجية بـ 21 معلمة و25 شهراً من البيانات، نوصي بالبدء بـ WFO المتدحرج واستخدام CPCV كفحص إضافي.

معلمات WFO الرئيسية

تصور معلمات WFO الرئيسية

طول فترة التدريب

فترة تدريب قصيرة جداً — بيانات غير كافية لتحسين موثوق. طويلة جداً — البيانات القديمة تُخفف الأنماط الحالية.

قاعدة عامة: يجب أن يحتوي التدريب على 200-300 صفقة على الأقل. إذا كانت الاستراتيجية تُنفذ صفقتين يومياً:

Tmin=300 صفقة2 صفقة/يوم=150 يوم5 أشهرT_{min} = \frac{300\ \text{صفقة}}{2\ \text{صفقة/يوم}} = 150\ \text{يوم} \approx 5\ \text{أشهر}

للعملات المشفرة مع تحولات نظامها، نوصي بعدم تجاوز 6-12 شهراً للنافذة المتدحرجة.

طول فترة الاختبار

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

القاعدة: الاختبار = 20-33% من التدريب. إذا كان التدريب = 6 أشهر، فالاختبار = 1.5-2 شهر.

التداخل

في WFO المتدحرج، يمكن أن تتداخل النوافذ. التداخل يزيد عدد نقاط بيانات OOS لكنه يُدخل ارتباطاً بين التقديرات:

Without overlap:
  Train [01..06] → Test [07..09]
  Train [07..12] → Test [01..03]

With 50% overlap:
  Train [01..06] → Test [07..09]
  Train [04..09] → Test [10..12]
  Train [07..12] → Test [01..03]

التوصية: تداخل 50% على فترة التدريب — توازن جيد بين عدد النوافذ واستقلالية التقديرات.

تكرار إعادة التحسين

يُحدد مدى تكرار إعادة حساب المعلمات. في سوق العملات المشفرة، التكرار الأمثل هو كل 1-3 أشهر. إعادة التحسين المتكررة جداً تزيد خطر الإفراط في التخصيص على الضوضاء؛ والأقل تكراراً — خطر تقادم المعلمات.

نسبة كفاءة المشي للأمام ومعدل التدهور

تصور نسبة كفاءة المشي للأمام والتدهور

نسبة كفاءة المشي للأمام (WFER)

المقياس الرئيسي لـ WFO — نسبة عوائد OOS إلى عوائد IS:

WFER=PnLOOSPnLIS\text{WFER} = \frac{\text{PnL}_{OOS}}{\text{PnL}_{IS}}

التفسير:

WFER التفسير
> 0.8 متانة ممتازة. المعلمات تنتقل إلى بيانات جديدة.
0.5 — 0.8 متانة مقبولة. الاستراتيجية تعمل لكن مع تدهور.
0.3 — 0.5 حالة حدية. إفراط جزئي في التخصيص محتمل.
< 0.3 إفراط في التخصيص. المعلمات مُلائمة لبيانات IS.
< 0 الاستراتيجية غير مربحة OOS. إفراط كامل في التخصيص أو خطأ منطقي.

إذا كان WFER < 0.5 — فالاستراتيجية على الأرجح مُفرطة في التخصيص. هذا هو مُرشحنا الأساسي.

معدل التدهور

يُظهر مدى سرعة فقدان المعلمات المثلى لفعاليتها بمرور الوقت:

معدل التدهور=d(OOS PnL)dt\text{معدل التدهور} = \frac{d(\text{OOS PnL})}{dt}

عملياً: قسّم فترة الاختبار إلى فترات فرعية وتتبع ديناميكيات PnL:

def degradation_rate(oos_returns: np.ndarray, n_subperiods: int = 4) -> float:
    """
    Estimate parameter degradation rate.

    Splits the OOS period into sub-intervals and computes the slope
    of linear regression of PnL against sub-interval number.

    Returns:
        slope: negative = degradation, positive = improvement
    """
    chunk_size = len(oos_returns) // n_subperiods
    subperiod_pnls = []

    for i in range(n_subperiods):
        start = i * chunk_size
        end = start + chunk_size
        sub_pnl = np.sum(oos_returns[start:end])
        subperiod_pnls.append(sub_pnl)

    x = np.arange(n_subperiods)
    slope = np.polyfit(x, subperiod_pnls, 1)[0]

    return slope

إذا كان معدل التدهور سلبياً بشدة — فالمعلمات تتقادم بسرعة، وتحتاج إلى إعادة تحسين أكثر تكراراً أو فترة تدريب أقصر.

تنفيذ خط أنابيب WFO الكامل بـ Python

تصور بنية خط أنابيب WFO

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Callable, List, Optional
import warnings

@dataclass
class WFOWindow:
    """A single walk-forward window."""
    window_id: int
    train_start: int         # train start index
    train_end: int           # train end index (exclusive)
    test_start: int          # test start index
    test_end: int            # test end index (exclusive)
    best_params: dict = field(default_factory=dict)
    is_pnl: float = 0.0     # in-sample PnL
    oos_pnl: float = 0.0    # out-of-sample PnL
    oos_returns: np.ndarray = field(default_factory=lambda: np.array([]))
    wfer: float = 0.0       # walk-forward efficiency ratio

@dataclass
class WFOResult:
    """Result of the entire WFO."""
    windows: List[WFOWindow]
    aggregate_oos_pnl: float
    aggregate_is_pnl: float
    wfer: float
    degradation_rate: float
    oos_equity: np.ndarray
    oos_sharpe: float
    oos_max_dd: float
    n_windows: int
    passed: bool             # whether the strategy passed the filter

class WalkForwardOptimizer:
    """
    Walk-Forward Optimization pipeline.

    Supports anchored (expanding) and rolling (sliding) modes.
    """

    def __init__(
        self,
        data: np.ndarray,
        optimize_fn: Callable,
        evaluate_fn: Callable,
        mode: str = "rolling",         # "rolling" or "anchored"
        train_size: int = 180,         # days
        test_size: int = 60,           # days
        step_size: int = 60,           # window step size, days
        min_trades: int = 30,          # min number of trades in OOS
        wfer_threshold: float = 0.5,   # WFER threshold for accept/reject
    ):
        self.data = data
        self.optimize_fn = optimize_fn
        self.evaluate_fn = evaluate_fn
        self.mode = mode
        self.train_size = train_size
        self.test_size = test_size
        self.step_size = step_size
        self.min_trades = min_trades
        self.wfer_threshold = wfer_threshold

    def generate_windows(self) -> List[WFOWindow]:
        """Generate walk-forward windows."""
        n = len(self.data)
        windows = []
        window_id = 0

        if self.mode == "rolling":
            start = 0
            while start + self.train_size + self.test_size <= n:
                w = WFOWindow(
                    window_id=window_id,
                    train_start=start,
                    train_end=start + self.train_size,
                    test_start=start + self.train_size,
                    test_end=min(start + self.train_size + self.test_size, n),
                )
                windows.append(w)
                start += self.step_size
                window_id += 1

        elif self.mode == "anchored":
            train_end = self.train_size
            while train_end + self.test_size <= n:
                w = WFOWindow(
                    window_id=window_id,
                    train_start=0,
                    train_end=train_end,
                    test_start=train_end,
                    test_end=min(train_end + self.test_size, n),
                )
                windows.append(w)
                train_end += self.step_size
                window_id += 1

        return windows

    def run(self) -> WFOResult:
        """Run the full WFO pipeline."""
        windows = self.generate_windows()
        all_oos_returns = []

        for w in windows:
            train_data = self.data[w.train_start:w.train_end]
            test_data = self.data[w.test_start:w.test_end]

            best_params, is_pnl = self.optimize_fn(train_data)
            w.best_params = best_params
            w.is_pnl = is_pnl

            oos_pnl, oos_returns = self.evaluate_fn(test_data, best_params)
            w.oos_pnl = oos_pnl
            w.oos_returns = oos_returns

            if is_pnl != 0:
                w.wfer = oos_pnl / is_pnl
            else:
                w.wfer = 0.0

            all_oos_returns.extend(oos_returns)

        all_oos = np.array(all_oos_returns)
        oos_equity = np.cumprod(1 + all_oos)
        peak = np.maximum.accumulate(oos_equity)
        max_dd = ((oos_equity - peak) / peak).min()

        aggregate_is = sum(w.is_pnl for w in windows)
        aggregate_oos = sum(w.oos_pnl for w in windows)
        wfer = aggregate_oos / aggregate_is if aggregate_is != 0 else 0

        if np.std(all_oos) > 0:
            oos_sharpe = np.mean(all_oos) / np.std(all_oos) * np.sqrt(252)
        else:
            oos_sharpe = 0

        deg_rate = self._degradation_rate(windows)

        passed = wfer >= self.wfer_threshold and aggregate_oos > 0

        return WFOResult(
            windows=windows,
            aggregate_oos_pnl=aggregate_oos,
            aggregate_is_pnl=aggregate_is,
            wfer=wfer,
            degradation_rate=deg_rate,
            oos_equity=oos_equity,
            oos_sharpe=oos_sharpe,
            oos_max_dd=max_dd,
            n_windows=len(windows),
            passed=passed,
        )

    def _degradation_rate(self, windows: List[WFOWindow]) -> float:
        """Slope of OOS PnL across window numbers."""
        if len(windows) < 3:
            return 0.0
        pnls = [w.oos_pnl for w in windows]
        x = np.arange(len(pnls))
        slope = np.polyfit(x, pnls, 1)[0]
        return slope

مثال على الاستخدام

import numpy as np

np.random.seed(42)
prices = 100 * np.cumprod(1 + np.random.normal(0.0002, 0.02, 750))

def my_optimize(train_data):
    """
    Optimize strategy on train data.
    Returns (best_params, is_pnl).
    """
    best_pnl = -np.inf
    best_params = {}

    for fast in range(5, 30, 5):
        for slow in range(20, 100, 10):
            if fast >= slow:
                continue
            pnl, _ = _run_strategy(train_data, fast, slow)
            if pnl > best_pnl:
                best_pnl = pnl
                best_params = {"fast": fast, "slow": slow}

    return best_params, best_pnl

def my_evaluate(test_data, params):
    """
    Evaluate strategy on test data with fixed parameters.
    Returns (oos_pnl, oos_returns).
    """
    pnl, returns = _run_strategy(test_data, params["fast"], params["slow"])
    return pnl, returns

def _run_strategy(data, fast_period, slow_period):
    """Simple MA crossover strategy."""
    fast_ma = pd.Series(data).rolling(fast_period).mean().values
    slow_ma = pd.Series(data).rolling(slow_period).mean().values

    position = 0
    returns = []

    for i in range(slow_period, len(data) - 1):
        if fast_ma[i] > slow_ma[i] and position <= 0:
            position = 1
        elif fast_ma[i] < slow_ma[i] and position >= 0:
            position = -1

        daily_ret = (data[i + 1] - data[i]) / data[i]
        returns.append(position * daily_ret)

    total_pnl = np.sum(returns)
    return total_pnl, np.array(returns)

wfo = WalkForwardOptimizer(
    data=prices,
    optimize_fn=my_optimize,
    evaluate_fn=my_evaluate,
    mode="rolling",
    train_size=180,    # 6 months
    test_size=60,      # 2 months
    step_size=60,      # step = test
)

result = wfo.run()

print(f"Windows: {result.n_windows}")
print(f"OOS PnL: {result.aggregate_oos_pnl:.4f}")
print(f"IS PnL:  {result.aggregate_is_pnl:.4f}")
print(f"WFER:    {result.wfer:.3f}")
print(f"OOS Sharpe: {result.oos_sharpe:.2f}")
print(f"OOS MaxDD:  {result.oos_max_dd:.2%}")
print(f"Degradation: {result.degradation_rate:.5f}")
print(f"Passed:  {result.passed}")

for w in result.windows:
    print(f"  Window {w.window_id}: IS={w.is_pnl:.4f} OOS={w.oos_pnl:.4f} "
          f"WFER={w.wfer:.2f} params={w.best_params}")

تفسير النتائج: متى تثق ومتى ترفض

الاستراتيجية اجتازت WFO

إذا كان WFER >= 0.5 عبر جميع النوافذ، و OOS PnL إيجابي ومستقر:

Window 0: IS=0.0812  OOS=0.0531  WFER=0.65  params={'fast': 10, 'slow': 50}
Window 1: IS=0.0744  OOS=0.0489  WFER=0.66  params={'fast': 10, 'slow': 50}
Window 2: IS=0.0698  OOS=0.0401  WFER=0.57  params={'fast': 15, 'slow': 50}
Window 3: IS=0.0823  OOS=0.0512  WFER=0.62  params={'fast': 10, 'slow': 60}
Window 4: IS=0.0756  OOS=0.0478  WFER=0.63  params={'fast': 10, 'slow': 50}
→ Aggregate WFER: 0.63, all windows > 0.5, parameters are stable

علامات جيدة:

  • WFER مستقر عبر النوافذ (بدون قفزات حادة)
  • المعلمات متشابهة بين النوافذ (fast = 10-15, slow = 50-60)
  • OOS PnL إيجابي في معظم النوافذ
  • معدل التدهور قريب من الصفر

الاستراتيجية فشلت في WFO

Window 0: IS=0.2341  OOS=-0.0312  WFER=-0.13  params={'fast': 5, 'slow': 95}
Window 1: IS=0.1987  OOS=0.0089   WFER=0.04   params={'fast': 25, 'slow': 30}
Window 2: IS=0.2156  OOS=-0.0567  WFER=-0.26  params={'fast': 10, 'slow': 90}
Window 3: IS=0.1834  OOS=0.0234   WFER=0.13   params={'fast': 20, 'slow': 40}
→ Aggregate WFER: -0.07, IS is high, OOS is near zero → overfitting

علامات الإفراط في التخصيص:

  • IS PnL عالي، OOS PnL منخفض/سلبي — إفراط كلاسيكي في التخصيص
  • المعلمات تتباين بشكل كبير بين النوافذ — لا يوجد أمثل مستقر
  • WFER < 0.3 في معظم النوافذ — المعلمات لا تنتقل
  • معدل التدهور سلبي بشدة — تدهور سريع

المزيد عن تحليل استقرار المعلمات — في مقال تحليل الهضبة. إذا كان الأمثل "حاداً" (ينخفض بشدة مع تغييرات صغيرة في المعلمات) — هذه إشارة إضافية للإفراط في التخصيص.

خصوصيات WFO للعملات المشفرة

تصور خصوصيات WFO للعملات المشفرة

تخلق العملات المشفرة مشاكل فريدة لـ WFO لا توجد في الأسواق التقليدية.

تحولات النظام

يتحول سوق العملات المشفرة بين أنظمة مختلفة جذرياً: اتجاه صعودي، اتجاه هبوطي، حركة جانبية بتقلبات عالية/منخفضة. المعلمات المثلى في نظام واحد قد تكون غير مربحة في نظام آخر.

الحل: استخدام WFO المتدحرج (وليس المُرتكز) بنافذة 4-6 أشهر. هذا يسمح بـ"نسيان" الأنظمة القديمة. بالإضافة إلى ذلك — تجميع البيانات حسب التقلب وتشغيل WFO بشكل منفصل لكل مجموعة.

تاريخ قصير

معظم العملات البديلة لديها أقل من 3 سنوات من تاريخ التداول. مع تدريب = 6 أشهر واختبار = 2 شهر، ستحصل على 4-5 نوافذ فقط — تقدير ضعيف إحصائياً.

الحل: استخدام CPCV بدلاً من أو بالإضافة إلى WFO المتدحرج. CPCV يولّد المزيد من التوليفات من نفس البيانات. لـ 10 مجموعات و k=2: 45 توليفة بدلاً من 4-5 نوافذ.

تغييرات هيكلية في السيولة

سيولة أزواج العملات المشفرة غير مستقرة: يمكن أن يكون الزوج سائلاً لمدة 6 أشهر، ثم تنخفض الأحجام 10 مرات. المعلمات المُحسّنة على سوق سائل لا تعمل على سوق غير سائل.

الحل: إضافة مرشح سيولة إلى خط أنابيب WFO. استبعاد النوافذ التي يكون فيها متوسط الحجم اليومي أقل من العتبة. التحقق من أن السيولة في فترة الاختبار قابلة للمقارنة مع فترة التدريب.

تأثير Funding Rate

لاستراتيجيات العقود الآجلة ذات الرافعة المالية، يمكن لمعدلات التمويل أن تغير نتائج OOS بشكل جذري. تُظهر الاستراتيجية +5% OOS على مدى شهرين، لكن برافعة مالية 10 أضعاف، يأكل التمويل 3.6%.

تحليل مفصل لتأثير معدل التمويل — في مقالنا معدلات التمويل تقتل رافعتك المالية. تأكد من احتساب تكاليف التمويل عند تقييم OOS PnL في WFO.

استراتيجيات متعددة المعلمات: لماذا WFO حاسم مع 12+ معلمة

لعنة الأبعاد في التحسين متعدد المعلمات

استراتيجية بـ 21 معلمة (12 فصل + 9 وصفية) على 25 شهراً من بيانات زوج واحد هي نموذج بمساحة بحث هائلة.

لعنة الأبعاد

عدد توليفات المعلمات ينمو أسياً مع عدد المعلمات:

التوليفات=i=1nPi\text{التوليفات} = \prod_{i=1}^{n} |P_i|

إذا أخذت كل من المعلمات الـ 21 ما لا يقل عن 10 قيم:

1021=10 سكستليون توليفة10^{21} = 10\ \text{سكستليون توليفة}

حتى مع التحسين البايزي (التفاصيل في النزول الإحداثي مقابل البايزي)، تستكشف جزءاً ضئيلاً من المساحة. احتمال أن يكون الأمثل المُكتشف قطعة ضوضاء وليس نمطاً حقيقياً ينمو مع عدد المعلمات.

صيغة Bonferroni للمقارنات المتعددة

إذا اختبرت MM توليفة معلمات، فاحتمال "اكتشاف" كاذب (إيجاد نتيجة جيدة بالصدفة):

P(اكتشاف كاذب)=1(1α)M1eαMP(\text{اكتشاف كاذب}) = 1 - (1 - \alpha)^M \approx 1 - e^{-\alpha M}

عند α=0.05\alpha = 0.05 و M=10000M = 10000 توليفة مُجرّبة:

P1e5001.0P \approx 1 - e^{-500} \approx 1.0

مضمون أنك ستجد معلمات "تعمل" — لكنها في الحقيقة مُلائمة للضوضاء. بدون WFO، لا توجد طريقة للتمييز بين ميزة حقيقية وقطعة إحصائية.

القاعدة: عدد نقاط بيانات OOS مقابل عدد المعلمات

قاعدة عامة للثقة في نتائج WFO:

صفقات OOSالمعلمات>10\frac{\text{صفقات OOS}}{\text{المعلمات}} > 10

لـ 21 معلمة، تحتاج إلى 210 صفقة OOS على الأقل. إذا أنتج WFO أقل من ذلك — لا يمكن الوثوق بالنتيجة.

استراتيجية +3342% PnL@ML: 21 معلمة، 25 شهراً من البيانات. بافتراض 5 نوافذ OOS من 60 يوماً، صفقتان يومياً — المجموع 5×60×2=6005 \times 60 \times 2 = 600 صفقة OOS. النسبة 600/21=28.6600/21 = 28.6 — مقبولة، لكن فقط إذا كان WFER > 0.5.

دمج WFO مع Optuna

تكامل التحسين البايزي مع Optuna

في كل نافذة WFO، تحتاج إلى تحسين المعلمات. لـ 21 معلمة، البحث الشبكي مستحيل، والنزول الإحداثي غير فعال. الخيار الأمثل هو التحسين البايزي عبر Optuna.

import optuna
from optuna.samplers import TPESampler

def optuna_optimize(train_data: np.ndarray, n_trials: int = 500) -> tuple:
    """
    Optimize strategy parameters using Optuna.
    Used inside each WFO window.
    """

    def objective(trial):
        fast = trial.suggest_int("fast_period", 3, 50)
        slow = trial.suggest_int("slow_period", 20, 200)
        atr_period = trial.suggest_int("atr_period", 5, 50)
        atr_mult = trial.suggest_float("atr_multiplier", 0.5, 4.0)
        rsi_period = trial.suggest_int("rsi_period", 5, 30)
        rsi_upper = trial.suggest_int("rsi_upper", 60, 85)
        rsi_lower = trial.suggest_int("rsi_lower", 15, 40)
        vol_window = trial.suggest_int("vol_window", 10, 100)
        position_size = trial.suggest_float("position_size", 0.1, 1.0)
        take_profit = trial.suggest_float("take_profit", 0.005, 0.05)
        stop_loss = trial.suggest_float("stop_loss", 0.003, 0.03)
        trailing_pct = trial.suggest_float("trailing_pct", 0.002, 0.02)

        if fast >= slow:
            return -1e6  # invalid combination

        params = {
            "fast_period": fast, "slow_period": slow,
            "atr_period": atr_period, "atr_multiplier": atr_mult,
            "rsi_period": rsi_period, "rsi_upper": rsi_upper,
            "rsi_lower": rsi_lower, "vol_window": vol_window,
            "position_size": position_size,
            "take_profit": take_profit, "stop_loss": stop_loss,
            "trailing_pct": trailing_pct,
        }

        pnl, _ = run_strategy(train_data, params)

        _, returns = run_strategy(train_data, params)
        if len(returns) < 30 or np.std(returns) == 0:
            return -1e6
        sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252)
        return sharpe

    optuna.logging.set_verbosity(optuna.logging.WARNING)

    study = optuna.create_study(
        direction="maximize",
        sampler=TPESampler(seed=42),
    )
    study.optimize(objective, n_trials=n_trials, show_progress_bar=False)

    best_params = study.best_params
    best_pnl, _ = run_strategy(train_data, best_params)

    return best_params, best_pnl

wfo = WalkForwardOptimizer(
    data=prices,
    optimize_fn=optuna_optimize,     # Optuna instead of grid search
    evaluate_fn=my_evaluate,
    mode="rolling",
    train_size=180,
    test_size=60,
    step_size=60,
)

result = wfo.run()

مهم: داخل WFO، حسّن Sharpe، وليس PnL. تحسين PnL يجد معلمات تُعظّم الربح على تسلسل محدد من الصفقات. تحسين Sharpe يجد معلمات بأفضل نسبة عائد إلى مخاطرة — وهي أكثر متانة OOS.

مقارنة تفصيلية بين Optuna TPE والنزول الإحداثي — في مقال النزول الإحداثي مقابل البايزي.

تصور نتائج WFO

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

def plot_wfo_results(result: WFOResult, data: np.ndarray):
    """Visualize Walk-Forward Optimization results."""
    fig, axes = plt.subplots(3, 1, figsize=(16, 14))

    ax = axes[0]
    ax.plot(result.oos_equity, color='#4FC3F7', linewidth=1.5)
    ax.axhline(1.0, color='#FF5252', linestyle='--', alpha=0.5, label='Break-even')
    ax.set_title(f'OOS Equity Curve (WFER={result.wfer:.2f}, Sharpe={result.oos_sharpe:.2f})')
    ax.set_ylabel('Equity')
    ax.legend()
    ax.grid(True, alpha=0.3)

    ax = axes[1]
    wfers = [w.wfer for w in result.windows]
    colors = ['#69F0AE' if w >= 0.5 else '#FFAB40' if w >= 0.3 else '#FF5252'
              for w in wfers]
    ax.bar(range(len(wfers)), wfers, color=colors, edgecolor='#1A237E', alpha=0.8)
    ax.axhline(0.5, color='#E040FB', linestyle='--', label='Threshold (0.5)')
    ax.axhline(0, color='gray', linestyle='-', alpha=0.3)
    ax.set_title('Walk-Forward Efficiency Ratio by Window')
    ax.set_xlabel('Window')
    ax.set_ylabel('WFER')
    ax.legend()

    ax = axes[2]
    x = np.arange(len(result.windows))
    width = 0.35
    ax.bar(x - width/2, [w.is_pnl for w in result.windows],
           width, label='IS PnL', color='#7C4DFF', alpha=0.7)
    ax.bar(x + width/2, [w.oos_pnl for w in result.windows],
           width, label='OOS PnL', color='#4FC3F7', alpha=0.7)
    ax.set_title('In-Sample vs Out-of-Sample PnL')
    ax.set_xlabel('Window')
    ax.set_ylabel('PnL')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('wfo_results.png', dpi=150)
    plt.show()

توصيات عملية

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

1. تشغيل WFO (متدحرج + مُرتكز)

قارن نتائج كلا الوضعين. إذا فشل WFO المتدحرج لكن نجح المُرتكز — على الأرجح الاستراتيجية تعمل فقط على البيانات المبكرة.

2. تحقق من WFER لكل نافذة

ليس فقط WFER المُجمّع، بل كل نافذة على حدة. إذا كان 2 من 6 نوافذ لديها WFER < 0 — هذه مشكلة، حتى لو كان المجمّع > 0.5.

3. قارن المعلمات بين النوافذ

إذا "قفزت" المعلمات المثلى من نافذة إلى أخرى — لا توجد ميزة مستقرة. استخدم تحليل الهضبة للتحقق من استقرار الأمثل.

4. تحقق من معدل التدهور

معدل تدهور سلبي بشدة = المعلمات تفقد فعاليتها بسرعة. تحتاج إلى إعادة تحسين أكثر تكراراً أو إعادة هيكلة الاستراتيجية.

5. تطبيق مونت كارلو بوتستراب على نتائج OOS

OOS PnL المُجمّع هو أيضاً تقدير نقطي واحد. طبّق مونت كارلو بوتستراب على مصفوفة عوائد OOS للحصول على فترات الثقة.

6. احتساب التكاليف

يجب أن يتضمن OOS PnL العمولات، الانزلاق السعري، ومعدلات التمويل. OOS PnL جميل بدون تكاليف هو وهم. المزيد من التفاصيل — معدلات التمويل تقتل رافعتك المالية.

الحد الأدنى لمتطلبات البيانات

عدد المعلمات الحد الأدنى لصفقات OOS الحد الأدنى لنوافذ WFO الحد الأدنى للبيانات (صفقتان/يوم)
2-5 50 3 ~6 أشهر
6-10 100 4 ~12 شهراً
11-15 150 5 ~18 شهراً
16-21 210 6 ~24 شهراً
22+ +300 +8 ~+36 شهراً

الاستراتيجية بـ 21 معلمة و25 شهراً من البيانات

لنعد إلى السؤال من بداية المقال: 21 معلمة مُحسّنة على 25 شهراً من بيانات زوج واحد. PnL@ML = +3342%. كيف نتحقق؟

الخطوة 1. WFO متدحرج: تدريب = 8 أشهر، اختبار = 2 شهر، خطوة = 2 شهر. نحصل على ~8 نوافذ.

الخطوة 2. WFO مُرتكز: أول تدريب = 8 أشهر، اختبار = 2 شهر. نحصل على ~8 نوافذ.

الخطوة 3. CPCV: 10 مجموعات من ~2.5 شهر، k = 2. نحصل على 45 توليفة.

الخطوة 4. لكل طريقة، تحقق من:

  • WFER >= 0.5؟
  • المعلمات مستقرة بين النوافذ؟
  • معدل التدهور مقبول؟
  • صفقات OOS / المعلمات >= 10؟

الخطوة 5. مونت كارلو بوتستراب على عوائد OOS المُجمّعة. النسبة المئوية الخامسة لـ PnL > 0؟

إذا فشل أي من هذه الاختبارات — فالاستراتيجية بـ +3342% على الأرجح مُفرطة في التخصيص. 21 معلمة على 25 شهراً من زوج واحد — هذه نسبة معلمات إلى بيانات مرتفعة للغاية. بدون اجتياز WFO، لا يمكن الوثوق بها.

نوصي أيضاً بالتحقق من كفاءة الاستراتيجية مع مراعاة PnL حسب الوقت النشط — سيكشف هذا عن أي جزء من +3342% ناتج عن وقت الاحتفاظ بالمركز مقابل الميزة الحقيقية.

الخلاصة

تحسين المشي للأمام ليس اختيارياً — إنه ضرورة. إنها الطريقة الوحيدة التي تتحقق بشكل منهجي من قابلية نقل المعلمات إلى بيانات جديدة. التقسيم الواحد للتدريب/الاختبار هو يانصيب. الاختبار الرجعي الكامل على جميع البيانات هو خداع للذات.

النقاط الرئيسية:

  1. WFER < 0.5 = إفراط في التخصيص. إذا كان PnL خارج العينة أقل من نصف داخل العينة — فالمعلمات مُلائمة.

  2. استقرار المعلمات أهم من الحد الأقصى. معلمات تحقق +15% في كل نافذة أفضل من معلمات تحقق +40% في واحدة و-10% في أخرى.

  3. WFO المتدحرج للعملات المشفرة. تحولات النظام تجعل WFO المُرتكز أقل موثوقية. نافذة متدحرجة 4-6 أشهر هي التوازن الأمثل.

  4. كلما زادت المعلمات — زادت صرامة المتطلبات. 21 معلمة تتطلب ما لا يقل عن 210 صفقة OOS و6+ نوافذ WFO. بدون هذا، لا يمكن التحقق من النتيجة.

  5. WFO + مونت كارلو بوتستراب + تحليل الهضبة — ثلاث طبقات من الحماية ضد الإفراط في التخصيص. كل طبقة تلتقط ما تفوته الأخريات.

استراتيجية تجتاز WFO بـ WFER > 0.5 عبر جميع النوافذ، ومعلمات مستقرة، وبوتستراب إيجابي عند النسبة المئوية الخامسة — هذه استراتيجية يمكنك أن تأتمنها على أموال حقيقية. كل ما عداها هو ملاءمة منحنيات بمنحنى حقوق ملكية جميل.


روابط مفيدة

  1. Pardo, R. — The Evaluation and Optimization of Trading Strategies (Wiley)
  2. Lopez de Prado, M. — Advances in Financial Machine Learning, Chapter 12: Backtesting
  3. Bailey, D.H. et al. — The Probability of Backtest Overfitting
  4. Lopez de Prado, M. — The Combinatorial Purged Cross-Validation (CPCV)
  5. Aronson, D.R. — Evidence-Based Technical Analysis
  6. Optuna: A Next-generation Hyperparameter Optimization Framework
  7. Kevin Davey — Building Winning Algorithmic Trading Systems: Walk-Forward Analysis
  8. White, H. — A Reality Check for Data Snooping (2000)
  9. Harvey, C.R. & Liu, Y. — Backtesting (2015)
  10. NumPy — numpy.cumprod

Citation

@article{soloviov2026walkforwardoptimization,
  author = {Soloviov, Eugen},
  title = {Walk-Forward Optimization: The Only Honest Strategy Test},
  year = {2026},
  url = {https://marketmaker.cc/en/blog/post/walk-forward-optimization},
  version = {0.1.0},
  description = {Why a single train/test split does not protect against overfitting, how walk-forward optimization systematically verifies parameter robustness, and why a strategy with +3342\% PnL@ML on 21 parameters is a ticking time bomb without WFO.}
}
blog.disclaimer

MarketMaker.cc Team

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

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

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

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

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