What is a drawdown?

Measure how far an equity curve sits below its own running peak, and how long it stays there.

A drawdown is how far the equity curve sits below its own highest point so far. If one unit of money grew to 1.20 and then fell to 0.90, the drawdown is 0.90 / 1.20 - 1, which is -25%. The maximum drawdown is the worst of those numbers over the whole curve.

In this lesson I compute the drawdown of a six-point curve I can check by hand, then on AAPL: the deepest fall, the day it hit bottom, the peak it fell from, and how many trading days it took to get back to that peak.

Step 1. Six points

cummax() is the running maximum, in the same family as cumprod() from Lesson 21. Row 0 is the first value, row 1 is the larger of the first two, row 2 is the largest of the first three. It never goes down.

import pandas as pd

equity = pd.Series([1.00, 1.20, 1.10, 0.90, 1.05, 1.30])

peak = equity.cummax()      # highest value up to and including this row

print(peak)
# -> 0    1.0
# -> 1    1.2
# -> 2    1.2
# -> 3    1.2
# -> 4    1.2
# -> 5    1.3
# -> dtype: float64
0    1.0
1    1.2
2    1.2
3    1.2
4    1.2
5    1.3
dtype: float64

The curve reached 1.20 on row 1, so the peak stays at 1.20 through rows 2, 3 and 4 even while the curve falls. On row 5 the curve makes a new high and the peak follows it up.

The drawdown is the curve divided by that peak, minus 1.

drawdown = equity / peak - 1

print(pd.DataFrame({"equity": equity, "peak": peak, "drawdown": drawdown}).round(4))
# ->    equity  peak  drawdown
# -> 0    1.00   1.0    0.0000
# -> 1    1.20   1.2    0.0000
# -> 2    1.10   1.2   -0.0833
# -> 3    0.90   1.2   -0.2500
# -> 4    1.05   1.2   -0.1250
# -> 5    1.30   1.3    0.0000
   equity  peak  drawdown
0    1.00   1.0    0.0000
1    1.20   1.2    0.0000
2    1.10   1.2   -0.0833
3    0.90   1.2   -0.2500
4    1.05   1.2   -0.1250
5    1.30   1.3    0.0000

Drawdown is zero on every row that sets a new high, and negative everywhere else. It is never positive, because the curve can never be above its own running maximum.

Row 3 by hand: the curve is at 0.90 and the peak behind it is 1.20.

print(round(0.90 / 1.20 - 1, 4))        # -> -0.25
print(round(drawdown.iloc[3], 4))       # -> -0.25
-0.25
-0.25

The maximum drawdown is .min(), and .idxmin() gives the row where it happened.

print(f"{drawdown.min():.2%}")          # -> -25.00%
print(drawdown.idxmin())                # -> 3
-25.00%
3

Row 5 is a new high, so the curve ends with no drawdown at all, even though it lost a quarter of its value along the way.

Step 2. AAPL, buy and hold

prices.csv sits next to this lesson and holds simulated data: daily closes for AAPL, MSFT, NVDA and JNJ from 2020 to 2025. I take AAPL, turn the closes into returns, and compound them into an equity curve.

prices = pd.read_csv("prices.csv", parse_dates=["Date"])

aapl   = prices[prices["Ticker"] == "AAPL"].set_index("Date")
ret    = aapl["Close"].pct_change().dropna()
equity = (1 + ret).cumprod()

print(len(equity))                          # -> 1565
print(f"{equity.iloc[-1] - 1:.2%}")         # -> 165.45%
1565
165.45%

Buying and holding turned one unit of money into 2.65. Now the same two lines as Step 1, on 1565 rows instead of six.

peak     = equity.cummax()
drawdown = equity / peak - 1

max_dd      = drawdown.min()
trough_date = drawdown.idxmin()

print(f"{max_dd:.2%}")           # -> -29.82%
print(trough_date.date())        # -> 2023-06-23
-29.82%
2023-06-23

The worst point of the whole sample is 23 June 2023, where the curve was 29.82% below the best level it had reached before that day.

