मॉन्टे कार्लो बूटस्ट्रैप: सिर्फ 10 लाइनों के कोड में बैकटेस्ट के लिए कॉन्फिडेंस इंटरवल कैसे पाएं

बैकटेस्ट से मिला एक-अंक अनुमान (single-point estimate) एक खतरनाक भ्रम क्यों है। कैसे मॉन्टे कार्लो बूटस्ट्रैप महज 2 सेकंड की गणना में PnL और MaxDD के लिए 95% कॉन्फिडेंस इंटरवल देता है, और यह किसी स्ट्रैटेजी को प्रोडक्शन में लॉन्च करने से पहले एक अनिवार्य कदम क्यों है।

📄 यह लेख एक शोध पेपर में विकसित हो गया। यहां वर्णित बूटस्ट्रैप कॉन्फिडेंस इंटरवल को सीरियल डिपेंडेंस के अंतर्गत एक नियंत्रित कवरेज परीक्षण से गुजारा गया है (iid बनाम ट्रेड-लेवल बनाम ब्लॉक बूटस्ट्रैप, ठीक-ठीक ज्ञात सत्य के साथ 6,000 प्रयोग)। पेपर ऑनलाइन (इंटरैक्टिव वर्शन + PDF) bootstrap.marketmaker.cc पर पढ़ें, कोड और डेटा github.com/suenot/bootstrap-coverage पर उपलब्ध है।

आपने एक स्ट्रैटेजी को बैकटेस्ट में चलाया। आपको PnL +42%, Sharpe 1.8, MaxDD -12% मिला। नतीजे शानदार दिख रहे हैं। आप बॉट को प्रोडक्शन में लॉन्च करते हैं, और एक महीने बाद पता चलता है कि ड्रॉडाउन पहले ही -28% तक पहुंच चुका है और PnL शून्य की ओर बढ़ रहा है।

गलती कहां हुई? यह न कोई बग है, न "बदल गया बाजार"। असली समस्या यह है कि आपने फैसला एक ही आंकड़े के आधार पर लिया — एक-अंक अनुमान। आपको यह तो पता चला कि स्ट्रैटेजी ने +42% दिखाया, लेकिन यह पता नहीं चला कि उस आंकड़े पर कितना भरोसा किया जा सकता है।

एक-अंक अनुमानों की समस्या

A single point estimate versus a full probability distribution एक अकेला डेटा पॉइंट (बाएं) भ्रामक तस्वीर देता है, जबकि पूरी डिस्ट्रीब्यूशन (दाएं) संभावित परिणामों की असली रेंज उजागर करती है।

ऐतिहासिक डेटा पर किया गया बैकटेस्ट, बाजार की घटनाओं के एक खास क्रम से गुजरने वाला सिर्फ एक रन होता है। नतीजा ट्रेडों के क्रम पर निर्भर करता है: वही स्ट्रैटेजी, वही ट्रेड, लेकिन अलग क्रम में, बिल्कुल अलग अधिकतम ड्रॉडाउन दिखा सकती है।

491 ट्रेडों की कल्पना करें। हर ट्रेड एक निश्चित रिटर्न डिस्ट्रीब्यूशन वाली एक रैंडम घटना है। ऐतिहासिक बैकटेस्ट इस प्रोसेस का सिर्फ एक अवतरण (realization) दिखाता है। यह वैसा ही है जैसे एक बार पासा फेंककर यह निष्कर्ष निकाल लेना कि पासा हमेशा चार पर ही गिरता है।

हमें असल में जिस चीज की जरूरत है:

  • एक-अंक अनुमान नहीं, बल्कि एक इंटरवल: "95% संभावना के साथ, अंतिम PnL X और Y के बीच होगा"
  • एक अकेला अधिकतम ड्रॉडाउन नहीं, बल्कि एक डिस्ट्रीब्यूशन: "5% सबसे खराब परिदृश्यों में, ड्रॉडाउन Z% से अधिक हो जाता है"
  • औसत नहीं, बल्कि टेल्स: अगर किस्मत आपके साथ न हो तो क्या होगा?

