4. Useful Functions#

4.1. Find the maximum drawdown#

PerformanceAnalytics provides several functions to calculate the drawdowns. What I offered here is an alternative to PerformanceAnalytics::table.Drawdowns. The benefits of my solutoin are:

  • It’s 1.89x faster than table.Drawdowns.

  • It doesn’t require the user to convert the input into a zoo or xts object, which is convenient if you’re using data.table.

4.1.1. Create a Sample Data#

The only inputs of our drawdowns function are 1) a return vector ret and 2) a date vector date. Note they should belong to the same stock.

Let’s generate these two variables.

set.seed(42)

# Generate 100 returns with mean 0 and standard deviation 0.05
ret = rnorm(100, mean=0, sd=0.05)

# Generate 100 dates starting from 2010/1/1
date = seq(as.Date("2010/1/1"), by="day", length.out=100)

4.1.2. The drawdowns Function#

The drawdowns below is a faster alternative to PerformanceAnalytics.

Inputs:

  • ret: a vector of returns

  • date: a vector of dates

  • top: the number of largest drawdowns to be returned

Output: A data.table with the following columns:

  • start: the start date of the drawdown

  • trough: the date of the trough

  • end: the last day of the darwdown (if new watermark hasn’t been reached yet, it’s the last day in the sample).

  • depth: the drawdown (percentage).

library(data.table)

drawdowns <- function(ret, date, top=5) {
    # This function calculates the drawdowns for a given return series
    # Input:
    #   ret: a vector of returns
    #   date: a vector of dates
    #   top: the number of largest drawdowns to be returned

    input = data.table(date, ret)[order(date)]

    # a drawdown is defined whenever the cumulative return is below the previous peak
    drawdowns_raw = input[, {
        ret_cum=cumprod(1 + ret)
        ret_cummax=cummax(c(1, ret_cum))[-1]
        drawdowns=ret_cum/ret_cummax - 1
        list(date, ret, ret_cum, ret_cummax, drawdowns)
    }]

    # print(drawdowns_raw)

    # find the largest drawdowns
    drawdowns = drawdowns_raw[, ':='(grp = rleid(drawdowns<0))
        ][drawdowns<0, 
        .(start=date[1], trough=date[which.min(drawdowns)],
          end=date[.N], depth=drawdowns[which.min(drawdowns)]),
        keyby=.(grp)
        ][order(depth)]

    # return the top drawdowns
    top = fifelse(top > nrow(drawdowns), nrow(drawdowns), top)
    drawdowns = drawdowns[1:top][, ':='(grp=NULL)]
    
    return(drawdowns[])
}

drawdowns(ret, date, top=2)
A data.table: 2 × 4
starttroughenddepth
<date><date><date><dbl>
2010-01-132010-02-162010-04-10-0.49650974
2010-01-022010-01-022010-01-03-0.02823491

If we use PerformanceAnalytics::table.Drawdowns, the code and result are offered below. As you can see, the result is the same.

suppressMessages({library(PerformanceAnalytics)})

inputs = zoo(ret, order.by=date)
table.Drawdowns(inputs, top=2)[, c('From', 'Trough', 'Depth')]
A data.frame: 2 × 3
FromTroughDepth
<date><date><dbl>
2010-01-132010-02-16-0.4965
2010-01-022010-01-02-0.0282