تحسين المشي للأمام: الاختبار الصادق الوحيد للاستراتيجية
لقد قمت بتحسين استراتيجية. 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 المُرتكز، بداية فترة التدريب ثابتة، ونهايتها تتوسع مع كل نافذة:
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. تُقسم البيانات إلى مجموعة، يُختار منها للاختبار. الفرق الرئيسي عن التحقق المتقاطع القياسي هو التنقية (إزالة البيانات عند حد التدريب/الاختبار) والحظر (فجوة إضافية لمنع تسرب البيانات):
مع : 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 الرئيسية

طول فترة التدريب
فترة تدريب قصيرة جداً — بيانات غير كافية لتحسين موثوق. طويلة جداً — البيانات القديمة تُخفف الأنماط الحالية.
قاعدة عامة: يجب أن يحتوي التدريب على 200-300 صفقة على الأقل. إذا كانت الاستراتيجية تُنفذ صفقتين يومياً:
للعملات المشفرة مع تحولات نظامها، نوصي بعدم تجاوز 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 | التفسير |
|---|---|
| > 0.8 | متانة ممتازة. المعلمات تنتقل إلى بيانات جديدة. |
| 0.5 — 0.8 | متانة مقبولة. الاستراتيجية تعمل لكن مع تدهور. |
| 0.3 — 0.5 | حالة حدية. إفراط جزئي في التخصيص محتمل. |
| < 0.3 | إفراط في التخصيص. المعلمات مُلائمة لبيانات IS. |
| < 0 | الاستراتيجية غير مربحة OOS. إفراط كامل في التخصيص أو خطأ منطقي. |
إذا كان WFER < 0.5 — فالاستراتيجية على الأرجح مُفرطة في التخصيص. هذا هو مُرشحنا الأساسي.
معدل التدهور
يُظهر مدى سرعة فقدان المعلمات المثلى لفعاليتها بمرور الوقت:
عملياً: قسّم فترة الاختبار إلى فترات فرعية وتتبع ديناميكيات 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

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 المتدحرج (وليس المُرتكز) بنافذة 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 شهراً من بيانات زوج واحد هي نموذج بمساحة بحث هائلة.
لعنة الأبعاد
عدد توليفات المعلمات ينمو أسياً مع عدد المعلمات:
إذا أخذت كل من المعلمات الـ 21 ما لا يقل عن 10 قيم:
حتى مع التحسين البايزي (التفاصيل في النزول الإحداثي مقابل البايزي)، تستكشف جزءاً ضئيلاً من المساحة. احتمال أن يكون الأمثل المُكتشف قطعة ضوضاء وليس نمطاً حقيقياً ينمو مع عدد المعلمات.
صيغة Bonferroni للمقارنات المتعددة
إذا اختبرت توليفة معلمات، فاحتمال "اكتشاف" كاذب (إيجاد نتيجة جيدة بالصدفة):
عند و توليفة مُجرّبة:
مضمون أنك ستجد معلمات "تعمل" — لكنها في الحقيقة مُلائمة للضوضاء. بدون WFO، لا توجد طريقة للتمييز بين ميزة حقيقية وقطعة إحصائية.
القاعدة: عدد نقاط بيانات OOS مقابل عدد المعلمات
قاعدة عامة للثقة في نتائج WFO:
لـ 21 معلمة، تحتاج إلى 210 صفقة OOS على الأقل. إذا أنتج WFO أقل من ذلك — لا يمكن الوثوق بالنتيجة.
استراتيجية +3342% PnL@ML: 21 معلمة، 25 شهراً من البيانات. بافتراض 5 نوافذ OOS من 60 يوماً، صفقتان يومياً — المجموع صفقة OOS. النسبة — مقبولة، لكن فقط إذا كان WFER > 0.5.
دمج WFO مع 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% ناتج عن وقت الاحتفاظ بالمركز مقابل الميزة الحقيقية.
الخلاصة
تحسين المشي للأمام ليس اختيارياً — إنه ضرورة. إنها الطريقة الوحيدة التي تتحقق بشكل منهجي من قابلية نقل المعلمات إلى بيانات جديدة. التقسيم الواحد للتدريب/الاختبار هو يانصيب. الاختبار الرجعي الكامل على جميع البيانات هو خداع للذات.
النقاط الرئيسية:
-
WFER < 0.5 = إفراط في التخصيص. إذا كان PnL خارج العينة أقل من نصف داخل العينة — فالمعلمات مُلائمة.
-
استقرار المعلمات أهم من الحد الأقصى. معلمات تحقق +15% في كل نافذة أفضل من معلمات تحقق +40% في واحدة و-10% في أخرى.
-
WFO المتدحرج للعملات المشفرة. تحولات النظام تجعل WFO المُرتكز أقل موثوقية. نافذة متدحرجة 4-6 أشهر هي التوازن الأمثل.
-
كلما زادت المعلمات — زادت صرامة المتطلبات. 21 معلمة تتطلب ما لا يقل عن 210 صفقة OOS و6+ نوافذ WFO. بدون هذا، لا يمكن التحقق من النتيجة.
-
WFO + مونت كارلو بوتستراب + تحليل الهضبة — ثلاث طبقات من الحماية ضد الإفراط في التخصيص. كل طبقة تلتقط ما تفوته الأخريات.
استراتيجية تجتاز WFO بـ WFER > 0.5 عبر جميع النوافذ، ومعلمات مستقرة، وبوتستراب إيجابي عند النسبة المئوية الخامسة — هذه استراتيجية يمكنك أن تأتمنها على أموال حقيقية. كل ما عداها هو ملاءمة منحنيات بمنحنى حقوق ملكية جميل.
روابط مفيدة
- Pardo, R. — The Evaluation and Optimization of Trading Strategies (Wiley)
- Lopez de Prado, M. — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Bailey, D.H. et al. — The Probability of Backtest Overfitting
- Lopez de Prado, M. — The Combinatorial Purged Cross-Validation (CPCV)
- Aronson, D.R. — Evidence-Based Technical Analysis
- Optuna: A Next-generation Hyperparameter Optimization Framework
- Kevin Davey — Building Winning Algorithmic Trading Systems: Walk-Forward Analysis
- White, H. — A Reality Check for Data Snooping (2000)
- Harvey, C.R. & Liu, Y. — Backtesting (2015)
- 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.}
}
MarketMaker.cc Team
البحوث والاستراتيجيات الكمية