Backtesting a Simple Stock Trading Strategy

[This article was first published on Modern Toolmaking, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

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.  


I recently read a post on ETF Prophet that explored an interesting stock trading strategy in Excel.  The strategy is simple: Find the high point of the stock over the last 200 days, and count the number of days that have elapsed since that high.  If its been more less than 100 days, own the stock.  If it’s been more than 100 days, don’t own it.  This strategy is very simple, but it yields some impressive results. (Note; however, that this example uses data that has not been adjusted from splits or dividends and could contain other errors.  Furthermore, we’re ignoring trading costs and execution delays, both of which affect strategy performance.)

Implementing this strategy in R is simple, and provides numerous advantages over excel, the primary of which is that pulling stock market data into R is easy, and we can test this strategy on a wide range of indexes with relatively little effort.

First of all, we download data for GSPC using quantmod. (GSPC stands for the S&P 500 index). Next, we construct a function to calculate the number of days since the n-day high in a time series, and a function to implement our trading strategy.  The latter function takes 2 parameters: the n-day high you wish to use, and the numbers of days past that high you will hold the stock.  The example is 200 and 100, but you could easily change this to the 500-day high and see what happens if you hold the stock 300 days past that before bailing out.  Since this function is parameterized, we can easily test many other versions of our strategy.  We pad the beginning of our strategy with zeros so it will be the same length as our input data. (If you wish for a more detailed explaination of the daysSinceHigh function, see the discussion on cross-validated).

rm(list = ls(all = TRUE))
#http://etfprophet.com/days-since-200-day-highs/
require(quantmod)
getSymbols('^GSPC',from='1900-01-01')
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)
}
myStock <- Cl(GSPC)
myPosition <- myStrat(myStock,100,200)
bmkReturns <- dailyReturn(myStock, type = "arithmetic")
myReturns <- bmkReturns*Lag(myPosition,1)
myReturns[1] <- 0
names(bmkReturns) <- 'SP500'
names(myReturns) <- 'Me'

We multiply our position (0,1) vector by the returns from the index to get our strategy’s returns.  Now we construct a function to return some statistics about a trading strategy, and compare our strategy to the benchmark.  Somewhat arbitrarily, I’ve decided to look at cumulative return, mean annual return, sharpe ratio, winning %, mean annual volatility, max drawdown, and max length drawdown.  Other stats would be easy to implement.


require(PerformanceAnalytics)
charts.PerformanceSummary(cbind(bmkReturns,myReturns))
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)
}
cbind(Me=Performance(myReturns),SP500=Performance(bmkReturns))

Results:
Me SP500
Cumulative Return 77.03146070 71.15426170
Annual Return 0.07326067 0.07189781
Annualized Sharpe Ratio 0.63648679 0.46535064
Win % 0.54484242 0.53242454
Annualized Volatility 0.11510163 0.15450243
Maximum Drawdown -0.33509517 -0.56775389
Max Length Drawdown 1553.00000000 1898.00000000


As you can see, this strategy compares favorably to the default “buy-and-hold” approach.

Finally, we test our strategy on 3 other indexes: FTSE which represents Ireland and the UK, the Dow Jones Industrial Index, which goes back to 1896, and the N225, which represents Japan.  I’ve functionalized the entire process, so you can test each new strategy with 1 line of code:

testStrategy <- function(myStock, nHold=100, nHigh=200) {
myPosition <- myStrat(myStock,nHold,nHigh)
bmkReturns <- dailyReturn(myStock, type = "arithmetic")
myReturns <- bmkReturns*Lag(myPosition,1)
myReturns[1] <- 0
names(bmkReturns) <- 'Index'
names(myReturns) <- 'Me'
charts.PerformanceSummary(cbind(bmkReturns,myReturns))
cbind(Me=Performance(myReturns),Index=Performance(bmkReturns))
}
getSymbols('^FTSE',from='1900-01-01')
getSymbols('DJIA', src='FRED')
getSymbols('^N225',from='1900-01-01')
testStrategy(Cl(FTSE),100,200)
testStrategy(na.omit(DJIA),100,200)
round(testStrategy(Cl(N225),100,200),8)

Results:
FTSE:
Me Index
Cumulative Return 3.56248582 3.8404476
Annual Return 0.05667121 0.0589431
Annualized Sharpe Ratio 0.45907768 0.3298633
Win % 0.53216374 0.5239884
Annualized Volatility 0.12344579 0.1786895
Maximum Drawdown -0.39653398 -0.5256991
Max Length Drawdown 1633.00000000 2960.0000000
DJIA:
Me Index
Cumulative Return 364.53412355 277.66780655
Annual Return 0.05282208 0.05033327
Annualized Sharpe Ratio 0.40688871 0.27453532
Win % 0.53748349 0.52523599
Annualized Volatility 0.12981947 0.18333987
Maximum Drawdown -0.53822732 -0.89185928
Max Length Drawdown 3847.00000000 6301.00000000
N225:
Me Index
Cumulative Return 0.98108503 -0.12146268
Annual Return 0.02560154 -0.00477699
Annualized Sharpe Ratio 0.17677957 -0.02053661
Win % 0.52871073 0.51337842
Annualized Volatility 0.14482181 0.23260863
Maximum Drawdown -0.49361328 -0.81871261
Max Length Drawdown 5342.00000000 5342.00000000

FTSE:

DJIA:

N225:

The strategy performs well on the other indexes.  It even manages a positive return on the N225!

Feel free to test this strategy with other parameters, or on other indexes.  For homework, think of possible ways that I have fooled myself in this backtest, and post them in the comments.  One example of this is that we have’t looked at transaction costs, which might be significant…


To leave a comment for the author, please follow the link and comment on their blog: Modern Toolmaking.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)