बिल्कुल इसी काम के लिए मॉन्टे कार्लो बूटस्ट्रैप है।

मॉन्टे कार्लो बूटस्ट्रैप क्या है

Monte Carlo bootstrap resampling: thousands of alternative equity paths generated from trade data बूटस्ट्रैप मूल डेटासेट से ट्रेडों को रिप्लेसमेंट के साथ रीसैंपल करके हजारों वैकल्पिक इक्विटी पथ बनाता है।

बूटस्ट्रैप एक रीसैंपलिंग पद्धति है जिसे 1979 में ब्रैडली एफ्रॉन ने प्रस्तावित किया था। विचार सुरुचिपूर्ण है: अगर हमारे पास डेटा का एक सैंपल है, तो हम मूल सैंपल से रिप्लेसमेंट के साथ रैंडम तरीके से एलिमेंट चुनकर हजारों "नए" सैंपल बना सकते हैं।

बैकटेस्ट के संदर्भ में यह इस तरह काम करता है:

  1. आपके पास हर ट्रेड के लिए रिटर्न्स का एक ऐरे है — उदाहरण के लिए 491 वैल्यू
  2. आप इस ऐरे में से रिप्लेसमेंट के साथ रैंडम तरीके से 491 वैल्यू चुनते हैं — कुछ ट्रेड दो बार आएंगे, कुछ बिल्कुल नहीं आएंगे
  3. आप इस नए सैंपल से एक इक्विटी कर्व बनाते हैं
  4. आप इसे 10,000 बार दोहराते हैं
  5. आपको अंतिम मेट्रिक्स की एक डिस्ट्रीब्यूशन मिलती है, न कि एक अकेला आंकड़ा

हर पुनरावृत्ति एक "वैकल्पिक परिदृश्य" है: क्या हो सकता था अगर ट्रेडों का क्रम और सेट थोड़ा अलग होता।

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()

# Array of returns for each trade
trade_returns = [...]  # 491 values, e.g. [0.012, -0.005, 0.008, ...]

n_simulations = 10000
results = []

for _ in range(n_simulations):
    # Random sample with replacement
    sampled = np.random.choice(trade_returns, size=len(trade_returns), replace=True)
    # Build equity curve
    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)
    })

रन टाइम: एक साधारण लैपटॉप पर ~2 सेकंड। आपकी स्ट्रैटेजी की 10,000 वैकल्पिक हिस्ट्री।

कॉन्फिडेंस इंटरवल निकालना

Confidence intervals for PnL, MaxDD, and Sharpe Ratio with 5th, 50th, and 95th percentiles स्ट्रैटेजी की प्रमुख मेट्रिक्स के लिए कॉन्फिडेंस इंटरवल: PnL, MaxDD और Sharpe Ratio, जिसमें 5वें, 50वें और 95वें पर्सेंटाइल बैंड दिखाए गए हैं।

अब हमारे पास एक आंकड़ा नहीं, बल्कि एक डिस्ट्रीब्यूशन है। इससे उपयोगी जानकारी निकालने का तरीका यह है:

import pandas as pd

df = pd.DataFrame(results)

# 95% confidence interval for PnL
pnl_5 = np.percentile(df['final_pnl'], 5)
pnl_50 = np.percentile(df['final_pnl'], 50)
pnl_95 = np.percentile(df['final_pnl'], 95)

# 95% confidence interval for MaxDD
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}")

एक असली स्ट्रैटेजी के लिए उदाहरण आउटपुट:

Metric5th percentile (worst)Median95th percentile (best)
PnL+18.3%+41.7%+72.1%
MaxDD-23.4%-12.8%-5.1%
Sharpe1.121.762.41

