MC Monte Carlo Methods for Trading
Simulating thousands of possible futures to validate your system
Learning Objectives
- •Understand Monte Carlo simulation and its role in system validation
- •Learn block bootstrap and why it beats random resampling
- •Interpret Monte Carlo output for breach probability
- •Read a max drawdown distribution chart
Explain Like I'm 5
Monte Carlo simulation runs your trading strategy through thousands of alternate realities. What if trades happened in a different order? What if your luck was worse? Better? By simulating thousands of scenarios, you discover how the strategy performs across the full range of possibilities — not just the one path that actually happened.
Think of It This Way
You're playing poker and you want to know if your strategy is actually good or if you just had a hot night. Monte Carlo replays the entire session 5,000 times with different shuffles. If you profit in 97% of replays, your strategy is real. If only 55%, you might have just been lucky.
1Why One Backtest Isn't Enough
2Block Bootstrap — The Right Way to Resample
3Reading the Max Drawdown Distribution
Max Drawdown Distribution (5,000 Monte Carlo Simulations)
4How to Read Monte Carlo Output
5Random vs Block Bootstrap — The Gap
Max DD Percentiles: Random vs Block Bootstrap
Key Formulas
Block Bootstrap Equity Curve
Equity curve from bootstrap simulation. E₀ is starting capital, r_b(t) are returns drawn from random blocks. Each simulation gives a different curve.
Breach Probability
Fraction of N simulations where max drawdown exceeds limit L. Simple: count how many simulations cross the line, divide by total simulations.
Hands-On Code
Monte Carlo Block Bootstrap
import numpy as np
def monte_carlo_analysis(trade_results, n_sims=5000, block_size=30):
"""Block bootstrap Monte Carlo for system validation."""
n_trades = len(trade_results)
max_dds = []
for _ in range(n_sims):
# Build simulated equity from random blocks
sim_trades = []
while len(sim_trades) < n_trades:
start = np.random.randint(0, n_trades - block_size)
block = trade_results[start:start + block_size]
sim_trades.extend(block)
sim_trades = sim_trades[:n_trades]
# Compute max drawdown
equity = np.cumprod(1 + np.array(sim_trades))
running_max = np.maximum.accumulate(equity)
drawdowns = (running_max - equity) / running_max
max_dds.append(drawdowns.max())
max_dds = np.array(max_dds)
breach_prob = (max_dds > 0.10).mean()
print(f"Breach probability (10% DD): {breach_prob:.2%}")
print(f"95th percentile DD: {np.percentile(max_dds, 95):.2%}")
print(f"99th percentile DD: {np.percentile(max_dds, 99):.2%}")The core of any serious system validation. Breach probability from 5,000+ simulations gives you the confidence to deploy — or the data to say "not yet."
Knowledge Check
Q1.Why use block bootstrap instead of random resampling?
Assignment
Run a Monte Carlo with 1,000 simulations on your backtest results. Compare block bootstrap (block_size=30) vs random. The random version will likely show lower max drawdown because it breaks the streak structure.