Moving Average
22 Feb 2015To install Systematic Investor Toolbox (SIT) please visit About page.
The Quantitative Approach To Tactical Asset Allocation Strategy(QATAA) by Mebane T. Faber model is using 10 month moving average as a filter to switch strategy to cash. If at the month end the asset’s price is above the moving average, it gets allocation; otherwise it’s allocation goes to cash.
The initial question i wanted to answer is what so special about 10 months; and why does 10 is constant for all assets and regimes. I played with idea of adjusting moving average look back based on the historical volatility. I.e. in periods of high volatility, the shorter moving average will take us from the market faster, while in low volatility, the longer moving average will keep us in the markets regardless of the whipsaws. Unfortunately, it resulted in higher turnover and worse results.
The good part is that i spend some time analyzing the base 10 month moving average strategy and seeing quite whipsaw, the simple fix is to use +/- 5% bands around 10 month moving average to reduce whipsaw, reduce turnover and increase returns.
Below I will show how this concept works:
#*****************************************************************
# Load historical data
#*****************************************************************
library(SIT)
load.packages('quantmod')
# load saved Proxies Raw Data, data.proxy.raw, to extend DBC and SHY
# please see http://systematicinvestor.github.io/Data-Proxy/ for more details
load('data/data.proxy.raw.Rdata')
tickers = '
SPY
CASH = SHY + TB3Y
'
data <- new.env()
getSymbols.extra(tickers, src = 'yahoo', from = '1970-01-01', env = data, raw.data = data.proxy.raw, set.symbolnames = T, auto.assign = T)
for(i in data$symbolnames) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T)
bt.prep(data, align='remove.na', fill.gaps = T)
#*****************************************************************
# helper function to visualize signal
#*****************************************************************
cash.visualize.signal = function(model = spl('base,bands'), dates='::') {
model = model[1]
signal = iif(model == 'base', prices > sma,
iif(cross.up(prices, sma * 1.05), 1, iif(cross.dn(prices, sma * 0.95), 0, NA))
)$SPY
signal[] = ifna( ifna.prev(signal), 0)
# create a model based on signal
data$weight[] = NA
data$weight$SPY = signal
data$weight$CASH = 1 - ifna( ifna.prev(data$weight$SPY), 0)
model = bt.run.share(data, clean.signal=T, silent=T)
# create a plot to visualize signal
p = prices$SPY
p1 = sma$SPY
e = model$equity
w = model$weight$SPY
highlight = (signal==1)[dates]
layout(1:4)
plota(p[dates] ,type='l', plotX=F, x.highlight = highlight)
plota.lines(p1 ,type='l', col='blue')
if(model == 'base')
plota.legend('SPY,sma','black,blue')
else {
plota.lines(1.05*p1 ,type='l', col='green')
plota.lines(0.95*p1 ,type='l', col='green')
plota.legend('SPY,sma,sma bands','black,blue,green')
}
plota( 100 * EMA(compute.drawdown(p),20)[dates] ,type='l', plotX=F, x.highlight = highlight)
plota.lines( 100 * EMA(compute.drawdown(e),20)[dates] ,col='blue')
plota.legend('SPY drawdown,Model drawdown', 'black, blue')
plota( 100 * EMA(p/mlag(p,252)[dates]-1,20) ,type='l', x.highlight = highlight, plotX=F)
abline(h=0, col='red')
plota.lines( 100 * EMA(e/mlag(e,252)[dates]-1,20) ,col='blue')
plota.legend('SPY 12M return, Model 12M return','black,blue')
plota(w[dates] ,type='s', x.highlight = highlight)
plota.legend('weight')
#iline(remove.col='green')
}
prices = data$prices
sma = bt.apply.matrix(prices, SMA, 10*22)
cash.visualize.signal('base', '2000::2001')
cash.visualize.signal('bands', '2000::2001')
The benefit of delay entry / exit is less trades, smaller turnover and hopefully better returns if whipsaws outweigh decrease in performance due to delay’s
#*****************************************************************
# Setup
#*****************************************************************
prices = data$prices
models = list()
#*****************************************************************
# SPY
#******************************************************************
data$weight[] = NA
data$weight$SPY = 1
models$SPY = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
#*****************************************************************
# SPY + 10 month go to cash filter
#******************************************************************
sma = bt.apply.matrix(prices, SMA, 10*22)
data$weight[] = NA
data$weight$SPY = iif(prices$SPY > sma$SPY, 1, 0)
data$weight$CASH = 1 - ifna( ifna.prev(data$weight$SPY), 0)
models$SPY.CASH = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
#*****************************************************************
# SPY + 10 month +5/-5% go to cash filter
#******************************************************************
data$weight[] = NA
data$weight$SPY = iif(cross.up(prices, sma * 1.05), 1, iif(cross.dn(prices, sma * 0.95), 0, NA))$SPY
data$weight$CASH = 1 - ifna( ifna.prev(data$weight$SPY), 0)
models$SPY.CASH.BAND = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
I also included my attempts at dynamic look back moving average, but in this form it is not useful.
#*****************************************************************
# SPY + dynamic cash filter base on volatility
#******************************************************************
ret = diff(log(prices))
hist.vol = bt.apply.matrix(ret, runSD, n = 21)
vol.rank = bt.apply.matrix(hist.vol, percent.rank, 252)
sma.cash = sma * NA
sma.cash[] = iif(vol.rank < 0.5, bt.apply.matrix(prices, SMA, 10*22), bt.apply.matrix(prices, SMA, 1*22))
data$weight[] = NA
data$weight$SPY = iif(prices$SPY <= sma$SPY | prices$SPY <= sma.cash$SPY, 0, iif(prices$SPY > sma$SPY, 1, NA))
data$weight$CASH = 1 - ifna( ifna.prev(data$weight$SPY), 0)
models$SPY.CASH.VOL.SIMPLE = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
#*****************************************************************
# SPY + dynamic cash filter base on volatility; multiple levels
#******************************************************************
nbreaks = 5
map.index = seq(0,1, 1/nbreaks)
map = bt.apply.matrix(vol.rank, function(x) as.numeric(cut(x, map.index)))
sma.cash = sma * NA
for(i in 1:nbreaks) {
temp = coredata(bt.apply.matrix(prices, SMA, (nbreaks - i + 1)* 2 *22))
index = ifna(map == i, F)
sma.cash[index] = temp[index]
}
data$weight[] = NA
data$weight$SPY = iif(prices$SPY <= sma$SPY | prices$SPY <= sma.cash$SPY, 0, iif(prices$SPY > sma$SPY, 1, NA))
data$weight$CASH = 1 - ifna( ifna.prev(data$weight$SPY), 0)
models$SPY.CASH.VOL = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
#*****************************************************************
# Report
#*****************************************************************
#strategy.performance.snapshoot(models, T)
plotbt(models, plotX = T, log = 'y', LeftMargin = 3, main = NULL)
mtext('Cumulative Performance', side = 2, line = 1)
print(plotbt.strategy.sidebyside(models, make.plot=F, return.table=T,perfromance.fn = engineering.returns.kpi))
SPY | SPY.CASH | SPY.CASH.BAND | SPY.CASH.VOL.SIMPLE | SPY.CASH.VOL | |
---|---|---|---|---|---|
Period | Jan1993 - Feb2015 | Jan1993 - Feb2015 | Jan1993 - Feb2015 | Jan1993 - Feb2015 | Jan1993 - Feb2015 |
Cagr | 9.4 | 9.9 | 12.1 | 9.2 | 8 |
DVR | 41.9 | 78.3 | 91.4 | 83.8 | 74 |
Sharpe | 56.7 | 83.6 | 97.1 | 90.8 | 77.1 |
R2 | 73.9 | 93.7 | 94.1 | 92.3 | 96 |
Win.Percent | 100 | 41.1 | 100 | 45.7 | 43.3 |
Avg.Trade | 623.7 | 1.9 | 27.6 | 0.7 | 0.7 |
MaxDD | -55.2 | -20.1 | -19.1 | -15.9 | -22.3 |
Num.Trades | 1 | 146 | 12 | 302 | 254 |
layout(1)
barplot.with.labels(sapply(models, compute.turnover, data), 'Average Annual Portfolio Turnover')
Next, let’s apply same bands logic to the TAA model:
tickers = '
US.STOCKS = VTI + VTSMX
FOREIGN.STOCKS = VEU + FDIVX
US.10YR.GOV.BOND = IEF + VFITX
REAL.ESTATE = VNQ + VGSIX
COMMODITIES = DBC + CRB
CASH = BND + VBMFX,
SP500 = SPY
'
# load saved Proxies Raw Data, data.proxy.raw
load('data/data.proxy.raw.Rdata')
data <- new.env()
getSymbols.extra(tickers, src = 'yahoo', from = '1970-01-01', env = data, raw.data = data.proxy.raw, auto.assign = T, set.symbolnames = T)
for(i in data$symbolnames) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T)
bt.prep(data, align='remove.na')
#*****************************************************************
# Setup
#*****************************************************************
data$universe = data$prices > 0
# do not allocate to CASH
data$universe$CASH = NA
data$universe$SP500 = NA
prices = data$prices * data$universe
n = ncol(prices)
period.ends = endpoints(prices, 'months')
period.ends = period.ends[period.ends > 0]
models = list()
#*****************************************************************
# Benchmarks
#*****************************************************************
data$weight[] = NA
data$weight$SP500 = 1
models$SP500 = bt.run.share(data, clean.signal=T, trade.summary=T, silent=T)
data$weight[] = NA
data$weight[period.ends,] = ntop(prices[period.ends,], n)
models$EW = bt.run.share(data, clean.signal=F, trade.summary=T, silent=T)
#*****************************************************************
#The [Quantitative Approach To Tactical Asset Allocation Strategy(QATAA) by Mebane T. Faber](http://mebfaber.com/timing-model/)
#[SSRN paper](http://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461)
#*****************************************************************
sma = bt.apply.matrix(prices, SMA, 10*22)
weight = NA * data$weight
weight = iif(prices > sma, 20/100, 0)
weight$CASH = 1 - rowSums(weight)
data$weight[] = NA
data$weight[period.ends,] = weight[period.ends,]
models$Model = bt.run.share(data, clean.signal=F, trade.summary=T, silent=T)
#*****************************************************************
# Alternative: MA bands
#*****************************************************************
sma = bt.apply.matrix(prices, SMA, 10*22)
signal = iif(cross.up(prices, sma * 1.05), 1, iif(cross.dn(prices, sma * 0.95), 0, NA))
signal = ifna(bt.apply.matrix(signal, ifna.prev),0)
weight = iif(signal == 1, 20/100, 0)
weight$CASH = 1 - rowSums(weight)
data$weight[] = NA
data$weight[period.ends,] = weight[period.ends,]
models$Model.B = bt.run.share(data, clean.signal=F, trade.summary=T, silent=T)
#*****************************************************************
# Report
#*****************************************************************
#strategy.performance.snapshoot(models, T)
plotbt(models, plotX = T, log = 'y', LeftMargin = 3, main = NULL)
mtext('Cumulative Performance', side = 2, line = 1)
print(plotbt.strategy.sidebyside(models, make.plot=F, return.table=T,perfromance.fn = engineering.returns.kpi))
SP500 | EW | Model | Model.B | |
---|---|---|---|---|
Period | Jun1996 - Feb2015 | Jun1996 - Feb2015 | Jun1996 - Feb2015 | Jun1996 - Feb2015 |
Cagr | 8.2 | 8.6 | 9.8 | 10.6 |
DVR | 28.7 | 64 | 117.4 | 127.9 |
Sharpe | 49.2 | 69.3 | 120.4 | 132.7 |
R2 | 58.4 | 92.4 | 97.5 | 96.5 |
Win.Percent | 100 | 59.9 | 64.4 | 64.6 |
Avg.Trade | 335.7 | 0.1 | 0.2 | 0.2 |
MaxDD | -55.2 | -47.5 | -17.1 | -13.1 |
Num.Trades | 1 | 1113 | 930 | 887 |
layout(1)
barplot.with.labels(sapply(models, compute.turnover, data), 'Average Annual Portfolio Turnover')
The bands logic is easy to implement and it reduced turnover and increase returns.
For other creative ideas on turnover reduction please read Rotational Trading Strategies: borrowing ideas from Engineering Returns post.
(this report was produced on: 2015-02-23)