अब फर्क साफ नजर आता है:

  • बैकटेस्ट ने PnL +42% दिखाया — लेकिन 5% सबसे खराब परिदृश्यों में PnL सिर्फ +18.3% है
  • बैकटेस्ट ने MaxDD -12% दिखाया — लेकिन 5% सबसे खराब परिदृश्यों में ड्रॉडाउन -23.4% है
  • Sharpe 1.8 — लेकिन निचली सीमा 1.12 है

5वां पर्सेंटाइल आपका "यथार्थवादी सबसे खराब स्थिति" है। अगर 5वें पर्सेंटाइल पर स्ट्रैटेजी लाभदायक नहीं रहती, तो इसे प्रोडक्शन में लॉन्च करना जोखिम भरा है।

विजुअलाइजेशन: फैन चार्ट

मॉन्टे कार्लो बूटस्ट्रैप को स्वाभाविक रूप से एक फैन चार्ट के रूप में विजुअलाइज़ किया जाता है — इक्विटी कर्व्स का एक पंखा:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Fan chart: all equity curves
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')

# Add percentiles
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()

# Histogram of final PnL
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()

फैन चार्ट संभावित नतीजों के फैलाव (spread) की सहज समझ देता है। संकरा पंखा यानी स्ट्रैटेजी स्थिर है। चौड़ा पंखा यानी नतीजा ट्रेडों के क्रम की "किस्मत" पर काफी हद तक निर्भर करता है।

Monte Carlo Visualization: Fan Chart and Distribution Histogram फैन चार्ट (बाएं) संभावित इक्विटी पथों के फैलाव को दिखाता है, और हिस्टोग्राम (दाएं) हाइलाइट किए गए कॉन्फिडेंस इंटरवल (5%, 50%, 95%) के साथ अंतिम रिटर्न्स की डेंसिटी डिस्ट्रीब्यूशन दिखाता है।

एडवांस्ड विश्लेषण: बर्बादी की संभावना (Probability of Ruin)

Probability of ruin analysis: equity curves either surviving or falling into ruin बर्बादी की संभावना का विजुअलाइजेशन: बचे रहने वाले इक्विटी पथ (सियान) ऊपर की ओर मुड़ते हैं जबकि बर्बाद हुए पथ (लाल) शून्य-इक्विटी की सीमा से नीचे गिर जाते हैं।

बूटस्ट्रैप एक महत्वपूर्ण सवाल का जवाब देने में मदद करता है: इस बात की क्या संभावना है कि स्ट्रैटेजी अपनी पूंजी का X% गंवा देगी?

# Probability of ruin: probability of drawdown exceeding 20%
ruin_threshold = -0.20
prob_ruin = (df['max_dd'] < ruin_threshold).mean()
print(f"P(MaxDD < -20%) = {prob_ruin:.1%}")

# Probability of negative PnL
prob_loss = (df['final_pnl'] < 0).mean()
print(f"P(PnL < 0) = {prob_loss:.1%}")

# Expected shortfall (CVaR): average loss in the 5% worst cases
worst_5pct = df['final_pnl'].quantile(0.05)
cvar = df[df['final_pnl'] <= worst_5pct]['final_pnl'].mean()
print(f"CVaR(5%) = {cvar:.1%}")

ये मेट्रिक्स एक अकेले बैकटेस्ट रन से हासिल करना असंभव है। फिर भी किसी स्ट्रैटेजी को लॉन्च करने का फैसला लेने के लिए ये अत्यंत जरूरी हैं।

गहरे ड्रॉडाउन गणितीय रूप से खतरनाक क्यों होते हैं और रिटर्न असममिति (asymmetry) कैसे काम करती है, इस बारे में अधिक जानने के लिए हमारा लेख Loss-Profit Asymmetry पढ़ें।

क्लासिकल बूटस्ट्रैप कब काम नहीं करता

इस पद्धति की कुछ सीमाएं हैं जिनके बारे में जानना जरूरी है।

रिटर्न्स का ऑटोकॉरिलेशन

क्लासिकल बूटस्ट्रैप यह मानता है कि ट्रेड स्वतंत्र (independent) हैं। असल में ऐसा अक्सर नहीं होता — किसी स्ट्रैटेजी में जीत और हार की लगातार लकीरें हो सकती हैं। अगर ऑटोकॉरिलेशन महत्वपूर्ण है, तो ब्लॉक बूटस्ट्रैप का इस्तेमाल करें:

def block_bootstrap(returns, block_size=10, n_simulations=10000):
    """Bootstrap preserving local dependency structure."""
    n = len(returns)
    results = []

    for _ in range(n_simulations):
        # Select random starting points for blocks
        starts = np.random.randint(0, n - block_size + 1, size=n // block_size + 1)
        # Assemble blocks
        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)

ब्लॉक बूटस्ट्रैप लगातार ट्रेडों के बीच स्थानीय निर्भरता (local dependency) को बनाए रखता है, जिससे MaxDD के लिए अधिक यथार्थवादी कॉन्फिडेंस इंटरवल मिलते हैं।

Block bootstrap resampling: sequential trade blocks shuffled and recombined ब्लॉक बूटस्ट्रैप ट्रेड सीक्वेंस को ब्लॉक्स में बांटकर और उन्हें रिप्लेसमेंट के साथ रीसैंपल करके ब्लॉक्स के भीतर ऑटोकॉरिलेशन बनाए रखता है।

बाजार की गैर-स्थिरता (Non-Stationarity)

बूटस्ट्रैप मूल ट्रेड डिस्ट्रीब्यूशन के साथ काम करता है। अगर बाजार संरचनात्मक रूप से बदल गया है (जैसे अस्थिरता घट गई हो या लिक्विडिटी बदल गई हो), तो ऐतिहासिक ट्रेड अप्रतिनिधिक (unrepresentative) हो सकते हैं। इसे ध्यान में रखने के लिए:

  • एक रोलिंग विंडो का इस्तेमाल करें: सिर्फ आखिरी N ट्रेडों पर बूटस्ट्रैप करें
  • हाल के ट्रेडों को ज्यादा वजन दें: वेटेड बूटस्ट्रैप
  • डेटा को मार्केट रीजीम के अनुसार बांटें और अलग-अलग बूटस्ट्रैप करें

ट्रेडों की छोटी संख्या

बूटस्ट्रैप n > 30 ट्रेडों पर भरोसेमंद होता है। अगर आपके पास सिर्फ 10 ट्रेड हैं — तो कोई भी रीसैंपलिंग मदद नहीं करेगी। 491 ट्रेड एक शानदार सैंपल है; आप नतीजों पर भरोसा कर सकते हैं।

बैकटेस्ट रोबस्टनेस आकलन के तरीकों की तुलना

MethodWhat it providesComplexityTimeWhen to use
Single backtestOne point estimateMinimalSecondsNever as a final result
Walk-forwardOut-of-sample metricsMediumMinutesTo check for overfitting
Monte Carlo bootstrapConfidence intervalsMinimal~2 secAlways before production
Monte Carlo pathNew price pathsHighMinutes-hoursFor stress testing
Cross-validationAverage metrics across foldsMediumMinutesFor parameter tuning

मॉन्टे कार्लो बूटस्ट्रैप ही इकलौता तरीका है जो न्यूनतम समय में जोखिमों की पूरी तस्वीर देता है।

चेकलिस्ट: नतीजों की व्याख्या करना

मॉन्टे कार्लो बूटस्ट्रैप के नतीजों की व्याख्या करने के लिए हमारी सिफारिश इस प्रकार है:

प्रोडक्शन में लॉन्च करें अगर:

  • 5वें पर्सेंटाइल पर PnL पॉजिटिव है
  • 5वें पर्सेंटाइल पर MaxDD आपकी जोखिम क्षमता के हिसाब से स्वीकार्य है
  • बर्बादी की संभावना < 1% है
  • 5वें पर्सेंटाइल पर Sharpe > 0.5 है

