Backtesting with Short positions

[This article was first published on Systematic Investor » R, 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.

I want to illustrate Backtesting with Short positions using an interesting strategy introduced by Woodshedder in the Simple, Long-Term Indicator Near to Giving Short Signal post. This strategy was also analyzed in details by MarketSci in Woodshedder’s Long-Term Indicator post.

The strategy uses the 5 day rate of change (ROC5) and the 252 day rate of change (ROC252):

  • Buy (or cover short) at the close if yesterday the ROC252 crossed above the ROC5 and today the ROC252 is still above the ROC5.
  • Sell (or open short) at the close if yesterday the ROC5 crossed above the ROC252 and today the ROC5 is still above the ROC252.

Following is a sample code to implement this strategy using the backtesting library in the Systematic Investor Toolbox:

# Load Systematic Investor Toolbox (SIT)
setInternet2(TRUE)
con = gzcon(url('https://github.com/systematicinvestor/SIT/raw/master/sit.gz', 'rb'))
	source(con)
close(con)

	#*****************************************************************
	# Load historical data
	#******************************************************************
	load.packages('quantmod')
	tickers = spl('SPY')

	data 	getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T)
	bt.prep(data, align='keep.all', dates='1970::2011')

	#*****************************************************************
	# Code Strategies
	#******************************************************************
	prices = data$prices

	# Buy & Hold
	data$weight[] = 1
	buy.hold = bt.run(data)

	# ROC Strategy
	roc5 = prices / mlag(prices,5)
	roc252 = prices / mlag(prices,252)
		roc5.1 = mlag(roc5,1)
		roc5.2 = mlag(roc5,2)
		roc252.1 = mlag(roc252,1)
		roc252.2 = mlag(roc252,2)

	data$weight[] = NA
		data$weight$SPY[] = iif(roc252.2 < roc5.2 & roc252.1 > roc5.1 & roc252 > roc5, 1, data$weight$SPY)
		data$weight$SPY[] = iif(roc252.2 > roc5.2 & roc252.1 < roc5.1 & roc252 < roc5, -1, data$weight$SPY)
	roc.cross = bt.run(data, trade.summary=T)

	#*****************************************************************
	# Create Report
	#******************************************************************
	plotbt.custom.report(roc.cross, buy.hold, trade.summary=T)

A quick comparison between the equity curve of the ROC strategy and the equity curve shown by Woodshedder reveals a significant discrepancy. The ROC strategy’s equity curve peaked in 2008-2009, while the equity curve shown by Woodshedder peaked in 2011. I re-created this strategy in Amibroker and got similar results to the ones reported by Woodshedder. So what is wrong?

The problem lies in a way the backtest is created, I used weights, not shares to create the backtest. Let’s compare the backtest performance using weights or shares of the long-only strategy when prices rise 10% each period:

P0 P1 P2
100 110 121
R1 R2
10% 10%

The total return using one share, [(one share) * P2] / 100 – 1 = 21% and the total return using weights, (1+R1)(1+R2) – 1 = 21%, are identical.

Consider the performance using weights or shares of the short-only strategy when prices fall 10% each period:

P0 P1 P2
100 90 81
R1 R2
-10% -10%

The total return using one share, [200 - (one share) * P2] / 100 – 1 = 19% and the total return using weights, (1+R1)(1+R2) – 1 = 21%, are different.

The difference arises because in period 1, prices have dropped 10% and hence we have $10 additional dollars to invest. So if the proceeds are reinvested, the portfolio value in period 0 is $100, in period 1 is $110 = 1.1 * $100, in period 2 is $121 = 1.1 * $110, and the total return is $121 / $100 – 1 = 21%.

Conclusion, if Backtesting with Short positions we cannot use weights * return to compute portfolio return because it assumes that all mark-to-market gains are reinvested right away, instead we should use shares to create the backtest.

Here is the code that implements shares backtest (type=’share’ in bt.run function) :

	#*****************************************************************
	# Code Strategies
	#****************************************************************** 	
	# If Backtesting with Short positions, always use type = 'share' backtest to get realistic results.
	data$weight[] = NA
		data$weight$SPY[] = iif(roc252.2 < roc5.2 & roc252.1 > roc5.1 & roc252 > roc5, 1, data$weight$SPY)
		data$weight$SPY[] = iif(roc252.2 > roc5.2 & roc252.1 < roc5.1 & roc252 < roc5, -1, data$weight$SPY)	

		capital = 100000
		data$weight[] = (capital / prices) * bt.exrem(data$weight)
	roc.cross.share = bt.run(data, type='share', trade.summary=T)							

	#*****************************************************************
	# Create Report
	#******************************************************************
	plotbt.custom.report(roc.cross.share, roc.cross, buy.hold, trade.summary=T)

Finally, the equity curve of the ROC strategy using type=’share’ in backtest is very similar to one reported by Woodshedder.

To view the complete source code for this example, please have a look at the bt.roc.cross.test() function in bt.test.r at github.


To leave a comment for the author, please follow the link and comment on their blog: Systematic Investor » R.

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)