Monte-Carlo-Bootstrap: Wie man in 10 Codezeilen Konfidenzintervalle für einen Backtest erhält
📄 Aus diesem Artikel ist ein Forschungspapier entstanden. Die hier beschriebenen Bootstrap-Konfidenzintervalle werden unter serieller Abhängigkeit einem kontrollierten Coverage-Test unterzogen (iid vs. Trade-Level vs. Block-Bootstrap, 6.000 Experimente mit exakt bekannter Wahrheit). Lesen Sie das Paper online (interaktive Version + PDF) unter bootstrap.marketmaker.cc, Code und Daten unter github.com/suenot/bootstrap-coverage.
Sie haben eine Strategie durch einen Backtest laufen lassen. Ergebnis: PnL +42 %, Sharpe 1,8, MaxDD -12 %. Die Ergebnisse sehen großartig aus. Sie starten den Bot live, und einen Monat später stellen Sie fest, dass der Drawdown bereits bei -28 % liegt und der PnL gegen null tendiert.
Was ist schiefgelaufen? Es ist kein Bug und auch kein "sich veränderter Markt". Das Problem ist, dass Sie eine Entscheidung auf Basis einer einzigen Zahl getroffen haben — einer Einzelpunktschätzung. Sie haben erfahren, dass die Strategie +42 % zeigte, aber nicht, wie sehr Sie dieser Zahl vertrauen können.
Das Problem mit Einzelpunktschätzungen
Ein einzelner Datenpunkt (links) vermittelt ein irreführendes Bild, während die vollständige Verteilung (rechts) die wahre Bandbreite möglicher Ergebnisse offenlegt.
Ein Backtest auf historischen Daten ist ein einziger Durchlauf durch eine bestimmte Abfolge von Marktereignissen. Das Ergebnis hängt von der Reihenfolge der Trades ab: dieselbe Strategie mit denselben Trades, aber in anderer Reihenfolge, kann einen völlig anderen maximalen Drawdown zeigen.
Stellen Sie sich 491 Trades vor. Jeder Trade ist ein Zufallsereignis mit einer bestimmten Renditeverteilung. Der historische Backtest zeigt nur eine Realisierung dieses Prozesses. Es ist, als würde man einen Würfel einmal werfen und daraus schließen, dass der Würfel immer eine Vier zeigt.
Was wir tatsächlich brauchen:
- Keine Punktschätzung, sondern ein Intervall: "mit 95 % Wahrscheinlichkeit liegt der finale PnL zwischen X und Y"
- Keinen einzelnen maximalen Drawdown, sondern eine Verteilung: "in den 5 % schlechtesten Szenarien übersteigt der Drawdown Z %"
- Nicht den Mittelwert, sondern die Ränder (Tails): was passiert, wenn das Glück nicht auf Ihrer Seite ist?
Genau dafür ist Monte-Carlo-Bootstrap da.
Was ist Monte-Carlo-Bootstrap
Bootstrap erzeugt Tausende alternativer Equity-Verläufe durch Resampling der Trades mit Zurücklegen aus dem ursprünglichen Datensatz.
Bootstrap ist eine 1979 von Bradley Efron vorgeschlagene Resampling-Methode. Die Idee ist elegant: Wenn wir eine Datenstichprobe haben, können wir Tausende "neuer" Stichproben erzeugen, indem wir zufällig Elemente aus der Originalmenge mit Zurücklegen auswählen.
Im Kontext eines Backtests funktioniert das so:
- Sie haben ein Array von Renditen für jeden Trade — zum Beispiel 491 Werte
- Sie wählen zufällig 491 Werte aus diesem Array mit Zurücklegen aus — manche Trades tauchen zweimal auf, manche gar nicht
- Sie bauen aus dieser neuen Stichprobe eine Equity-Kurve
- Sie wiederholen das 10.000 Mal
- Sie erhalten eine Verteilung der finalen Kennzahlen, keine Einzelzahl
Jede Iteration ist ein "alternatives Szenario": was hätte passieren können, wenn Reihenfolge und Zusammensetzung der Trades leicht anders gewesen wären.
Implementierung in 10 Zeilen
Hier eine vollständige, lauffähige Implementierung:
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)
})
Ausführungszeit: ~2 Sekunden auf einem gewöhnlichen Laptop. 10.000 alternative Historien Ihrer Strategie.
Konfidenzintervalle extrahieren
Konfidenzintervalle für zentrale Strategiekennzahlen: PnL, MaxDD und Sharpe Ratio, mit dem 5., 50. und 95. Perzentilband.
Jetzt haben wir nicht eine Zahl, sondern eine Verteilung. So extrahiert man daraus nützliche Informationen:
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}")
Beispielausgabe für eine reale Strategie:
| Metric | 5th percentile (worst) | Median | 95th percentile (best) |
|---|---|---|---|
| PnL | +18.3% | +41.7% | +72.1% |
| MaxDD | -23.4% | -12.8% | -5.1% |
| Sharpe | 1.12 | 1.76 | 2.41 |
Nun ist der Unterschied offensichtlich:
- Der Backtest zeigte PnL +42 % — aber in den 5 % schlechtesten Szenarien liegt der PnL nur bei +18,3 %
- Der Backtest zeigte MaxDD -12 % — aber in den 5 % schlechtesten Szenarien beträgt der Drawdown -23,4 %
- Sharpe 1,8 — aber die untere Grenze liegt bei 1,12
Das 5. Perzentil ist Ihr "realistischer Worst Case". Wenn die Strategie am 5. Perzentil nicht mehr profitabel ist, ist der Livegang riskant.
Visualisierung: Fan Chart
Monte-Carlo-Bootstrap lässt sich natürlich als Fan Chart visualisieren — ein Fächer aus Equity-Kurven:
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()
Ein Fan Chart vermittelt ein intuitives Verständnis der Streuung möglicher Ergebnisse. Ein schmaler Fächer bedeutet, dass die Strategie stabil ist. Ein breiter Fächer bedeutet, dass das Ergebnis stark vom "Glück" mit der Trade-Reihenfolge abhängt.
Das Fan Chart (links) zeigt die Streuung möglicher Equity-Verläufe, und das Histogramm (rechts) zeigt die Dichteverteilung der finalen Renditen mit hervorgehobenen Konfidenzintervallen (5 %, 50 %, 95 %).
Erweiterte Analyse: Ruinwahrscheinlichkeit
Visualisierung der Ruinwahrscheinlichkeit: überlebende Equity-Pfade (Cyan) verlaufen aufwärts, während ruinierte Pfade (Rot) unter die Null-Equity-Kante fallen.
Bootstrap erlaubt es, eine kritische Frage zu beantworten: Wie hoch ist die Wahrscheinlichkeit, dass die Strategie X % des Kapitals verliert?
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%}")
Diese Kennzahlen sind aus einem einzelnen Backtest-Lauf unmöglich zu gewinnen. Dennoch sind sie entscheidend für die Entscheidung, eine Strategie live zu schalten.
Mehr dazu, warum tiefe Drawdowns mathematisch gefährlich sind und wie die Renditeasymmetrie funktioniert, lesen Sie in unserem Artikel Loss-Profit Asymmetry.
Wann klassischer Bootstrap nicht funktioniert
Die Methode hat Grenzen, die wichtig zu kennen sind.
Autokorrelation der Renditen
Klassischer Bootstrap setzt voraus, dass Trades unabhängig sind. In der Realität ist das oft nicht der Fall — eine Strategie kann Gewinn- und Verlustserien haben. Ist die Autokorrelation signifikant, verwenden Sie Block-Bootstrap:
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)
Block-Bootstrap bewahrt lokale Abhängigkeiten zwischen aufeinanderfolgenden Trades und liefert so realistischere Konfidenzintervalle für den MaxDD.
Block-Bootstrap bewahrt die Autokorrelation innerhalb von Blöcken, indem die Trade-Sequenz in Blöcke unterteilt und diese mit Zurücklegen resampelt werden.
Nicht-Stationarität des Marktes
Bootstrap arbeitet mit der ursprünglichen Trade-Verteilung. Hat sich der Markt strukturell verändert (z. B. Volatilität gesunken oder Liquidität verändert), können historische Trades unrepräsentativ sein. Um dem Rechnung zu tragen:
- Verwenden Sie ein Rolling Window: Bootstrap nur auf den letzten N Trades
- Gewichten Sie jüngere Trades stärker: Weighted Bootstrap
- Teilen Sie die Daten nach Marktregimes auf und bootstrappen Sie separat
Geringe Anzahl an Trades
Bootstrap ist zuverlässig bei n > 30 Trades. Bei 10 Trades hilft kein noch so umfangreiches Resampling. 491 Trades sind eine ausgezeichnete Stichprobe; den Ergebnissen kann man vertrauen.
Vergleich der Ansätze zur Bewertung der Backtest-Robustheit
| Method | What it provides | Complexity | Time | When to use |
|---|---|---|---|---|
| Single backtest | One point estimate | Minimal | Seconds | Never as a final result |
| Walk-forward | Out-of-sample metrics | Medium | Minutes | To check for overfitting |
| Monte Carlo bootstrap | Confidence intervals | Minimal | ~2 sec | Always before production |
| Monte Carlo path | New price paths | High | Minutes-hours | For stress testing |
| Cross-validation | Average metrics across folds | Medium | Minutes | For parameter tuning |
Monte-Carlo-Bootstrap ist die einzige Methode, die in minimaler Zeit ein vollständiges Bild der Risiken liefert.
Checkliste: Ergebnisse interpretieren
So empfehlen wir, die Ergebnisse des Monte-Carlo-Bootstrap zu interpretieren:
Live schalten, wenn:
- der PnL am 5. Perzentil positiv ist
- der MaxDD am 5. Perzentil für Ihre Risikobereitschaft akzeptabel ist
- die Ruinwahrscheinlichkeit < 1 % ist
- der Sharpe am 5. Perzentil > 0,5 ist
Nachbesserung nötig, wenn:
- der PnL am 5. Perzentil nahe null liegt
- der MaxDD am 5. Perzentil deutlich schlechter ist als am 50.
- der Fan-Chart-Fächer breit gestreut ist — die Strategie ist instabil
Nicht live schalten, wenn:
- der PnL am 5. Perzentil negativ ist
- die Ruinwahrscheinlichkeit > 5 % ist
- das Konfidenzintervall für Sharpe die 0 einschließt
Unsere Erfahrung bei marketmaker.cc
Bei marketmaker.cc entwickeln wir unsere eigene Backtest-Engine, und Monte-Carlo-Bootstrap ist fester Bestandteil unserer Pipeline. Jede Strategie durchläuft automatisch den Bootstrap, bevor sie für den Live-Handel freigegeben wird.
Wir haben Bootstrap direkt in die Backtest-Engine integriert: Nach einem Lauf erhalten Sie nicht nur den finalen PnL, sondern einen vollständigen Report mit Konfidenzintervallen, Fan Chart, Ruinwahrscheinlichkeit und einem Vergleich von Block- vs. Standard-Bootstrap. Das kostet zusätzlich 2-3 Sekunden — ein vernachlässigbarer Preis für das Verständnis der realen Risiken.
Aus unserer Erfahrung: rund 30 % der Strategien, die nach der Einzelpunktschätzung attraktiv aussehen, werden nach dem Monte-Carlo-Bootstrap aussortiert. Ihr PnL am 5. Perzentil wird negativ, oder der MaxDD erweist sich als inakzeptabel. Ohne Bootstrap wären diese Strategien live gegangen und hätten sehr wahrscheinlich zu Verlusten geführt.
Fazit
Monte-Carlo-Bootstrap sind ~10 Zeilen Code und ~2 Sekunden Rechenzeit. Er verwandelt eine einzelne Zahl aus einem Backtest in eine vollständige Verteilung mit Konfidenzintervallen. Das ist wohl der ROI mit dem höchsten Ertrag unter allen quantitativen Analysetools:
- Minimale Kosten: Implementierung in 30 Minuten
- Maximaler Nutzen: Verständnis der realen Strategierisiken
- Keine Abhängigkeiten: nur NumPy
Wenn Sie noch keinen Bootstrap einsetzen — fügen Sie ihn noch heute Ihrer Pipeline hinzu. Es ist der einzige Weg, zu wissen, wie sehr Sie den Ergebnissen Ihres Backtests vertrauen können.
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.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.