Archive

Archive for December, 2011

Simple and Profitable

December 9, 2011 6 comments

The end of the month effect was examined by MarketSci in the The Last Day of the Month Blahs post. The idea is simple: buy on the last day of the month and sell a few days later. This idea was put into a strategy by Quanting Dutchman in the Strategy 2 – Monthly End-of-the-Month (MEOM) post.

I will follow the outline of the Quanting Dutchman’s strategy and will implement it using the backtesting library in the Systematic Investor Toolbox.

The strategy invests into the top 2 ETFs that are trading above a medium term moving avarage (WMA89) from the universe of 26 ETF’s: DIA, EEM, EFA, EWH, EWJ, EWT, EWZ, FXI, GLD, GSG, IEF, ILF, IWM, IYR, QQQ, SPY, VNQ, XLB, XLE, XLF, XLI, XLP, XLU, XLV, XLY, XLK. The strategy enters positions on the last day of the month at the close. The strategy exits positions two days later at the close. I will study two ranking schemes to select top two ETFs each month:

  • Rank1 = MA( C/Ref(C,-2), 5 ) * MA( C/Ref(C,-2), 40 )
  • Rank2 = MA( C/Ref(C,-2), 5 ) * Ref( MA( C/Ref(C,-2), 10 ), -5 )

Following code implements 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('DIA,EEM,EFA,EWH,EWJ,EWT,EWZ,FXI,GLD,GSG,IEF,ILF,IWM,IYR,QQQ,SPY,VNQ,XLB,XLE,XLF,XLI,XLP,XLU,XLV,XLY,XLK')	

	data <- new.env()
	getSymbols(tickers, src = 'yahoo', from = '1995-01-01', env = data, auto.assign = T)
		for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T)	
	bt.prep(data, align='keep.all', dates='1995::2011')
	
	#*****************************************************************
	# Code Strategies
	#****************************************************************** 
	prices = data$prices   
	n = ncol(prices)
	nperiods = nrow(prices)

	# Equal Weight
	data$weight[] = ntop(prices, n)
	equal.weight = bt.run(data)	
	
		
	# find month ends
	month.ends = endpoints(prices, 'months')
		month.ends = month.ends[month.ends > 0]		
	month.ends2 = iif(month.ends + 2 > nperiods, nperiods, month.ends + 2)
								   
	# Strategy MEOM - Equal Weight
	data$weight[] = NA
		data$weight[month.ends,] = ntop(prices, n)[month.ends,]	
		data$weight[month.ends2,] = 0
		
		capital = 100000
		data$weight[] = (capital / prices) * data$weight
	meom.equal.weight = bt.run(data, type='share')

	#*****************************************************************
	# Rank1 = MA( C/Ref(C,-2), 5 ) * MA( C/Ref(C,-2), 40 )
	#****************************************************************** 			
	# BuyRule = C > WMA(C, 89)
	buy.rule = prices > bt.apply.matrix(prices, function(x) { WMA(x, 89) } )		
		buy.rule = ifna(buy.rule, F)
		
	# 2-day returns
	ret2 = ifna(prices / mlag(prices, 2), 0)
	
	# Rank1 = MA( C/Ref(C,-2), 5 ) * MA( C/Ref(C,-2), 40 )
	position.score = bt.apply.matrix(ret2, SMA, 5) * bt.apply.matrix(ret2, SMA, 40)
		position.score[!buy.rule,] = NA
			
	# Strategy MEOM - top 2    
	data$weight[] = NA;
		data$weight[month.ends,] = ntop(position.score[month.ends,], 2)		
		data$weight[month.ends2,] = 0		
		
		capital = 100000
		data$weight[] = (capital / prices) * data$weight
	meom.top2.rank1 = bt.run(data, type='share', trade.summary=T)
	
	#*****************************************************************
	# Rank2 = MA( C/Ref(C,-2), 5 ) * Ref( MA( C/Ref(C,-2), 10 ), -5 )
	#****************************************************************** 		
	position.score = bt.apply.matrix(ret2, SMA, 5) * mlag( bt.apply.matrix(ret2, SMA, 10), 5)
		position.score[!buy.rule,] = NA
	
	# Strategy MEOM - top 2    
	data$weight[] = NA;
		data$weight[month.ends,] = ntop(position.score[month.ends,], 2)		
		data$weight[month.ends2,] = 0		
		
		capital = 100000
		data$weight[] = (capital / prices) * data$weight
	meom.top2.rank2 = bt.run(data, type='share', trade.summary=T)	
	
	#*****************************************************************
	# Create Report
	#****************************************************************** 
	plotbt.custom.report(meom.top2.rank2, meom.top2.rank1, meom.equal.weight, equal.weight, trade.summary=T)

This a great little strategy that is only invested 10% of time. You can improve returns by investing in a fixed income for the rest of the time. Quanting Dutchman tested this idea in My thoughts on “Go-In-Cash” post.

I also found promising results by using current Dow Jones Components instead of ETFs. To run the End of the Month strategy on current Dow Jones Components substitute:

tickers = spl('DIA,EEM,EFA,EWH,EWJ,EWT,EWZ,FXI,GLD,GSG,IEF,ILF,IWM,IYR,QQQ,SPY,VNQ,XLB,XLE,XLF,XLI,XLP,XLU,XLV,XLY,XLK')

