Factor Attribution to improve performance of the 1-Month Reversal Strategy
Today I want to show how to use Factor Attribution to boost performance of the 1-Month Reversal Strategy. The Short-Term Residual Reversal by D. Blitz, J. Huij, S. Lansdorp, M. Verbeek (2011) paper presents the idea and discusses the results as applied to US stock market since 1929. To improve 1-Month Reversal Strategy performance authors investigate an alternative position ranking metric based on the residuals from the rolling Factor Attribution regression.
The base 1-Month Reversal Strategy forms portfolios each month by buying 20% of loosers and short selling 20% of winners from the S&P 500 index based on the prior 1-Month returns. The 1-Month Residual Reversal Strategy forms portfolios each month by buying 20% of loosers and short selling 20% of winners from the S&P 500 index based on the residuals from the rolling Factor Attribution regression. Following are the steps to form 1-Month Residual Reversal Strategy portfolio each month:
- 1. for each company in the S&P 500 index, run a rolling Factor Attribution regression on the prior 36 months and compute residuals: e.1, e.2, …, e.t, …, e.T for t in 1:36
- 2. alternative position ranking metric = e.T / standard deviation of (e)
- 3. form portfolios by buying 20% of loosers and short selling 20% of winners from the S&P 500 index based on the alternative position ranking metric
Let’s start by loading historical prices for all companies in the S&P 500 and create SPY and Equal Weight benchmarks using the Systematic Investor Toolbox:
############################################################################### # Load Systematic Investor Toolbox (SIT) # https://systematicinvestor.wordpress.com/systematic-investor-toolbox/ ############################################################################### setInternet2(TRUE) con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb')) source(con) close(con) #***************************************************************** # Load historical data #****************************************************************** load.packages('quantmod') tickers = sp500.components()$tickers data <- new.env() getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T) # remove companies with less than 5 years of data rm.index = which( sapply(ls(data), function(x) nrow(data[[x]])) < 1000 ) rm(list=names(rm.index), envir=data) for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T) bt.prep(data, align='keep.all', dates='1994::') tickers = data$symbolnames data.spy <- new.env() getSymbols('SPY', src = 'yahoo', from = '1970-01-01', env = data.spy, auto.assign = T) bt.prep(data.spy, align='keep.all', dates='1994::') #***************************************************************** # Code Strategies #****************************************************************** prices = data$prices n = ncol(prices) #***************************************************************** # Setup monthly periods #****************************************************************** periodicity = 'months' period.ends = endpoints(data$prices, periodicity) period.ends = period.ends[period.ends > 0] prices = prices[period.ends, ] #***************************************************************** # Create Benchmarks, omit results for the first 36 months - to be consistent with Factor Attribution #****************************************************************** models = list() # SPY data.spy$weight[] = NA data.spy$weight[] = 1 data.spy$weight[1:period.ends[36],] = NA models$spy = bt.run(data.spy) # Equal Weight data$weight[] = NA data$weight[period.ends,] = ntop(prices, n) data$weight[1:period.ends[36],] = NA models$equal.weight = bt.run(data)
Next let’s run Factor Attribution on each the stocks in the S&P 500 index to determine it’s alternative position ranking metric. I will save both e.T and e.T / standard deviation of (e) metrics.
# function to compute additional custom stats for factor.rolling.regression factor.rolling.regression.custom.stats <- function(x,y,fit) { n = len(y) e = y - x %*% fit$coefficients se = sd(e) return(c(e[n], e[n]/se)) } #***************************************************************** # Load factors and align them with prices #****************************************************************** # load Fama/French factors factors = get.fama.french.data('F-F_Research_Data_Factors', periodicity = periodicity,download = T, clean = F) # align monthly dates map = match(format(index(factors$data), '%Y%m'), format(index(prices), '%Y%m')) dates = index(factors$data) dates[!is.na(map)] = index(prices)[na.omit(map)] index(factors$data) = as.Date(dates) # add factors and align data.fa <- new.env() for(i in tickers) data.fa[[i]] = data[[i]][period.ends, ] data.fa$factors = factors$data / 100 bt.prep(data.fa, align='remove.na') index = match( index(data.fa$prices), index(data$prices) ) prices = data$prices[index, ] #***************************************************************** # Compute Factor Attribution for each ticker #****************************************************************** temp = NA * prices factors = list() factors$last.e = temp factors$last.e_s = temp for(i in tickers) { cat(i, '\n') # Facto Loadings Regression obj = factor.rolling.regression(data.fa, i, 36, silent=T, factor.rolling.regression.custom.stats) for(j in 1:len(factors)) factors[[j]][,i] = obj$fl$custom[,j] } # add base strategy factors$one.month = coredata(prices / mlag(prices))
Next let’s group stocks into Quantiles based on 1-Month Reversal factors and create reports.
#***************************************************************** # Create Quantiles #****************************************************************** quantiles = list() for(name in names(factors)) { cat(name, '\n') quantiles[[name]] = bt.make.quintiles(factors[[name]], data, index, start.t = 1+36, prefix=paste(name,'_',sep='')) } #***************************************************************** # Create Report #****************************************************************** plotbt.custom.report.part1(quantiles$one.month$spread,quantiles$last.e$spread,quantiles$last.e_s$spread) plotbt.strategy.sidebyside(quantiles$one.month$spread,quantiles$last.e$spread,quantiles$last.e_s$spread) plotbt.custom.report.part1(quantiles$last.e_s)
The 1-Month Residual Reversal Strategy have done well over the last 10 years and handsomely outperformed the base 1-Month Reversal Strategy.
To view the complete source code for this example, please have a look at the bt.fa.one.month.test() function in bt.test.r at github.
Thank you soo much for sharing…Great post.
I was wondering how you adjust for survival bias in your setup. Can you get S&P membership of stocks historically?
I’m not aware of any public source for historical S&P constituents and historical prices for delisted companies.
I went through some of the short-term reversal literature in the past and this post is very interesting.
Several papers consider also 1. intra-industry effect (basically using sector neutral quantiles) and 2. trading cost/turnover (also testing smart rebalancing algorithms like rebalancing only those names going below a certain quantile threshold – De Groot2011 tests the 30/70, 40/60, 50/50, 60/40, 70/30 percentile thresholds).
Can you expand your analysis by including them for testing robustness?
Great post,
Paolo
Very interesting as always. I had a slight off the topic query
regarding this line of code where you pick up components of an Index from Yahoo
tickers = sp500.components()$tickers
How do I change it for any other index say FTSE 100, where can I find the ticker names to use. I tried looking up on Yahoo and it didn’t help me much. Where did you see the actual syntax i.e. “sp500.components”
Thanks for you attention.
John
John, the source code for the sp500.components function is located in the data.r at github. The sp500.components function downloads the table with S&P 500 index components from the following site:
List of S&P 500 companies – Wikipedia, the free encyclopedia
I believe there is some additional functionality to download index holdings available in the “qmao” package. However I did not find a way to get FTSE 100 index components.
I put together a ftse100.components() function in the data.r at github to get FTSE100 information from the following two sites: