Monte Carlo trade-sequence stress test
A backtest equity curve is one ordering of your closed trades. Bootstrap Monte Carlo asks what happens if the same P&L list arrives in a different order — thousands of times — and turns the answer into a percentile fan chart plus drawdown and ruin metrics you can size against.
The path that lied
Net profit and profit factor summarize the realized sequence. They do not tell you how often a different shuffle of the same trades would have hit a margin call, tripped a prop-firm daily-loss rule, or forced you to cut size mid-sample. Path dependency is not a curiosity — it is the difference between “the edge worked” and “the edge worked and the order was kind.”
Quantitative risk managers have answered this for decades with Monte Carlo: generate many what-if paths from the same data and report a distribution, not a single curve. For closed trades, the lightest assumption-free version is bootstrap resampling of the trade P&L list.
This guide is pedagogy for that loop. For parameter honesty across time, pair it with walk-forward in pure Python. For the broader validation stack, see the validation gauntlet.
Bootstrap in plain English
Start with N closed-trade P&L values (wins positive, losses negative). Treat each as an observation drawn from an unknown distribution. For each simulation:
- Draw
Ntrades at random with replacement from that pool. - Accumulate them into a synthetic equity curve from a fixed initial balance.
- Record final equity and peak-to-trough max drawdown for that path.
Repeat (typically on the order of 1,000 runs in teaching setups). You now have a cloud of plausible equity paths — not a forecast of tomorrow’s trade, but a map of how volatile the journey could have been given the same empirical edge.
historical P&L pool → resample with replacement × M sims
→ M equity curves
→ percentiles at each trade step (fan)
→ DD / VaR / ruin summaries
Why not fit a normal and sample from it?
Parametric Monte Carlo (fit μ, σ; draw Gaussian noise) is tempting and usually wrong for trade lists. Real P&L is fat-tailed, often skewed, sometimes multimodal — a mean-reversion book can look like a pile of small wins, a few large stops, and a fat right tail when a trend day finally pays. A normal fit smooths out the outliers that dominate drawdown. Bootstrap does not assume a shape; it samples from what actually happened.
Reading the fan chart
At every trade step, take the cross-section of all simulated equities and plot a few percentiles. A common teaching fan uses the 5th, 25th, 50th, 75th, and 95th:
- 50th (median) — typical path drift under the bootstrap.
- 5th — rough-run envelope: roughly 95% of sims finished above this line at that step.
- Fan width — visual measure of sequence uncertainty. A fan that opens fast in the first few dozen trades means early loss clusters are dangerous even when expectancy is positive.
Narrow fan + rising median: outcomes converge; order luck matters less. Wide early fan: same edge, fragile path — size and stop rules matter more than the headline profit factor.
Four risk numbers worth caching
Collapse the simulation cloud into a short dashboard (definitions match common risk practice; exact thresholds are yours to lock a priori):
- Median max drawdown — typical worst peak-to-trough (percent) across sims.
- Stress drawdown (95th percentile of max DD) — the drawdown exceeded in only about 5% of runs. Plan capacity against this, not against the single historical curve.
- Value at Risk (5%) — dollar gap from initial balance to the 5th-percentile final equity:
VaR ≈ InitialBalance − P5_FinalEquity(positive when the lower tail ends below start). - Probability of ruin — fraction of sims whose max drawdown breaches a pre-committed threshold (e.g. 20%). A signal about sequence sensitivity, not a court verdict.
If stress drawdown or VaR feels uncomfortable relative to account size, the first lever is usually lot size, not strategy replacement — see the position-size tool.
A pure-Python sketch
You do not need MetaTrader for the idea. Given a 1-D array of trade P&Ls, the engine is a few dozen lines of numpy. Pedagogy rewritten here for Quant for Free — not a paste of vendor source.
import numpy as np
def max_drawdown_pct(equity):
peak = np.maximum.accumulate(equity)
dd = (peak - equity) / np.maximum(peak, 1e-12)
return 100.0 * dd.max()
def bootstrap_paths(pnls, n_sims=1000, balance0=10_000.0, seed=42):
rng = np.random.default_rng(seed)
n = len(pnls)
curves = np.empty((n_sims, n + 1))
finals = np.empty(n_sims)
max_dds = np.empty(n_sims)
for i in range(n_sims):
sample = rng.choice(pnls, size=n, replace=True)
eq = np.empty(n + 1)
eq[0] = balance0
eq[1:] = balance0 + np.cumsum(sample)
curves[i] = eq
finals[i] = eq[-1]
max_dds[i] = max_drawdown_pct(eq)
return curves, finals, max_dds
def fan_percentiles(curves, q=(5, 25, 50, 75, 95)):
return {p: np.percentile(curves, p, axis=0) for p in q}
def summarize(finals, max_dds, balance0=10_000.0, ruin_pct=20.0):
p5 = np.percentile(finals, 5)
return {
"p50_final": float(np.percentile(finals, 50)),
"p5_final": float(p5),
"var_5pct": float(balance0 - p5),
"median_max_dd": float(np.percentile(max_dds, 50)),
"stress_dd_p95": float(np.percentile(max_dds, 95)),
"prob_ruin_pct": float(100.0 * np.mean(max_dds >= ruin_pct)),
}
Export the fan columns (step, P5, P25, P50, P75, P95) to CSV and plot in matplotlib or a notebook. Percentiles with linear interpolation on a sorted copy match common spreadsheet / NumPy defaults — keep the method identical if you compare tools.
Commission and slippage stress
Optional second pass: subtract a fixed commission plus a random slippage draw from [0, slip_max] on every resampled trade (conservative worst-side framing). Re-run the fan and metrics. Strategies that look fine on raw P&L often thin out once friction is applied uniformly — which is the point of the stress, not a claim about your broker’s exact fills.
Attributed example (source article)
GOLD H1 trend-follower trade list
The article reports a Strategy Tester export of a trend-following EA on GOLD H1: 182 closed trades (January–April 2026 in their write-up), initial balance $10,000, 1,000 bootstrap sims, ruin threshold 20% drawdown, slippage layer off for the headline run. Their statistics panel / narrative includes approximately:
- Median path ending near $13,654 (~36.5% gain on the median curve).
- Probability of hitting the 20% ruin threshold in about 25.4% of sims.
- 5% VaR on the order of $648.65 from the $10,000 start.
- Median max drawdown about 15.1%; 95th-percentile (“stress”) max drawdown about 30.6%.
Earlier in the same article, a separate EURUSD M30 anecdote claims roughly 12% of alternative orderings of a ~200-trade history would have triggered a margin call before trade 80 — same trades, different luck. Those figures are the author’s illustrations, not a Quant for Free live or published backtest.
LessonA single historical equity curve can hide a fat left tail of sequences. If you cannot absorb the stress drawdown at your intended size, cut risk before you debate expectancy.
The same article notes a practical rule of thumb from their testing across several trend systems: the 95th-percentile Monte Carlo drawdown often lands around 1.5×–2.5× the drawdown of the original backtest sequence. Treat that as their reported experience, not a universal constant — and never substitute it for running the bootstrap on your trade list.
Limitations (do not paper over these)
- Independence. Classic bootstrap treats trades as i.i.d. Real strategies cluster wins and losses with regimes. Spreading losses randomly can understate short-run drawdown in adverse regimes. Block bootstrap (resample contiguous chunks) is the usual first upgrade.
- No regime model. Pooling a trending year with a choppy year and resampling freely treats every mix as equally plausible. Markets do not.
- Lot-size stationarity. If early trades were small and later trades compounded, mixing them as equal P&L atoms misstates path risk. Prefer returns or fixed-fraction P&L, or segment by size policy.
- Thin samples. On the order of ~50 trades, percentile bands jitter run-to-run. The source article calls 200+ closed trades a practical minimum for decision-grade bands — again, their guidance, not a Q4F claim about your book.
- Not a substitute for OOS design. Sequence MC does not fix parameter overfit, strategy shopping, or look-ahead. Keep walk-forward and the helpers in oos-lab in the loop.
A pre-trust checklist
Before believing a Monte Carlo fan — mine, yours, or a vendor’s — I want all of these to be true:
- Inputs are closed-trade P&Ls (or fixed-fraction returns), not open equity ticks with look-ahead.
- Ruin threshold, sim count, and cost model were locked before reading the fan.
- Stress drawdown and VaR are sized against the live account, not against hope.
- Independence / regime / lot-size limits are acknowledged; block bootstrap considered if streaks matter.
- Sample length is thick enough that re-seeding does not reshuffle the story.
- Walk-forward / trial logging / DSR still apply — sequence MC is not a free pass on overfitting.