18 — Backtest & Validate

Maximum Drawdown & Recovery#

RiskStatistical
◆ The PatternMeasuring worst-case loss from peak — depth, duration, and recovery time

Maximum drawdown is the largest peak-to-trough decline in portfolio value. It answers the question every investor asks: "How bad can it get?" Recovery time — how long to reach a new high — is equally important.

DD(t) = (Peak(t) − Value(t)) / Peak(t)
Drawdown at time t. Max Drawdown = max over all t. Expressed as percentage.
// Interactive equity curve with drawdown shading
Volatility20%
Max DD—
Psychology: A 50% drawdown requires a 100% gain to recover. A 33% drawdown needs 50%. The math is against you — managing drawdown is as important as maximising return.
Pattern bridge: Drawdown measures the path, not just the endpoint, and recovery is harder than the fall: a 50% loss needs a 100% gain. That arithmetic drives drawdown analysis and stop-loss rules.
Worked example

Simulated stock-like returns — 7% a year on average with 16% volatility. The median maximum drawdown is 13.7% over one year, 25.7% over five and 38.0% over twenty. The same strategy has a deeper worst drawdown the longer you watch it, so drawdowns from histories of different lengths cannot be compared directly.

Where it misleads
  • A backtest’s maximum drawdown is one draw from a distribution; expect worse live.
  • Limits set from a short history will be breached by a perfectly normal strategy.
  • Drawdown depends on the order of returns (see Monte Carlo); the same trades can produce very different worst cases.
In code
rng = np.random.default_rng(10)
def median_mdd(years, paths=2_000, mu=0.07, sigma=0.16):
    r = rng.normal(mu / 252, sigma / np.sqrt(252), (paths, 252 * years))
    wealth = np.exp(np.cumsum(r, axis=1))
    return np.median((1 - wealth / np.maximum.accumulate(wealth, axis=1)).max(axis=1))
table = {y: round(median_mdd(y), 3) for y in (1, 5, 20)}
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 — drawdown analysis
import numpy as np

cum_returns = (1 + returns).cumprod()
running_max = cum_returns.cummax()
drawdown = (cum_returns - running_max) / running_max
max_dd = drawdown.min()
print(f"Max Drawdown: {max_dd:.1%}")

# Recovery time
underwater = drawdown < 0
recovery_periods = underwater.astype(int).groupby(
    (~underwater).cumsum()
).sum()
Sources
  • M. Magdon-Ismail, A. F. Atiya, A. Pratap & Y. S. Abu-Mostafa, “On the Maximum Drawdown of a Brownian Motion”, Journal of Applied Probability 41(1), 2004
  • M. Magdon-Ismail & A. F. Atiya, “Maximum Drawdown”, Risk 17(10), 2004

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

Linked from 2
← Previous
Sharpe Ratio & Risk-Adjusted Returns
Open in the full reader, with the topic sidebar