print((drawdown < 0).sum(), "of", len(drawdown), "days below the peak")
# -> 1472 of 1565 days below the peak
1472 of 1565 days below the peak

Step 3. From the peak, to the bottom, and back

Three dates describe the fall: the peak it started from, the trough, and the day the curve got back to the old peak. The peak date is the highest point of the curve up to the trough, which is .idxmax() on the slice ending at the trough.

peak_date = equity.loc[:trough_date].idxmax()

print(peak_date.date())                       # -> 2022-05-03
print(round(equity.loc[peak_date], 4))        # -> 1.8705
print(round(equity.loc[trough_date], 4))      # -> 1.3127
2022-05-03
1.8705
1.3127

Those two levels give the drawdown back.

print(round(equity.loc[trough_date] / equity.loc[peak_date] - 1, 4))    # -> -0.2982
-0.2982

The fall itself took 298 trading days. equity.loc[peak_date:trough_date] is the slice from one date to the other, and its length counts both ends, so I subtract one to count the steps between them.

print(len(equity.loc[peak_date:trough_date]) - 1)      # -> 298
298

For the recovery I take everything from the trough onwards and keep the rows that are back at the old peak level. The first of those is the day the drawdown closed.

after     = equity.loc[trough_date:]
recovered = after[after >= equity.loc[peak_date]]

back_date = recovered.index[0]

print(back_date.date())                                # -> 2024-06-07
print(len(equity.loc[trough_date:back_date]) - 1)      # -> 250
print(len(equity.loc[peak_date:back_date]) - 1)        # -> 548
2024-06-07
250
548

AAPL took 298 trading days to fall 29.82% and another 250 to climb back, 548 days from the old peak to the new one.

Step 4. All four tickers

The same three lines work on any close column, so I put them in a function and run it on each ticker.

def drawdown_of(close):
    equity = (1 + close.pct_change().dropna()).cumprod()
    return equity / equity.cummax() - 1

for ticker in ["AAPL", "MSFT", "NVDA", "JNJ"]:
    close = prices[prices["Ticker"] == ticker].set_index("Date")["Close"]
    dd    = drawdown_of(close)
    print(f"{ticker:5s} {dd.min():7.2%}   {dd.idxmin().date()}")
# -> AAPL  -29.82%   2023-06-23
# -> MSFT  -32.95%   2023-11-27
# -> NVDA  -55.97%   2023-06-28
# -> JNJ   -20.17%   2025-11-06
AAPL  -29.82%   2023-06-23
MSFT  -32.95%   2023-11-27
NVDA  -55.97%   2023-06-28
JNJ   -20.17%   2025-11-06

NVDA fell 55.97% from its peak and JNJ fell 20.17%. Over the same six years NVDA returned 272.55% and JNJ returned 107.03%.

Your turn

Take NVDA. Find the peak date its worst drawdown started from, how many trading days the fall took, and whether the curve got back to that peak before the end of the sample.

import pandas as pd

prices = pd.read_csv("prices.csv", parse_dates=["Date"])

nvda   = prices[prices["Ticker"] == "NVDA"].set_index("Date")
equity = (1 + nvda["Close"].pct_change().dropna()).cumprod()

peak     = equity.cummax()
drawdown = equity / peak - 1

trough_date = drawdown.idxmin()
peak_date   = equity.loc[:trough_date].idxmax()

print(f"{drawdown.min():.2%}")                          # -> -55.97%
print(peak_date.date())                                 # -> 2022-10-03
print(trough_date.date())                               # -> 2023-06-28
print(len(equity.loc[peak_date:trough_date]) - 1)       # -> 192

after     = equity.loc[trough_date:]
recovered = after[after >= equity.loc[peak_date]]

print(len(recovered))                                   # -> 11
print(recovered.index[0].date())                        # -> 2025-10-29
print(len(equity.loc[trough_date:recovered.index[0]]) - 1)   # -> 610

NVDA fell for 192 trading days and needed 610 more to get back. Only 11 days in the sample sit at or above that old peak, and they are the last 11.