## Simple and Profitable

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

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:

- A Quantitative Approach to Tactical Asset Allocation by M. Faber (2006)
- Tactical Asset Allocation by MarketSci
- Automatic 7 by E. Mamula
- Decision Moose by W.Dirlam
- Simple Sector ETF Momentum Strategy Performance

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:

- Consider a variety of ranking methods. I.e. 1/2/3/6/12 month returns and their combinations, risk-adjusted ranking.
- To control drawdowns and increase performance consider the timing mechanism as presented in A Quantitative Approach to Tactical Asset Allocation by M. Faber (2006).
- Consider a different asset universe. Include ETFs that are less correlated to the other assets, like Commodities, Fixed Income, and International Equity Markets. For example, have a look at the Single Country International Strategy post.

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.