Logo Icon

Backtesting Part 2: Splits, Dividends, Trading Costs and Log Plots

Note: This post is NOT financial advice! This is just a fun way to explore some of the capabilities R has for importing and manipulating data.

UPDATE 9/21/2011: Costas correctly points out that I should lag my strategy vector by one day, as today’s returns are determined the position we chose yesterday. I updated the code, results, and charts to reflect this. Please alert me to any similar errors in the future.

In my last post, I demonstrated how to backtest a simple momentum-based stock trading strategy in R. However, there were a few issues with my implementation: I ignored splits, dividends and transaction costs, all of which can have a large impact on a strategy. I also came up with a better plot to help show how a given strategy compares to a benchmark, and wrapped everything together into one function. c First of all, we need to reload our functions from the last post. These functions define our strategy and analyze its performance.

set.seed(42)

daysSinceHigh <- function(x, n){
   apply(embed(x, n), 1, which.max)-1
}

myStrat <- function(x, nHold=100, nHigh=200) {
    position <- ifelse(daysSinceHigh(x, nHigh)<=nHold,1,0)
    c(rep(0,nHigh-1),position)
}

Performance <- function(x) {

    cumRetx = Return.cumulative(x)
    annRetx = Return.annualized(x, scale=252)
    sharpex = SharpeRatio.annualized(x, scale=252)
    winpctx = length(x[x > 0])/length(x[x != 0])
    annSDx = sd.annualized(x, scale=252)

    DDs <- findDrawdowns(x)
    maxDDx = min(DDs$return)
    maxLx = max(DDs$length)


    Perf = c(cumRetx, annRetx, sharpex, winpctx, annSDx, maxDDx, maxLx)
    names(Perf) = c("Cumulative Return", "Annual Return","Annualized Sharpe Ratio",
        "Win %", "Annualized Volatility", "Maximum Drawdown", "Max Length Drawdown")
    return(Perf)
}

Next, we can test our strategy. I’ve added a couple new indexes:

testStrategy <- function(symbol, nHold=100, nHigh=200, cost=.005, ylog=FALSE, wealth.index = FALSE) {
    require(quantmod)

    #Load Data
    myStock <- getSymbols(symbol, from='2003-01-01')
    myStock <- adjustOHLC(get(myStock),symbol.name=symbol)
    myStock <- Cl(myStock)
  myStock <- na.omit(myStock)

    #Determine position
    myPosition <- myStrat(myStock,nHold,nHigh)
    bmkReturns <- dailyReturn(myStock, type = "arithmetic")
    myReturns <- bmkReturns*Lag(myPosition,1)
    myReturns[1] <- 0
    names(bmkReturns) <- symbol
    names(myReturns) <- 'Me'

    #Add trading costs
    trade =  as.numeric(myPosition!=Lag(myPosition,1))
    trade[1] = 1
    trade = trade*cost
    myReturns = myReturns-trade

    #Make plot
    require(PerformanceAnalytics)
    symbol <- sub('^','',symbol,fixed=TRUE)
    Title <- paste('High=',nHigh,' Hold=',nHold,' on ',symbol,sep='')
    if (ylog) {wealth.index = TRUE}
    layout(matrix(c(1, 2, 3)), height = c(2, 1, 1.3), width = 1)
    par(mar = c(1, 4, 4, 2))
    print(chart.CumReturns(cbind(bmkReturns,myReturns), main=Title, ylab = "Cumulative Return",
                        wealth.index = wealth.index,ylog=ylog))
    print(chart.RelativePerformance(myReturns,bmkReturns, ylab = "Relative Return", main = ""))
    print(chart.Drawdown(cbind(bmkReturns,myReturns),legend.loc = 'bottomleft', ylab = "Drawdown", main = ""))

    #Return Benchmarked Stats
    round(cbind(Me=Performance(myReturns),Index=Performance(bmkReturns)), 2)
}

This function does a lot of lifting: 1. It loads the data and adjusts the closing price for splits and dividends. It uses the splits/dividends data from yahoo, but performs its own, more accurate calculations. 2. It determines a position series, based on the “daysSinceHigh” function. This part is the same as in my last post. 3. It determines trades, which are defined as days when today’s position is different from the previous day’s positions. I assumed that transactions costs are 0.5% of equity, so on trading days I subtracted 0.005 from my Returns. 4. It makes a plot. This plot is different from the charts.PerformanceSummary we used last time. The first plot shows cumulative returns of my strategy and the index, while the second plot shows the relative performance of my strategy over the benchmark (also known as alpha). The third plot shows drawdowns. 5. It returns a data table of statistics, comparing the strategy to the benchmark.

I tested this strategy on GSPC, FTSE, DJI, N225, EEM, EFA, and GLD. (The last 3 are ETFs). The strategy performs well on some indexes, and poorly on others. Here’s the results of my backtest:

