20 — Backtest & Validate

Monte Carlo Simulation#

SimulationStatistical
◆ The PatternSimulating thousands of paths — confidence bands on strategy performance

Monte Carlo generates thousands of possible equity paths by resampling or simulating returns. Instead of one backtest (which is one path through history), you get a distribution of outcomes. The 5th percentile shows your realistic worst case.

Pathᵢ(t) = Πd=1t (1 + rd(i))
Each path is a product of randomly sampled daily returns. N paths give a fan of possible outcomes.
// Interactive — simulated equity paths with confidence bands
Paths100
5th %ile—
Limitation: Monte Carlo assumes returns are i.i.d. Real markets have autocorrelation, volatility clustering, and regime changes. Use block bootstrap to preserve some time structure.
Pattern bridge: Each simulated path is a random walk; thousands of them turn one backtest into a distribution of outcomes. The same simulation gives value at risk its Monte Carlo variant.
Worked example

One year of 250 trade returns has a maximum drawdown of 12.5%. Shuffle the same trades into 5,000 other orders: the final result is identical (True), but the maximum drawdown ranges from 12.6% to 26.8% (5th to 95th percentile). The order you happened to get was among the luckiest; plan for the distribution, not the history.

Where it misleads
  • Shuffling assumes trades are independent; strategies with streaks or regimes need block resampling.
  • Simulated paths can only recombine what happened; they cannot produce a crash larger than any in the data.
  • A thousand simulated paths from a wrong model are a thousand wrong answers.
In code
rng = np.random.default_rng(12)
trades = rng.normal(0.002, 0.02, 250)                 # one year of trade returns
def mdd(rets):
    w = np.cumprod(1 + rets); return (1 - w / np.maximum.accumulate(w)).max()
actual = mdd(trades)
shuffled = [mdd(rng.permutation(trades)) for _ in range(5_000)]   # same trades, other orders
lo, hi = np.percentile(shuffled, [5, 95])
final_same = np.isclose(np.prod(1 + trades), np.prod(1 + rng.permutation(trades)))
Python, in your browser — edit the code and run it again

Assumes import numpy as np and import pandas as pd (and SciPy where imported). Each snippet simulates or makes up its own data, as the comments say.

The same idea with a library
# Python — Monte Carlo equity simulation
import numpy as np

daily_returns = df['returns'].values
n_sims, n_days = 1000, 252

# Resample with replacement
paths = np.zeros((n_sims, n_days))
for i in range(n_sims):
    sampled = np.random.choice(daily_returns, size=n_days)
    paths[i] = np.cumprod(1 + sampled)

# Confidence bands
p5  = np.percentile(paths, 5, axis=0)
p95 = np.percentile(paths, 95, axis=0)
Sources
  • An Introduction to the Bootstrap, B. Efron & R. J. Tibshirani, Chapman & Hall, 1993
  • Monte Carlo Methods in Financial Engineering, P. Glasserman, Springer, 2003

Last reviewed October 2026 · How this site checks its content · Report a mistake

Linked from 5
← Previous
Walk-Forward Validation
Open in the full reader, with the topic sidebar