Maximum Drawdown: How Deep and How Long Recovery Dey
Maximum drawdown na the biggest peak-to-trough fall for a value series. Learn wetin deep drawdown mean and why recovery fit take longer to reach new high.
Maximum drawdown na the biggest fall from peak reach trough for one value series, measured as percentage of the peak wey dey run. If account climb reach $120,000, fall reach $84,000, then recover, e get maximum drawdown of 30%. That number go remain forever because the statistic dey record the worst period for the account history, no be the situation today. Most people dey mention only how deep the fall be. But two other numbers dey inside the same statistic: the time from the peak down to the low, and the much longer time from that low back to a new high.
Maximum drawdown dey calculate how?
The whole calculation na one pass through the series. For each observation, you update the running maximum, measure the current value against am, then keep the worst reading wey you don see so far.
- Set the running peak to the first value.
- For each new value, raise the peak if the value higher.
- Calculate the drawdown for that point: value divide by peak, minus one.
- Keep the reading wey most negative. Na that one be maximum drawdown.
No returns series, no volatility estimate, and no distribution assumption enter the calculation. The loop dey run on ordinary list of numbers with Python standard library. For fresh Ubuntu box, install the interpreter first:
apt-get update && apt-get install -y python3
Save the loop as maxdd.py. The prices below na made-up ten-point series. Dem small enough make you check am 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 e go print maximum drawdown: -15.4%. Check am by hand: the peak before the low na 104, the low na 88, and 88 divide by 104 na 0.846. The series later end at 106, wey be all-time high, but the 15.4% still remain. Maximum drawdown na permanent record of the worst moment for one sample. E no talk anything about where the series dey today. Two more variables inside the same loop fit capture the timing: the index where the peak dey set, and the index where the worst reading happen.
Drawdown curve dey look like wetin
Drawdown curve dey keep step 3 for every point, instead of reducing am to one worst value. The line dey stay for zero anytime the series print new high, and e hang below zero in between. Na why dem dey call the gap “underwater”. See the curve for SPY, an exchange traded fund wey dey track S&P 500, based 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 dey spend long periods near zero, then e drop enter small number of deep notches. The final month for the record read -1.27% against the running peak. Two features of the shape dey show the lesson clearly. Recoveries dey climb slowly instead of bouncing sharply. And every new peak reset the curve to zero, so the chart no longer show the visual memory of wetin happen before.
How long e dey take to recover from a drawdown?
Three different numbers dey inside one drawdown, and treating dem as one na the commonest way people dey misread performance table:
- Depth: how far the value fall below its peak, in percent.
- Time to the low: calendar days from the peak reach the bottom.
- Recovery time: calendar days from that bottom reach a new high.
The third one usually dey longest, and na the one headline number never show. The panel below rebuild every completed underwater stretch for SPY since 2016, using daily closes. A stretch start on the day peak set, and e end on the day the fund first close above that peak.
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 for this list start for Feb 2020 and reach 34.2% below the previous peak. E spend 33 calendar days falling and 148 climbing back, making 181 days underwater altogether. The next entry, from Jan 2022, no deep reach 25.4%, but e keep the fund below its old high for 746 days.
Depth and duration fit separate completely. A sharp fall with quick round trip and a shallow decline wey last for years both dey reduce to one percentage. The March 2020 crash na textbook example of the first pattern, while how markets recover from crashes dey follow the second leg of the journey.
Why maximum drawdown dey worse as you measure am for longer
Maximum drawdown na maximum, so adding more history fit only leave am the same or make am deeper. For longer window, the running peak dey at least as high at every point as the peak for shorter window. Na this one explain the arithmetic. If you measure the same fund across five windows wey all end for July 31, 2026, you go see 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_yearsFor the trailing 1 year window, the worst fall na 9.1%. If you stretch the same series reach 10 years, the reading become 34.2%, and the worst point happen for Mar 2020. The fund no become more risky. Na the sample just become longer.
The same arithmetic dey distort strategy comparisons. Three-year backtest and twenty-year backtest fit describe the same behaviour but still report very different drawdowns. So, the figure no mean much unless you attach the measurement window to am. Look-ahead bias for backtesting dey cover another way wey clean-looking backtest fit flatter itself.
Maximum drawdown vs volatility
Volatility dey measure the normal size of daily move for either direction. Maximum drawdown dey measure one particular downward path. Both na risk numbers, but dem dey answer different questions. The panel put dem side by side for eight household names over the five years reach 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 carry the deepest fall for the group at 45.4%, against annualized volatility of 22.6%. For the shallow end, KO fall 20.9% with volatility of 16.7%. Volatility na average across every day for the period, and averages fit overlook one long, slow decline. Drawdown na the single worst path wey the price actually follow. Holding wey dey look quiet but drift lower for two years fit record deep drawdown with volatility reading wey no look unusual.
The low volatility anomaly dey examine how calm names behave over long horizons, while Kelly criterion position sizing show how drawdown tolerance dey turn into position size.
Wetin the number no fit tell you
Four limits dey important to remember:
- Na one realised path. The same process fit don produce much worse figure for another draw.
- E depend on sample length, so you fit compare two drawdowns only across matching windows.
- E depend on sampling frequency. Month-end closes dey hide intraday lows and produce smaller number than daily closes for the same period.
- E no carry any forward information. If something dey 30% below peak, e no tell you whether the 31st point go show tomorrow.
Na this last limit make automated system need drawdown policy, no be only drawdown threshold. “Halt at 20%” answer one question but leave two unanswered: wetin go happen to open positions when the halt happen, and wetin evidence go turn the system back on. Circuit breakers for trading bots dey explain the structure of that policy. The cost of arbitrary flat rule dey show for the recovery leg, where system wey stop near the low and restart late go miss the climb. Missing the best days put numbers on that calculation.
One measurement detail matter before you compare two figures: drawdown from fund price series and drawdown from real account no dey be the same whenever cash enter or comot and whenever dividends land. how monthly returns are measured explain this fully.
Maximum drawdown FAQ
Maximum drawdown wey good na which one?
No be one universal figure dey. Na benchmark plus time window wey you use measure am. For the ten years reach July 31, 2026, SPY worst fall from one running peak measure 34.2% on daily closes. Strategy wey quote smaller number for shorter sample no mean say e safer.
Maximum drawdown and volatility na the same thing?
No. Volatility dey describe the normal daily movement. Maximum drawdown dey describe the single worst journey from peak reach trough. Holding wey get low volatility but dey fall steadily for two years fit get deeper drawdown pass jumpy one wey still dey make new highs.
How long e dey take to recover from maximum drawdown?
E dey vary, and the climb back often dey take the longer half. For SPY since 2016, the deepest completed period below previous high last 181 calendar days from peak reach new high. Out of that period, 33 days na when e dey fall.
Maximum drawdown dey predict future losses?
No. E dey describe one path wey don already happen, and longer sample almost always dey produce bigger figure. E record wetin strategy don survive so far, but e no set any limit for wetin fit happen next.
Every panel above store the SQL wey dey behind am. Open one, change the ticker or the window, then run the same drawdown calculation for Strasmore terminal.