مونت كارلو بوتستراب: كيف تحصل على فترات الثقة لاختبار رجعي في 10 أسطر من الكود
قمت بتشغيل استراتيجية عبر اختبار رجعي. حصلت على PnL +42%، وSharpe 1.8، وMaxDD -12%. تبدو النتائج رائعة. أطلقت الروبوت في الإنتاج، وبعد شهر اكتشفت أن التراجع وصل بالفعل إلى -28% وأن PnL يتجه نحو الصفر.
ما الخطأ الذي حدث؟ ليس خللاً برمجياً ولا "تغيّراً في السوق". المشكلة هي أنك اتخذت قرارك بناءً على رقم واحد — تقدير نقطي وحيد. علمت أن الاستراتيجية أظهرت +42%، لكنك لم تعلم مدى إمكانية الوثوق بهذا الرقم.
مشكلة التقديرات النقطية الوحيدة
نقطة بيانات واحدة (يسار) تعطي صورة مضللة، بينما التوزيع الكامل (يمين) يكشف النطاق الحقيقي للنتائج المحتملة.
الاختبار الرجعي على البيانات التاريخية هو تشغيل واحد عبر تسلسل محدد من أحداث السوق. النتيجة تعتمد على ترتيب الصفقات: نفس الاستراتيجية بنفس الصفقات، لكن بترتيب مختلف، يمكن أن تُظهر حداً أقصى مختلفاً تماماً للتراجع.
تخيل 491 صفقة. كل صفقة هي حدث عشوائي بتوزيع عوائد معين. الاختبار الرجعي التاريخي يُظهر تحقّقاً واحداً فقط من هذه العملية. إنه مثل رمي حجر نرد مرة واحدة والاستنتاج بأن الحجر يسقط دائماً على أربعة.
ما نحتاجه فعلياً:
- ليس تقديراً نقطياً، بل فترة: "بنسبة 95%، سيكون PnL النهائي بين X وY"
- ليس حداً أقصى واحداً للتراجع، بل توزيعاً: "في أسوأ 5% من السيناريوهات، يتجاوز التراجع Z%"
- ليس المتوسط، بل الأطراف: ماذا يحدث إذا لم يكن الحظ في صفّك؟
هذا بالضبط ما يفعله مونت كارلو بوتستراب.
ما هو مونت كارلو بوتستراب
يولّد البوتستراب آلاف المسارات البديلة لرأس المال عن طريق إعادة أخذ العينات مع الإرجاع من مجموعة البيانات الأصلية.
البوتستراب هو طريقة إعادة أخذ العينات اقترحها Bradley Efron في عام 1979. الفكرة أنيقة: إذا كانت لدينا عينة بيانات، يمكننا توليد آلاف العينات "الجديدة" عن طريق اختيار عناصر عشوائياً من الأصلية مع الإرجاع.
في سياق الاختبار الرجعي، يعمل هكذا:
- لديك مصفوفة عوائد لكل صفقة — مثلاً 491 قيمة
- تختار عشوائياً 491 قيمة من هذه المصفوفة مع الإرجاع — بعض الصفقات ستظهر مرتين، وبعضها لن يظهر أبداً
- تبني منحنى رأس المال من هذه العينة الجديدة
- تكرر ذلك 10,000 مرة
- تحصل على توزيع للمقاييس النهائية، وليس رقماً واحداً
كل تكرار هو "سيناريو بديل" واحد: ما كان يمكن أن يحدث لو كان ترتيب ومجموعة الصفقات مختلفاً قليلاً.
التنفيذ في 10 أسطر
إليك التنفيذ الكامل العامل:
import numpy as np
def max_drawdown(equity_curve):
"""Calculate the maximum drawdown of an equity curve."""
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
return drawdown.min()
trade_returns = [...] # 491 values, e.g. [0.012, -0.005, 0.008, ...]
n_simulations = 10000
results = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
"sharpe": np.mean(sampled) / np.std(sampled) * np.sqrt(252)
})
وقت التنفيذ: ~ثانيتان على حاسوب محمول عادي. 10,000 تاريخ بديل لاستراتيجيتك.
استخراج فترات الثقة
فترات الثقة لمقاييس الاستراتيجية الرئيسية: PnL وMaxDD وSharpe Ratio، تُظهر نطاقات المئوي الخامس (الأسوأ) والخمسين (الوسيط) والخامس والتسعين (الأفضل).
الآن لدينا ليس رقماً واحداً، بل توزيع. إليك كيفية استخراج المعلومات المفيدة منه:
import pandas as pd
df = pd.DataFrame(results)
pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)
dd_5 = np.percentile(df['max_dd'], 5) # 5th — worst case
dd_50 = np.percentile(df['max_dd'], 50)
dd_95 = np.percentile(df['max_dd'], 95) # 95th — best case
print(f"PnL: {pnl_5:.1%} | {pnl_50:.1%} | {pnl_95:.1%}")
print(f"MaxDD: {dd_5:.1%} | {dd_50:.1%} | {dd_95:.1%}")
print(f"Sharpe: {np.percentile(df['sharpe'], 5):.2f} — {np.percentile(df['sharpe'], 95):.2f}")
مثال على المخرجات لاستراتيجية حقيقية:
| المقياس | المئوي الخامس (الأسوأ) | الوسيط | المئوي الـ 95 (الأفضل) |
|---|---|---|---|
| PnL | +18.3% | +41.7% | +72.1% |
| MaxDD | -23.4% | -12.8% | -5.1% |
| Sharpe | 1.12 | 1.76 | 2.41 |
الآن الفرق واضح:
- أظهر الاختبار الرجعي PnL +42% — لكن في أسوأ 5% من السيناريوهات، PnL هو +18.3% فقط
- أظهر الاختبار الرجعي MaxDD -12% — لكن في أسوأ 5% من السيناريوهات، التراجع هو -23.4%
- Sharpe 1.8 — لكن الحد الأدنى هو 1.12
المئوي الخامس هو "أسوأ حالة واقعية" لديك. إذا توقفت الاستراتيجية عن الربحية عند المئوي الخامس، فإن إطلاقها في الإنتاج محفوف بالمخاطر.
التصوير البياني: مخطط المروحة
يُصوَّر مونت كارلو بوتستراب بشكل طبيعي كـمخطط مروحة — مروحة من منحنيات رأس المال:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
ax = axes[0]
for i in range(min(500, n_simulations)):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
ax.plot(equity, alpha=0.02, color='#4FC3F7')
all_equities = []
for _ in range(n_simulations):
sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
equity = np.cumprod(1 + sampled)
all_equities.append(equity)
all_equities = np.array(all_equities)
p5 = np.percentile(all_equities, 5, axis=0)
p50 = np.percentile(all_equities, 50, axis=0)
p95 = np.percentile(all_equities, 95, axis=0)
ax.fill_between(range(len(p5)), p5, p95, alpha=0.3, color='#7C4DFF', label='90% CI')
ax.plot(p50, color='#E040FB', linewidth=2, label='Median')
ax.set_title('Monte Carlo Bootstrap: Equity Curves')
ax.legend()
ax = axes[1]
ax.hist(df['final_pnl'] * 100, bins=80, color='#4FC3F7', alpha=0.7, edgecolor='#1A237E')
ax.axvline(pnl_5 * 100, color='#FF5252', linestyle='--', label=f'5th: {pnl_5:.1%}')
ax.axvline(pnl_50 * 100, color='#E040FB', linestyle='--', label=f'Median: {pnl_50:.1%}')
ax.axvline(pnl_95 * 100, color='#69F0AE', linestyle='--', label=f'95th: {pnl_95:.1%}')
ax.set_title('Distribution of Final PnL')
ax.set_xlabel('PnL, %')
ax.legend()
plt.tight_layout()
plt.savefig('monte_carlo_fan_chart.png', dpi=150)
plt.show()
يوفر مخطط المروحة فهماً بديهياً لـمدى انتشار النتائج المحتملة. مروحة ضيقة تعني أن الاستراتيجية مستقرة. مروحة واسعة تعني أن النتيجة تعتمد بشكل كبير على "الحظ" في تسلسل الصفقات.
مخطط المروحة (يسار) يُظهر انتشار مسارات رأس المال المحتملة، والمدرج التكراري (يمين) يُظهر كثافة توزيع العوائد النهائية مع تمييز فترات الثقة (5%، 50%، 95%).
التحليل المتقدم: احتمال الإفلاس
يسمح لك البوتستراب بالإجابة على سؤال حاسم: ما هو احتمال أن تخسر الاستراتيجية X% من رأس المال؟
ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")
prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")
worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")
هذه المقاييس مستحيل الحصول عليها من تشغيل اختبار رجعي واحد. ومع ذلك فهي حاسمة لاتخاذ قرار إطلاق الاستراتيجية.
لمزيد من المعلومات حول لماذا التراجعات العميقة خطيرة رياضياً وكيف تعمل عدم تماثل العوائد، اقرأ مقالتنا عدم تماثل الخسارة والربح.
عندما لا يعمل البوتستراب الكلاسيكي
للطريقة قيود من المهم معرفتها.
الارتباط الذاتي للعوائد
يفترض البوتستراب الكلاسيكي أن الصفقات مستقلة. في الواقع، هذا غالباً ليس كذلك — يمكن أن يكون للاستراتيجية سلاسل من المكاسب والخسائر. إذا كان الارتباط الذاتي كبيراً، استخدم البوتستراب الكتلي:
def block_bootstrap(returns, block_size=10, n_simulations=10000):
"""Bootstrap preserving local dependency structure."""
n = len(returns)
results = []
for _ in range(n_simulations):
starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
sampled = np.concatenate([returns[s:s+block_size] for s in starts])[:n]
equity = np.cumprod(1 + sampled)
results.append({
"final_pnl": equity[-1] - 1,
"max_dd": max_drawdown(equity),
})
return pd.DataFrame(results)
يحافظ البوتستراب الكتلي على التبعيات المحلية بين الصفقات المتتالية، مما يوفر فترات ثقة أكثر واقعية لـ MaxDD.
عدم استقرارية السوق
يعمل البوتستراب مع توزيع الصفقات الأصلي. إذا تغير السوق هيكلياً (مثلاً انخفض التقلب أو تغيرت السيولة)، فقد تكون الصفقات التاريخية غير ممثلة. لمراعاة ذلك:
- استخدم نافذة متحركة: البوتستراب فقط على آخر N صفقة
- أعطِ وزناً أكبر للصفقات الأخيرة: البوتستراب الموزون
- قسّم البيانات حسب أنظمة السوق وقم بالبوتستراب بشكل منفصل
عدد صفقات قليل
البوتستراب موثوق عندما يكون n > 30 صفقة. إذا كان لديك 10 صفقات — لا كمية من إعادة أخذ العينات ستساعد. 491 صفقة عينة ممتازة؛ يمكنك الوثوق بالنتائج.
مقارنة مناهج تقييم متانة الاختبار الرجعي
| الطريقة | ما تقدمه | التعقيد | الوقت | متى تُستخدم |
|---|---|---|---|---|
| اختبار رجعي واحد | تقدير نقطي واحد | أدنى | ثوانٍ | أبداً كنتيجة نهائية |
| Walk-forward | مقاييس خارج العينة | متوسط | دقائق | للتحقق من الإفراط في التخصيص |
| مونت كارلو بوتستراب | فترات الثقة | أدنى | ~ثانيتان | دائماً قبل الإنتاج |
| مسار مونت كارلو | مسارات أسعار جديدة | عالٍ | دقائق-ساعات | لاختبار الضغط |
| التحقق المتقاطع | متوسط المقاييس عبر الطيات | متوسط | دقائق | لضبط المعلمات |
مونت كارلو بوتستراب هو الطريقة الوحيدة التي تقدم في وقت أدنى صورة كاملة للمخاطر.
قائمة مراجعة: تفسير النتائج
إليك كيف نوصي بتفسير نتائج مونت كارلو بوتستراب:
الإطلاق في الإنتاج إذا:
- PnL عند المئوي الخامس إيجابي
- MaxDD عند المئوي الخامس مقبول لشهيتك للمخاطر
- احتمال الإفلاس < 1%
- Sharpe عند المئوي الخامس > 0.5
يحتاج إلى عمل إذا:
- PnL عند المئوي الخامس قريب من الصفر
- MaxDD عند المئوي الخامس أسوأ بكثير من المئوي الخمسين
- انتشار واسع لمخطط المروحة — الاستراتيجية غير مستقرة
لا تُطلق إذا:
- PnL عند المئوي الخامس سلبي
- احتمال الإفلاس > 5%
- فترة الثقة لـ Sharpe تتضمن 0
تجربتنا في marketmaker.cc
في marketmaker.cc، نطوّر محرك الاختبار الرجعي الخاص بنا، ومونت كارلو بوتستراب جزء لا يتجزأ من خط أنابيبنا. كل استراتيجية تمر عبر البوتستراب تلقائياً قبل الموافقة عليها للتداول المباشر.
دمجنا البوتستراب مباشرة في محرك الاختبار الرجعي: بعد التشغيل، لا تحصل فقط على PnL النهائي، بل تقريراً كاملاً بفترات الثقة، ومخطط المروحة، واحتمال الإفلاس، ومقارنة البوتستراب الكتلي مقابل القياسي. يستغرق هذا 2-3 ثوانٍ إضافية — ثمن زهيد لفهم المخاطر الحقيقية.
من تجربتنا: حوالي 30% من الاستراتيجيات التي تبدو جذابة بالتقدير النقطي الوحيد يتم تصفيتها بعد مونت كارلو بوتستراب. يصبح PnL عند المئوي الخامس سلبياً أو يتبين أن MaxDD غير مقبول. بدون البوتستراب، كانت هذه الاستراتيجيات ستذهب إلى الإنتاج وكانت على الأرجح ستؤدي إلى خسائر.
الخلاصة
مونت كارلو بوتستراب هو 10 أسطر من الكود وثانيتان من الحساب. يحوّل رقماً واحداً من اختبار رجعي إلى توزيع كامل بفترات ثقة. ربما يكون هذا أعلى عائد استثمار بين جميع أدوات التحليل الكمي:
- تكلفة دنيا: تنفيذ في 30 دقيقة
- عائد أقصى: فهم المخاطر الحقيقية للاستراتيجية
- بدون تبعيات: فقط NumPy
إذا لم تكن تستخدم البوتستراب بعد — أضفه إلى خط أنابيبك اليوم. إنه الطريقة الوحيدة لمعرفة مدى إمكانية الوثوق بنتائج اختبارك الرجعي.
References
- Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
- Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
- Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
- QuantStart — Monte Carlo Simulation for Backtest Analysis
- Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
- NumPy — numpy.random.choice
Citation
@software{soloviov2026montecarlobootstrap,
author = {Soloviov, Eugen},
title = {Monte Carlo Bootstrap: How to Get Confidence Intervals for a Backtest in 10 Lines of Code},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/monte-carlo-bootstrap-backtest},
version = {0.1.0},
description = {Why a single-point estimate from a backtest is a dangerous illusion. How Monte Carlo bootstrap in 2 seconds of computation gives you a 95\% confidence interval for PnL and MaxDD, and why this is a mandatory step before launching a strategy in production.}
}
MarketMaker.cc Team
البحوث والاستراتيجيات الكمية