3 min read

Backtesting a trend following strategy

Introduction

The idea of this post is to measure the performance of a simple trend following strategy. The implementation of the strategy is the simplest possible but I believe it’s interesting as a result. The idea is to be long the S&P 500 ETF if the long term trend is positive. If the trend is negative I’m out of the market. The objective of the strategy is to reduce the portfolio drawdown even though it reduces the strategy return.

Get the data

I start by reading the data, for this example I use the SPY ETF.

options("getSymbols.warning4.0"=FALSE)

library(quantmod)
library(xts)
library(PerformanceAnalytics)
library(magrittr)

SPY = getSymbols('SPY', src='yahoo', auto.assign=FALSE, from='1990-01-01')

Define the signal

The signal I’ll use is the rate of change of the 120 SMA. In order to implement it I’ll need to compute the SMA(x, 120), compute the ROC and then lag it to prevent adding a look-ahead bias

roc_sma <- function(x){
  sma = SMA(Ad(x), n=120)
  indicator = ROC(sma, n=1)
  Lag(indicator, 1)
}

indicator = roc_sma(SPY)
SPY$signal = ifelse(indicator >= 0, 1, 0) 
tail(SPY)
##            SPY.Open SPY.High SPY.Low SPY.Close SPY.Volume SPY.Adjusted signal
## 2020-06-15   298.02   308.28  296.74    307.05  135782700     305.7047      0
## 2020-06-16   315.48   315.64  307.67    312.96  137627500     311.5888      0
## 2020-06-17   314.07   314.39  310.86    311.66   82954600     310.2945      0
## 2020-06-18   310.01   312.30  309.51    311.78   80828700     310.4140      0
## 2020-06-19   314.17   314.38  306.53    308.64  135549600     308.6400      0
## 2020-06-22   307.99   311.05  306.75    310.62   74649400     310.6200      0

quantmod makes it simple to include the indicator to a plot. It’s clear it doesn’t work perfectly but it’s definately negative at the end of 2008.

add_indicator <- newTA(roc_sma, col='green', type='h')
chartSeries(SPY["2006/2012"], 
            TA="addSMA(120, col='green'); add_indicator()")

The first step to evaluate the strategy is to compute the simple return of just holding SPY.

SPY$R = Ad(SPY)/Lag(Ad(SPY), 1) - 1
SPY = SPY[complete.cases(SPY), ]
tail(SPY)
##            SPY.Open SPY.High SPY.Low SPY.Close SPY.Volume SPY.Adjusted signal
## 2020-06-15   298.02   308.28  296.74    307.05  135782700     305.7047      0
## 2020-06-16   315.48   315.64  307.67    312.96  137627500     311.5888      0
## 2020-06-17   314.07   314.39  310.86    311.66   82954600     310.2945      0
## 2020-06-18   310.01   312.30  309.51    311.78   80828700     310.4140      0
## 2020-06-19   314.17   314.38  306.53    308.64  135549600     308.6400      0
## 2020-06-22   307.99   311.05  306.75    310.62   74649400     310.6200      0
##                        R
## 2020-06-15  0.0093356162
## 2020-06-16  0.0192476392
## 2020-06-17 -0.0041538110
## 2020-06-18  0.0003850406
## 2020-06-19 -0.0057149033
## 2020-06-22  0.0064151759

Define the strategy

In order to compute the strategy return I simply multiply the SPY return to the signal.

SPY$R_strategy = SPY$R * SPY$signal

I Remove NA values and change the column names to have more descriptive labels.

ret = SPY[, c("R", "R_strategy")]
names(ret) = c("Buy-Hold", "Trend Following")
ret = ret[complete.cases(ret), ]

Evaluate returns

The annualized returns seem promising during the period given the objective outlined above. The returns of the trend-following strategy are ~2% lower but the volatility is also reduced, therefore the Share ratio is higher.

table.AnnualizedReturns(ret, geometric=FALSE)
##                           Buy-Hold Trend Following
## Annualized Return           0.1092          0.0804
## Annualized Std Dev          0.1904          0.1214
## Annualized Sharpe (Rf=0%)   0.5736          0.6620

Even though the returns are lower long term the idea of being long the market when the trend is positive makes it more realistic for an investor to stick with it in the long run as the drawdowns are reduced. The following plots help give an intuition of how this works.

charts.PerformanceSummary(ret, wealth.index=TRUE, geometric=FALSE)

Below I compare the results of both strategies in market downturns.

charts.PerformanceSummary(ret["1997/2002"], wealth.index=TRUE, geometric=FALSE)

charts.PerformanceSummary(ret["2007/2010"], wealth.index=TRUE, geometric=FALSE)

charts.PerformanceSummary(ret["2019-06/2020"], wealth.index=TRUE, geometric=FALSE)

charts.PerformanceSummary(ret["2017/2019"], wealth.index=TRUE, geometric=FALSE)