Following along with E.P. Chan's book, I'm attempting to calculate the maximum drawdown and the longest drawdown duration from cumulative portfolio returns. He codes it in MATLAB, but I wanted to try my hand at the same code in Python.

import pandas as pd

def drawdownCalculator(data):
    highwatermark = data.copy()
    highwatermark[:] = 0
    drawdown = data.copy()
    drawdown[:] = 0
    drawdownduration = data.copy()
    t = 1
    while t <= len(data):
        highwatermark[t] = max(highwatermark[t-1], data[t])
        drawdown[t] = (1 + highwatermark[t])/(1 + data[t]) - 1
        if drawdown[t] == 0:
            drawdownduration[t] = 0
            drawdownduration[t] = drawdownduration[t-1] + 1
        t += 1
    return drawdown.max(), drawdownduration.max()
max_drawdown, max_drawdown_time = drawdownCalculator(cumulative_returns) #cumulative_returns is a Pandas series

I thought I had it figured out, but I'm getting the following error:

return self._engine.get_value(s, k, tz=getattr(series.dtype, "tz", None))
  File "pandas/_libs/index.pyx", line 80, in pandas._libs.index.IndexEngine.get_value
  File "pandas/_libs/index.pyx", line 88, in pandas._libs.index.IndexEngine.get_value
  File "pandas/_libs/index.pyx", line 131, in pandas._libs.index.IndexEngine.get_loc
  File "pandas/_libs/hashtable_class_helper.pxi", line 992, in pandas._libs.hashtable.Int64HashTable.get_item
  File "pandas/_libs/hashtable_class_helper.pxi", line 998, in pandas._libs.hashtable.Int64HashTable.get_item
KeyError: 0

Thank you in advance


2 Answers 2


I'm guessing your Series is indexed by a timestamp, which would explain why accessing by an integer doesn't work. But I can't tell for sure since you haven't shown us any data.

The good news is that I don't need that anyway. Here is a more idiomatic way to compute what you want:

highwatermarks = cumulative_returns.cummax()

drawdowns = (1 + highwatermarks)/(1 + cumulative_returns) - 1

max_drawdown = max(drawdowns)

There is no simple way to compute duration with array notation. Fortunately, this question shows how to use an accumulator for exactly your scenario:

from itertools import accumulate

drawdown_times = (drawdowns > 0).astype(np.int64)

max_drawdown_time = max(accumulate(drawdown_times, lambda x,y: (x+y)*y))

Alternatively, you can group the consecutive durations together. I don't recommend this approach, but I'll include it for posterity:

max_drawdown_time = drawdown_times.groupby((drawdown_times != drawdown_times.shift()).cumsum()).cumsum().max()
  • $\begingroup$ Thank you for your answer! By the way, I was looking through the proposed edits on this answer, are the formulas I was using incorrect? $\endgroup$
    – DickyBrown
    Commented Jun 22, 2020 at 21:58
  • $\begingroup$ @DickyBrown I doubt it matters how you define "drawdown" as long as you are consistent. I've used pnl.cummax() - pnl; ie., the positive difference in P&L. $\endgroup$ Commented Jun 22, 2020 at 21:59
  • $\begingroup$ @chrisaycock Your code does not return a max drawdown. For example, it's returns ~122% as the max draw for SPY in 2008...it should be ~55% $\endgroup$
    – amdopt
    Commented Jun 22, 2020 at 22:06
  • $\begingroup$ @amdopt You are free to add your own answer or comment to make your case. You are not entitled to fundamentally alter someone else's post. $\endgroup$ Commented Jun 22, 2020 at 22:09

In addition to chrisaycock's answer, we could also normalize the maximum drawdown with the high wartermarks instead of the current cumulative returns.

highwatermarks = cumulative_returns.cummax()
drawdowns = 1 - (1 + cumulative_returns) / (1 + highwatermarks)
max_drawdown = max(drawdowns)

Here we observe the drawdown definition defined here. $$MDD=\frac{Trough\ Value−Peak\ Value}{Peak\ Value}$$

  • 1
    $\begingroup$ Thanks for sharing. Your equation might do the work. But technically you need to insert a condition that time point of Trough Value should be after that of Peak Value. $\endgroup$
    – FlyUFalcon
    Commented Aug 2, 2022 at 7:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.