testStrategy('^GSPC',100,200,ylog=TRUE)
A plot comparing the performance of a custom trading strategy (Me) to the GSPC index (S&P 500) from January 2003 to August 2024. The cumulative return plot shows the GSPC index achieving significant growth, especially after 2010, while the custom strategy follows with more modest gains. The daily return plot illustrates the volatility in the GSPC index, with sharper movements compared to the more stable returns of the custom strategy. The drawdown plot highlights deeper losses during market downturns for both the index and the strategy, although the custom strategy generally experiences less severe drawdowns.
#>                             Me   Index
#> Cumulative Return         2.55    5.16
#> Annual Return             0.06    0.09
#> Annualized Sharpe Ratio   0.43    0.46
#> Win %                     0.55    0.54
#> Annualized Volatility     0.14    0.19
#> Maximum Drawdown         -0.34   -0.57
#> Max Length Drawdown     632.00 1376.00
testStrategy('^FTSE',100,200)
A plot comparing the performance of a custom trading strategy (Me) to the FTSE index from January 2003 to August 2024. The cumulative return plot shows the FTSE index experiencing fluctuations with periods of growth and decline, while the custom strategy generally underperforms, though it follows similar trends. The daily return plot highlights volatility in both the index and the strategy, with the index showing more frequent and larger movements. The drawdown plot indicates significant losses during market downturns for both, but the custom strategy tends to have less severe drawdowns.
#>                              Me   Index
#> Cumulative Return          0.76    1.06
#> Annual Return              0.03    0.03
#> Annualized Sharpe Ratio    0.25    0.19
#> Win %                      0.53    0.53
#> Annualized Volatility      0.11    0.17
#> Maximum Drawdown          -0.24   -0.48
#> Max Length Drawdown     2858.00 1496.00
testStrategy('^DJI',100,200,ylog=TRUE)
A plot comparing the performance of a custom trading strategy (Me) to the DJI index (Dow Jones Industrial Average) from January 2003 to August 2024. The cumulative return plot shows the DJI index experiencing significant growth, especially post-2010, while the custom strategy underperforms, showing more modest gains. The daily return plot illustrates the volatility in the DJI index with noticeable fluctuations, while the custom strategy shows smoother, less volatile returns. The drawdown plot highlights periods of significant losses for both the index and the strategy, with the strategy generally experiencing less severe drawdowns.
#>                             Me   Index
#> Cumulative Return         1.49    3.77
#> Annual Return             0.04    0.07
#> Annualized Sharpe Ratio   0.32    0.42
#> Win %                     0.54    0.54
#> Annualized Volatility     0.13    0.18
#> Maximum Drawdown         -0.39   -0.54
#> Max Length Drawdown     894.00 1359.00
testStrategy('^N225',100,200)
A plot comparing the performance of a custom trading strategy (Me) to the N225 index (Nikkei 225) from January 2003 to August 2024. The cumulative return plot shows the N225 index experiencing significant growth, particularly after 2012, while the custom strategy lags with lower overall returns. The daily return plot reveals the index's volatility with noticeable fluctuations, whereas the custom strategy maintains more stable returns. The drawdown plot indicates periods of significant losses, where both the index and the strategy experience drawdowns, but the strategy generally has smaller and less frequent drawdowns.
#>                              Me   Index
#> Cumulative Return          0.83    3.20
#> Annual Return              0.03    0.07
#> Annualized Sharpe Ratio    0.17    0.31
#> Win %                      0.52    0.53
#> Annualized Volatility      0.17    0.23
#> Maximum Drawdown          -0.46   -0.61
#> Max Length Drawdown     2619.00 1862.00
testStrategy('EEM',100,200)
A plot comparing the performance of a custom trading strategy (Me) to the EEM index (Emerging Markets ETF) from April 2003 to August 2024. The cumulative return plot shows the EEM index achieving substantial growth with high volatility, while the custom strategy significantly underperforms, remaining flat with minimal returns. The daily return plot illustrates the high volatility of the EEM index, with frequent and large fluctuations, whereas the custom strategy shows more stable but consistently lower returns. The drawdown plot highlights deep and prolonged drawdowns for both the index and the strategy, with the strategy experiencing less severe but still significant drawdowns.
#>                              Me   Index
#> Cumulative Return          2.85   64.10
#> Annual Return              0.07    0.22
#> Annualized Sharpe Ratio    0.13    0.33
#> Win %                      0.53    0.53
#> Annualized Volatility      0.49    0.66
#> Maximum Drawdown          -0.71   -0.59
#> Max Length Drawdown     4245.00 1581.00
testStrategy('EFA',100,200)
A plot comparing the performance of a custom trading strategy (Me) to the EFA index (iShares MSCI EAFE ETF) from January 2003 to August 2024. The cumulative return plot shows the EFA index achieving substantial growth over the period, with some volatility, while the custom strategy underperforms, following the index's trend but with lower overall returns. The daily return plot highlights the index's fluctuations, showing larger and more frequent movements compared to the custom strategy, which remains more stable. The drawdown plot indicates significant periods of losses for both the index and the strategy, with the strategy experiencing smaller and less frequent drawdowns.
#>                             Me   Index
#> Cumulative Return         7.47   12.87
#> Annual Return             0.10    0.13
#> Annualized Sharpe Ratio   0.23    0.27
#> Win %                     0.54    0.54
#> Annualized Volatility     0.46    0.48
#> Maximum Drawdown         -0.37   -0.61
#> Max Length Drawdown     908.00 1660.00
testStrategy('GLD',100,200)
A plot comparing the performance of a custom trading strategy (Me) to the GLD index (SPDR Gold Shares ETF) from November 2004 to August 2024. The cumulative return plot shows the GLD index experiencing significant growth, especially during gold market rallies, while the custom strategy underperforms, with returns that are relatively flat and only show minor increases. The daily return plot illustrates the volatility in the GLD index, with sharp movements, compared to the smoother returns of the custom strategy. The drawdown plot highlights deep and prolonged losses during market downturns for both the index and the strategy, though the strategy generally has less severe drawdowns.
#>                              Me   Index
#> Cumulative Return          1.59    4.33
#> Annual Return              0.05    0.09
#> Annualized Sharpe Ratio    0.38    0.50
#> Win %                      0.53    0.53
#> Annualized Volatility      0.13    0.18
#> Maximum Drawdown          -0.37   -0.46
#> Max Length Drawdown     2254.00 2248.00

As you can see, this strategy tends to reduce drawdowns, but it also sometimes reduces overall returns. In some cases, you could leverage up the strategy, which would increase both returns and drawdowns, but that’s the subject of another post.

stay in touch