What Is Maximum Drawdown? Depth vs Recovery
Maximum drawdown is the largest peak to trough fall in a value series. See what counts as a deep one, and why the climb back to a new high takes longer.
Maximum drawdown is the largest peak to trough fall in a value series, measured as a percentage of the running peak. An account that climbs to $120,000, sinks to $84,000, then recovers has a maximum drawdown of 30%, and it keeps that number forever, since the statistic records the worst stretch in the history rather than the state today. Most people quote the depth and stop there. Two more numbers sit inside the same statistic: the time from the peak down to the low, and the far longer time from that low back to a new high.
How is maximum drawdown calculated?
The whole calculation is one pass over the series. At each observation you update the running maximum, measure the current value against it, and keep the worst reading seen so far.
- Set the running peak to the first value.
- At each new value, raise the peak if the value is higher.
- Compute the drawdown at that point: value divided by peak, minus one.
- Keep the most negative reading. That is the maximum drawdown.
No returns series, no volatility estimate, and no distribution assumption enter into it. The loop runs on a plain list of numbers with the Python standard library. On a fresh Ubuntu box, install the interpreter first:
apt-get update && apt-get install -y python3
Save the loop as maxdd.py. The prices below are a made up ten point series, kept small enough to check by hand:
prices = [100.0, 104.0, 98.0, 92.0, 95.0, 88.0, 90.0, 101.0, 99.0, 106.0]
peak = prices[0]
max_dd = 0.0
for price in prices:
if price > peak:
peak = price
drawdown = price / peak - 1.0
if drawdown < max_dd:
max_dd = drawdown
print(f"maximum drawdown: {max_dd * 100:.1f}%")
Run python3 maxdd.py and it prints maximum drawdown: -15.4%. Check it by hand: the peak before the low is 104, the low is 88, and 88 divided by 104 is 0.846. The series then ends at 106, an all time high, and the 15.4% still stands. Maximum drawdown is a permanent record of the worst moment in a sample, and it says nothing about where the series sits today. Two more variables in the same loop capture the timing: the index where the peak was set, and the index where the worst reading landed.
What a drawdown curve looks like
A drawdown curve keeps step 3 at every point instead of reducing it to one worst value. The line sits at zero whenever the series prints a new high and hangs below zero in between, which is why the gap is called "underwater". Here is that curve for SPY, an exchange traded fund tracking the S&P 500, on month end closes since January 2016.
The exact SQL behind every number
WITH monthly AS (
SELECT toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York'))) AS month_start,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY month_start
),
runs AS (
SELECT month_start,
close_px,
max(close_px) OVER (ORDER BY month_start
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM monthly
)
SELECT formatDateTime(month_start, '%Y-%m') AS month,
round(100 * (close_px / running_peak - 1), 2) AS drawdown_pct
FROM runs
ORDER BY month_startAcross 127 month end readings the line spends long stretches pinned near zero and drops into a handful of deep notches. The final month on file reads -1.27% against the running peak. Two features of the shape do the teaching. Recoveries are slow climbs rather than sharp bounces, and every new peak resets the curve to zero, erasing the visual memory of what came before.
How long does it take to recover from a drawdown?
Three separate numbers live inside one drawdown, and treating them as one is the most common misreading of a performance table:
- Depth: how far the value fell below its peak, in percent.
- Time to the low: calendar days from the peak to the bottom.
- Recovery time: calendar days from that bottom back to a new high.
The third is usually the longest, and it is the one a headline number never shows. The panel below rebuilds every completed underwater stretch for SPY since 2016 from daily closes. A stretch starts on the day a peak is set and ends on the day the fund first closes above it.
The exact SQL behind every number
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-01-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY session_date
),
runs AS (
SELECT session_date,
close_px,
max(close_px) OVER (ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM daily
),
episodes AS (
SELECT running_peak AS peak_px,
min(session_date) AS peak_date,
argMin(session_date, close_px) AS trough_date,
min(close_px) AS trough_px
FROM runs
GROUP BY running_peak
),
sequenced AS (
SELECT peak_px,
peak_date,
trough_date,
trough_px,
leadInFrame(peak_date, 1) OVER (ORDER BY peak_date
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS recovery_date
FROM episodes
)
SELECT formatDateTime(peak_date, '%b %Y') AS episode_label,
round(100 * (1 - trough_px / peak_px), 1) AS fall_from_peak_pct,
dateDiff('day', peak_date, trough_date) AS days_to_trough,
dateDiff('day', trough_date, recovery_date) AS days_to_new_high,
dateDiff('day', peak_date, recovery_date) AS days_underwater
FROM sequenced
WHERE recovery_date > trough_date
AND round(100 * (1 - trough_px / peak_px), 1) >= 5
ORDER BY fall_from_peak_pct DESC
LIMIT 8The deepest completed stretch on the list begins in Feb 2020 and reaches 34.2% below the prior peak. It spent 33 calendar days falling and 148 climbing back, 181 days underwater in total. The next entry, from Jan 2022, is shallower at 25.4%, and it kept the fund below its old high for 746 days.
Depth and duration come apart completely. A violent fall with a quick round trip and a shallow grind lasting years both compress into one percentage. The March 2020 crash is the textbook case of the first shape, and how markets recover from crashes follows the second leg of the trip.
Why maximum drawdown gets worse the longer you measure
Maximum drawdown is a maximum, so adding history can only leave it flat or push it deeper. The running peak in a longer window is at least as high at every point as the peak in a shorter one, which pins the arithmetic. Measuring the same fund over five windows that all end on July 31, 2026 shows the effect.
The exact SQL behind every number
WITH daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2016-07-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY session_date
),
scoped AS (
SELECT arrayJoin([1, 2, 3, 5, 10]) AS lookback_years,
session_date,
close_px
FROM daily
),
windowed AS (
SELECT lookback_years, session_date, close_px
FROM scoped
WHERE session_date >= subtractYears(toDate('2026-07-31'), lookback_years)
),
runs AS (
SELECT lookback_years,
session_date,
close_px,
max(close_px) OVER (PARTITION BY lookback_years ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak
FROM windowed
)
SELECT concat(toString(lookback_years), if(lookback_years = 1, ' year', ' years')) AS lookback,
round(100 * max(1 - close_px / running_peak), 1) AS max_drawdown_pct,
formatDateTime(argMax(session_date, 1 - close_px / running_peak), '%b %Y') AS worst_point
FROM runs
GROUP BY lookback_years
ORDER BY lookback_yearsOver the trailing 1 year window the worst fall measured 9.1%. Stretch the identical series to 10 years and the reading is 34.2%, with the worst point landing in Mar 2020. The fund did not become riskier. The sample got longer.
The same arithmetic distorts strategy comparisons. A three year backtest and a twenty year backtest can describe identical behaviour and still report very different drawdowns, so the figure means little without the window attached to it. Look ahead bias in backtesting covers another way a clean looking backtest flatters itself.
Maximum drawdown vs volatility
Volatility measures the typical size of a daily move in either direction. Maximum drawdown measures one specific downward path. Both are risk numbers, and they answer different questions. The panel puts them side by side for eight household names over the five years to July 31, 2026, with volatility annualized from daily closes.
The exact SQL behind every number
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS session_date,
argMax(toFloat64(close), window_start) AS close_px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'AAPL', 'MSFT', 'KO', 'JNJ', 'CVX', 'VZ', 'PG')
AND toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2021-08-01')
AND toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-07-31')
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY ticker, session_date
),
runs AS (
SELECT ticker,
session_date,
close_px,
max(close_px) OVER (PARTITION BY ticker ORDER BY session_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_peak,
lagInFrame(close_px, 1) OVER (PARTITION BY ticker ORDER BY session_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM daily
)
SELECT ticker,
round(100 * max(1 - close_px / running_peak), 1) AS max_drawdown_pct,
round(100 * sqrt(252) * stddevSampIf(close_px / prev_close - 1, prev_close > 0), 1) AS annualized_volatility_pct
FROM runs
GROUP BY ticker
HAVING countIf(prev_close > 0) > 20
ORDER BY max_drawdown_pct DESCRead the two columns together. VZ carries the deepest fall in the group at 45.4%, against annualized volatility of 22.6%. At the shallow end, KO fell 20.9% with volatility of 16.7%. Volatility is an average over every day in the window, and averages forgive a long slow decline. Drawdown is the single worst path the price actually walked. A quiet holding that drifts lower for two years can post a deep drawdown with an unremarkable volatility reading.
The low volatility anomaly looks at how the calm names behave over long horizons, and Kelly criterion position sizing shows how a drawdown tolerance turns into a position size.
What the number cannot tell you
Four limits are worth carrying around:
- It is one realised path. The same process could have produced a much worse figure on a different draw.
- It depends on sample length, so two drawdowns compare only over matching windows.
- It depends on sampling frequency. Month end closes hide the intraday lows and produce a smaller number than daily closes over the same period.
- It carries no forward information. Sitting 30% below a peak says nothing about whether the 31st point arrives tomorrow.
That last limit is why an automated system needs a drawdown policy rather than a drawdown threshold. "Halt at 20%" answers one question and leaves two unanswered: what happens to open positions at the halt, and what evidence turns the system back on. Circuit breakers for trading bots works through the shape of that policy. The cost of an arbitrary flat rule lands in the recovery leg, where a system that stops near the low and restarts late sits out the climb. Missing the best days puts numbers on that arithmetic.
One measurement detail matters before comparing two figures: a drawdown on a fund's price series and a drawdown on a real account differ whenever cash moves in or out and whenever dividends land, which how monthly returns are measured covers in full.
Maximum drawdown FAQ
What is a good maximum drawdown?
There is no universal figure, only a benchmark and a window to measure against. Over the ten years to July 31, 2026, SPY's worst fall from a running peak measured 34.2% on daily closes. A strategy quoting a smaller number over a shorter sample has not necessarily been safer.
Is maximum drawdown the same as volatility?
No. Volatility describes the typical daily move; maximum drawdown describes the single worst peak to trough path. A low volatility holding that declines steadily for two years can carry a deeper drawdown than a jumpy one that keeps setting new highs.
How long does it take to recover from a maximum drawdown?
It varies, and the climb back is often the longer half. For SPY since 2016, the deepest completed stretch below a prior high ran 181 calendar days from peak to new high, of which 33 were spent falling.
Does maximum drawdown predict future losses?
No. It describes one path that already happened, and a longer sample almost always produces a larger figure. It records what a strategy has survived so far, with no bound on what comes next.
Every panel above stores the SQL behind it. Open one, swap the ticker or the window, and run the same drawdown arithmetic on the Strasmore terminal.