A Monte Carlo simulation takes the trade results from your backtest and generates thousands of alternative sequences — by reshuffling trade order or resampling with replacement — so you see a distribution of drawdowns and outcomes instead of the single path history happened to produce. This guide explains both methods from first principles, shows a minimal implementation, and covers how to read drawdown percentiles and risk of ruin. Educational material only, not financial advice: Monte Carlo stress-tests the path of a backtest, it cannot validate the edge behind it, and trading carries a real risk of loss.
You take the list of individual trade results from your backtest, generate thousands of alternative orderings or samples of those trades, rebuild the equity curve for each one, and read off the distribution of outcomes — especially maximum drawdown — rather than the single number your one historical backtest produced. In practice that means two standard techniques: reshuffling (randomly permuting the order of the same trades) and resampling (drawing trades at random with replacement to build new samples). Both can be done in a few lines of code or in most strategy-testing software.
The reason this matters is that a backtest hands you exactly one path. Your strategy took, say, 400 trades over five years, and those trades arrived in one particular order — an order that is substantially luck. Path-dependent statistics such as maximum drawdown, longest losing streak, and time-to-recovery depend heavily on that ordering. If a cluster of losers had happened to land back-to-back near the start, the same trade population would have produced a much scarier drawdown, and possibly ruin, with nothing about the strategy's logic being different.
Monte Carlo methods make that hidden sensitivity visible. Instead of "my max drawdown was 14%", you get "across 10,000 reshuffled paths, the median max drawdown was 16% and the 95th percentile was 27%" — a statement about the range of experiences the same trade quality could plausibly produce. That range, not the single observed path, is the honest input to position-sizing and capitalization decisions. The rest of this article builds both techniques from first principles and shows what they can and cannot tell you.
The simplest Monte Carlo variant keeps your exact trade population and only randomizes the order. Take the sequence of per-trade returns, shuffle it into a random permutation, rebuild the equity curve, record the maximum drawdown (and any other path statistics you care about), and repeat thousands of times.
One property makes permutation easy to reason about: because every reshuffled path contains exactly the same trades, the final compounded return is identical on every path — multiplication is commutative, so the product of (1 + r) factors does not care about order (the same holds for the sum if you work in additive terms). Permutation therefore isolates one question cleanly: holding trade quality fixed, how much of my drawdown experience was sequencing luck?
The answer is usually "a lot". The observed historical ordering is a single draw from the space of orderings, and there is nothing privileged about it. If your backtest showed a 14% max drawdown but the permutation distribution puts the 95th percentile at 27%, then planning your risk around 14% means planning around a lucky draw. A common working rule is to size positions and set capital so that you could survive a drawdown in the tail of the reshuffled distribution — the 95th or 99th percentile — rather than the one history happened to serve.
One honest caveat belongs here rather than later: pure permutation assumes trade results are exchangeable — that any ordering was equally likely. Real trade sequences often are not: losses cluster in regimes, volatility bunches, and a trend-following system's winners arrive in streaks. Permutation deliberately destroys that structure, which can make it optimistic about clustering-driven drawdowns. The block-based variants discussed in the final section address this.
The second standard technique is the bootstrap: instead of reordering the same trades, you draw N trades at random with replacement from your trade list and build an equity curve from the draw. Some trades appear twice or three times, others not at all. Repeat thousands of times and you get a distribution not just of paths but of trade compositions — each simulated history is a plausible alternative sample from the same underlying population, in the spirit of Bradley Efron's original bootstrap.
This changes what varies. Under permutation, final return was fixed and only the path moved. Under resampling, the final return varies too, because a resample that happens to omit your three biggest winners tells you what the strategy looks like without them. That is a genuinely useful stress: if the profitability of a 400-trade backtest evaporates in the sizeable fraction of resamples that miss a handful of outlier wins, the backtest's headline number is being carried by a few lucky trades rather than a repeatable process. This connects directly to sample-size reasoning — small trade counts produce wide, unstable bootstrap distributions, which is the statistics telling you that you do not yet have enough evidence.
Resampling also lets you ask forward-shaped questions the raw backtest cannot: draw only 80 trades per path to simulate "a year like these" and examine the distribution of one-year outcomes, or vary N to see how outcome dispersion shrinks with more trades.
The caveats mirror permutation's: sampling with replacement assumes trades are independent draws from a stable population — no regime dependence, no serial correlation, and a future that resembles the past. Every bootstrap conclusion is conditional on those assumptions, so treat the output as a sensitivity analysis, not a probability forecast.
Both methods fit in a short script. Export your per-trade percentage returns (TradingView's Strategy Tester exports trade lists to CSV; MetaTrader's report can be parsed the same way) and run something like this:
import numpy as np
# Per-trade fractional returns from your backtest, in historical order.
trades = np.array([...]) # e.g. [0.012, -0.008, 0.021, ...]
rng = np.random.default_rng(42)
N_SIMS = 10_000
def max_drawdown(returns):
equity = np.cumprod(1.0 + returns)
peaks = np.maximum.accumulate(equity)
return np.max(1.0 - equity / peaks)
perm_dd, boot_dd, boot_ret = [], [], []
for _ in range(N_SIMS):
shuffled = rng.permutation(trades) # Method 1: reshuffle order
perm_dd.append(max_drawdown(shuffled))
sample = rng.choice(trades, size=trades.size, replace=True) # Method 2
boot_dd.append(max_drawdown(sample))
boot_ret.append(np.prod(1.0 + sample) - 1.0)
for name, dist in [("perm maxDD", perm_dd), ("boot maxDD", boot_dd)]:
p50, p95, p99 = np.percentile(dist, [50, 95, 99])
print(f"{name}: median {p50:.1%}, 95th {p95:.1%}, 99th {p99:.1%}")
print(f"boot final return 5th pct: {np.percentile(boot_ret, 5):.1%}")A few implementation notes. Work with fractional returns and compounding (cumprod), not raw currency profits, if your strategy sizes positions as a percentage of equity — otherwise the drawdown arithmetic silently assumes fixed sizing. Fix the random seed so results are reproducible. And 10,000 simulations is cheap; tail percentiles stabilize with more paths, and the 99th percentile of a 1,000-path run is still noisy.
If per-trade sizing varied in your backtest, resample the (return, size) pairs together rather than returns alone, or the simulation quietly rewrites your risk management.
The output of either method is a distribution, and the discipline is deciding in advance which percentiles you will plan around.
Drawdown percentiles are the primary product. The median reshuffled max drawdown tells you the typical sequencing experience; the 95th and 99th percentiles tell you what unlucky-but-plausible looks like with the same trade quality. The practical use is capitalization and sizing: if you would abandon the strategy (or breach a broker's margin, or a prop firm's drawdown rule) at a 20% drawdown, and the 95th percentile of simulated drawdowns is 27%, then you are — with uncomfortable probability — going to quit this strategy at its low even if its trade population stays exactly as good as the backtest. That is a design flaw you can fix now, by sizing down, rather than discover live.
Risk of ruin is the same idea pushed to a threshold: define ruin as equity breaching some level (down 30%, down 50%, a funding rule — your choice), and report the fraction of simulated paths that touch it. Because position sizing scales every trade's impact on equity, ruin probability is acutely sensitive to sizing — the same trade list can produce a negligible ruin fraction at 0.5% risk per trade and an alarming one at 3%. This is exactly the exploration the interactive risk-of-ruin calculator on ForexCodes is built for: vary risk per trade and the ruin threshold against a trade distribution and watch the tail respond.
What you should not do is read the profit percentiles as a forecast. The distribution of simulated final returns describes resampling variability around your backtest — it inherits every flaw the backtest has, and it says nothing about whether the edge persists tomorrow.
Monte Carlo output looks authoritative — thousands of paths, clean percentiles — so it is worth being precise about what the machinery assumes and what it can never do.
The central assumption in both methods is independence and exchangeability: that your trades are draws from a stable distribution with no memory. Real trade sequences violate this in known ways — volatility clusters, losses arrive in regime-driven streaks, and consecutive trades taken in the same market conditions are correlated. Naive shuffling destroys that structure and can therefore understate clustered-drawdown risk. The standard mitigation is the block bootstrap family (Künsch, 1989; Politis and Romano's stationary bootstrap, 1994): resample contiguous blocks of trades rather than individual trades, preserving short-range dependence inside each block. If your permutation percentiles look meaningfully better than your block-resampled percentiles, the difference is the clustering the naive method was hiding.
The deeper limitation is garbage in, garbage out. Monte Carlo reshuffles the trades you feed it; it has no opinion about whether those trades are trustworthy. If the backtest is curve-fit, repaints, ignores realistic spread and slippage, or was the best of a hundred variants you tried, every simulated path inherits those flaws — you get a beautifully rendered distribution around a mirage. Sequencing risk is one failure mode of backtests; selection bias and lookahead are others, and they need their own tools (walk-forward analysis, the deflated Sharpe ratio, and code-level audits of the kind ForexCodes performs).
So the honest framing: Monte Carlo stress-tests the path, not the edge. Use it to size positions so you survive plausible bad luck. Everything here is educational only and not financial advice — no simulation makes a strategy profitable, and trading carries a real risk of loss.
Educational only — not financial advice. Trading involves substantial risk of loss.