काम की जरूरत है अगर:

  • 5वें पर्सेंटाइल पर PnL शून्य के करीब है
  • 5वें पर्सेंटाइल पर MaxDD, 50वें पर्सेंटाइल की तुलना में काफी खराब है
  • फैन चार्ट का फैलाव चौड़ा है — स्ट्रैटेजी अस्थिर है

लॉन्च न करें अगर:

  • 5वें पर्सेंटाइल पर PnL नेगेटिव है
  • बर्बादी की संभावना > 5% है
  • Sharpe का कॉन्फिडेंस इंटरवल 0 को शामिल करता है

marketmaker.cc पर हमारा अनुभव

marketmaker.cc पर हम अपना खुद का बैकटेस्ट इंजन बनाते हैं, और मॉन्टे कार्लो बूटस्ट्रैप हमारी पाइपलाइन का अभिन्न हिस्सा है। हर स्ट्रैटेजी लाइव ट्रेडिंग के लिए स्वीकृत होने से पहले स्वचालित रूप से बूटस्ट्रैप से गुजरती है।

हमने बूटस्ट्रैप को सीधे बैकटेस्ट इंजन में इंटीग्रेट किया है: रन के बाद, आपको सिर्फ अंतिम PnL नहीं, बल्कि कॉन्फिडेंस इंटरवल, फैन चार्ट, बर्बादी की संभावना, और ब्लॉक बनाम स्टैंडर्ड बूटस्ट्रैप की तुलना के साथ एक पूरी रिपोर्ट मिलती है। इसमें अतिरिक्त 2-3 सेकंड लगते हैं — असली जोखिमों को समझने के लिए यह एक नगण्य कीमत है।

हमारे अनुभव से: करीब 30% स्ट्रैटेजी जो एक-अंक अनुमान के हिसाब से आकर्षक दिखती हैं, मॉन्टे कार्लो बूटस्ट्रैप के बाद फिल्टर हो जाती हैं। उनका 5वां पर्सेंटाइल PnL नेगेटिव हो जाता है या MaxDD अस्वीकार्य निकलता है। बूटस्ट्रैप के बिना, ये स्ट्रैटेजी प्रोडक्शन में चली जातीं और बहुत संभावना है कि नुकसान का कारण बनतीं।

निष्कर्ष

मॉन्टे कार्लो बूटस्ट्रैप महज ~10 लाइनों का कोड और ~2 सेकंड की गणना है। यह बैकटेस्ट के एक अकेले आंकड़े को कॉन्फिडेंस इंटरवल वाली पूरी डिस्ट्रीब्यूशन में बदल देता है। यह शायद किसी भी क्वांटिटेटिव विश्लेषण टूल का सबसे ज्यादा ROI है:

  • न्यूनतम लागत: 30 मिनट में इम्प्लीमेंटेशन
  • अधिकतम फायदा: स्ट्रैटेजी के असली जोखिमों की समझ
  • कोई डिपेंडेंसी नहीं: सिर्फ NumPy

अगर आप अभी बूटस्ट्रैप का इस्तेमाल नहीं कर रहे — तो इसे आज ही अपनी पाइपलाइन में जोड़ें। अपने बैकटेस्ट नतीजों पर कितना भरोसा किया जा सकता है, यह जानने का यही एकमात्र तरीका है।


References

  1. Efron, B. — Bootstrap Methods: Another Look at the Jackknife (1979)
  2. Davison, A.C., Hinkley, D.V. — Bootstrap Methods and their Application (Cambridge)
  3. Aronson, D.R. — Evidence-Based Technical Analysis: Monte Carlo permutation
  4. QuantStart — Monte Carlo Simulation for Backtest Analysis
  5. Marcos Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  6. Kevin Davey — Building Winning Algorithmic Trading Systems: Monte Carlo Analysis
  7. 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.}
}