Calculating Maximum Drawdown and Drawdown Duration from Returns
Summary
The document explains how to calculate portfolio drawdowns from cumulative returns and find both the deepest decline from a prior peak and the longest period spent below a peak. It identifies a common pandas indexing error: integer access can fail when a Series uses timestamps or another noninteger index. Using cumulative maximum values avoids dependence on index labels when constructing the drawdown series.
For duration, the document proposes tracking consecutive observations with positive drawdown using an accumulator, and also shows a grouping-based alternative. A second answer gives an equivalent drawdown expression normalized by the high-water mark. These are implementation suggestions rather than a comparison on sample data; the document provides no empirical results. The formulas presume cumulative returns are represented consistently, and the discussion does not address missing values, irregular observation spacing, or how to define duration in calendar time.
Key ideas
- A pandas Series indexed by dates may not support positional access through integer labels.
- Use cumulative maxima of cumulative returns to identify each observation's previous peak.
- Drawdown is measured as the decline from a peak relative to the current portfolio value in the first formulation.
- The longest drawdown duration can be found by accumulating consecutive periods with positive drawdown.
- An alternative expression normalizes the decline by the peak value.
Tags
Full text
# Global Maximum Drawdown and Maximum Drawdown Duration Implementation in Python
# Global Maximum Drawdown and Maximum Drawdown Duration Implementation in Python
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()
drawdownduration[:]=0
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
else:
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
## Answer by chrisaycock (score 5, accepted)
https://quant.stackexchange.com/a/55131
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()
```
## Answer by Kevin (score 1)
https://quant.stackexchange.com/a/60315
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}$$
Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.