← Zurück zu den Artikeln
March 6, 2026
5 min read

Monte-Carlo-Bootstrap: Wie man in 10 Codezeilen Konfidenzintervalle für einen Backtest erhält

#algotrading
#backtest
#Monte Carlo
#bootstrap
#confidence intervals
#risk management
#statistics
🎯
Part 5 of 9 · Collection
Backtesting Without Fooling Yourself

📄 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

A single point estimate versus a full probability distribution 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

Monte Carlo bootstrap resampling: thousands of alternative equity paths generated from trade data 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:

  1. Sie haben ein Array von Renditen für jeden Trade — zum Beispiel 491 Werte
  2. Sie wählen zufällig 491 Werte aus diesem Array mit Zurücklegen aus — manche Trades tauchen zweimal auf, manche gar nicht
  3. Sie bauen aus dieser neuen Stichprobe eine Equity-Kurve
  4. Sie wiederholen das 10.000 Mal
  5. 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

Confidence intervals for PnL, MaxDD, and Sharpe Ratio with 5th, 50th, and 95th percentiles 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.

Monte Carlo Visualization: Fan Chart and Distribution Histogram 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

Probability of ruin analysis: equity curves either surviving or falling into ruin 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 resampling: sequential trade blocks shuffled and recombined 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

  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.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

Dem Markt einen Schritt voraus

Abonniere unseren Newsletter für exklusive KI-Trading-Einblicke, Marktanalysen und Plattform-Updates.

Wir respektieren deine Privatsphäre. Jederzeit abbestellbar.