with

tickers = dow.jones.components()

in the above code. Here are the summary stats for the End of the Month strategy using current Dow Jones Components.

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

Multi-Asset Backtest : Rotational Trading Strategies

December 6, 2011 4 comments

I want to discuss the implementation of Rotational Trading Strategies using the backtesting library in the Systematic Investor Toolbox.The Rotational Trading strategy switches investment allocations throughout the time, betting on few top ranked assets. For example, the ranking can be based on relative strength or momentum. A few examples of the Rotational Trading Strategies (or Tactical Asset Allocation) are:

I want to illustrate the Rotational Trading using the strategy introduced at ETF Screen in the ETF Sector Strategy post. Each month, this strategy invests into the top two of the 21 ETFs sorted by their 6 month returns. To reduce the turnover, in subsequent months the ETF positions are kept as long as these ETFs are in the top 6 rank.

Before we can implement this strategy, we need to create two helper routines. First, let’s create a function that will select the top N positions for each period:

############################################################################### 
# Select top N for each period
############################################################################### 
ntop <- function
(
	data,		# matrix with observations
	topn = 1, 	# top n
	dirMaxMin = TRUE
) 
{
	out = data
	out[] = NA
	
	for( i in 1:nrow(data) ) {
		x = coredata(data[i,])
		o = sort.list(x, na.last = TRUE, decreasing = dirMaxMin)
		index = which(!is.na(x))
		x[] = NA
		
		if(len(index)>0) {
			n = min(topn, len(index))
			x[o[1:n]] = 1/n
		}
		out[i,] = x
	}
	out[is.na(out)] = 0	
	return( out )
}

Next, let’s create a function that will select the top N positions for each period and keep them until they drop below KeepN rank:

############################################################################### 
# Select top N for each period, and keep them till they drop below keepn rank
############################################################################### 
ntop.keep <- function
(
	data, 		# matrix with observations
	topn = 1, 	# top n
	keepn = 1, 	# keep n
	dirMaxMin = TRUE
) 
{
	out = data
	out[] = NA
	
	for( i in 1:nrow(data) ) {
		x = coredata(data[i,])
		o = sort.list(x, na.last = TRUE, decreasing = dirMaxMin)
		index = which(!is.na(x))
		x[] = NA
			
		if(len(index)>0) {
			n = min(topn, len(index))
			x[o[1:n]] = 1
		
			# keepn logic
			if( i>=2 ) {
				y = coredata(out[(i-1),])	# previous period selection
				n1 = min(keepn, len(index))
				y[-o[1:n1]] = NA	# remove all not in top keepn
				
				index1 = which(!is.na(y))
				if(len(index1)>0) {
					x[] = NA
					x[index1] = 1	# keep old selection
					
					for( j in 1:n ) {
						if( sum(x, na.rm = T) == topn ) break
						x[o[j]] = 1
					}
				}
			}
		}		
		out[i,] = x/sum(x, na.rm = T)	
	}
	out[is.na(out)] = 0	
	return( out )
}

Now we are ready 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('XLY,XLP,XLE,XLF,XLV,XLI,XLB,XLK,XLU,IWB,IWD,IWF,IWM,IWN,IWO,IWP,IWR,IWS,IWV,IWW,IWZ')	

	data <- new.env()
	getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T)
		for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T)
	bt.prep(data, align='keep.all', dates='1970::2011')

	#*****************************************************************
	# Code Strategies
	#****************************************************************** 
	prices = data$prices  
	n = len(tickers)  

	# find month ends
	month.ends = endpoints(prices, 'months')
		month.ends = month.ends[month.ends > 0]		
		
	# Equal Weight
	data$weight[] = NA
		data$weight[month.ends,] = ntop(prices, n)[month.ends,]	
		capital = 100000
		data$weight[] = (capital / prices) * data$weight
	equal.weight = bt.run(data, type='share')
		
			
	# Rank on 6 month return
	position.score = prices / mlag(prices, 126)	
	
	# Select Top 2 funds
	data$weight[] = NA
		data$weight[month.ends,] = ntop(position.score[month.ends,], 2)	
		capital = 100000
		data$weight[] = (capital / prices) * bt.exrem(data$weight)		
	top2 = bt.run(data, type='share', trade.summary=T)

	# Seletop Top 2 funds,  and Keep then till they are in 1:6 rank
	data$weight[] = NA
		data$weight[month.ends,] = ntop.keep(position.score[month.ends,], 2, 6)	
		capital = 100000
		data$weight[] = (capital / prices) * bt.exrem(data$weight)		
	top2.keep6 = bt.run(data, type='share', trade.summary=T)
	
	#*****************************************************************
	# Create Report
	#****************************************************************** 
	plotbt.custom.report(top2.keep6, top2, equal.weight, trade.summary=T)

There are many ways to improve this strategy. Here is a sample list of additional ways to consider:

The only boundary is your imagination. I would also recommend to do sensitivity analysis during your strategy development to make sure your are not overfitting the data.

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

For more examples, please have a look at the implementation of the Timing Model as presented in A Quantitative Approach to Tactical Asset Allocation by M. Faber (2006) at the bt.timing.model.test() function in bt.test.r at github.

Backtesting with Short positions

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.