Many of the sites I linked to in the previous post have articles or papers on momentum investing that investigate the typical ranking factors; 3, 6, 9, and 12 month returns. Most (not all) of the articles seek to find which is the “best” look-back period to rank the assets. Say that the outcome of the article is that the 6 month look-back has the highest returns. A trading a strategy that just uses a 6 month look-back period to rank the assets leaves me vulnerable to over-fitting based on the backtest results. The backtest tells us nothing more than which strategy performed the best in the past, it tells us nothing about the future… duh!

Whenever I review the results from backtests, I always ask myself a lot of “what if” questions. Here are 3 “what if” questions that I would ask for this backtest are:

- What if the strategy based on a 6 month look-back under performs and the 9 month or 3 month starts to over perform?
- What if the strategies based on 3, 6, and 9 month look-back periods have about the same return and risk profile, which strategy should I trade?
- What if the assets with high volatility are dominating the rankings and hence driving the returns?

The backtests shown are simple backtests meant to demonstrate the variability in returns based on look-back periods and number of assets traded.

The graphs below show the performance of a momentum strategy using 3, 6, 9, and 12 month returns and trading the Top 1, 4, and 8 ranked assets. You will notice that there is significant volatility and variability in returns only trading 1 asset. The variability between look-back periods is reduced, but there is still no one clear “best” look-back period. There are periods of under performance and over performance for all look back periods in the test.

Here is the R code used for the backtests and the plots. Leave a comment if you have any questions about the code below.

library(FinancialInstrument) library(TTR) library(PerformanceAnalytics) RankRB <- function(x){ # Computes the rank of an xts object of ranking factors # ranking factors are the factors that are ranked (i.e. asset returns) # # args: # x = xts object of ranking factors # # Returns: # Returns an xts object with ranks # (e.g. for ranking asset returns, the asset with the greatest return # receives a rank of 1) r <- as.xts(t(apply(-x, 1, rank, na.last = "keep"))) return(r) } MonthlyAd <- function(x){ # Converts daily data to monthly and returns only the monthly close # Note: only used with Yahoo Finance data so far # Thanks to Joshua Ulrich for the Monthly Ad function # # args: # x = daily price data from Yahoo Finance # # Returns: # xts object with the monthly adjusted close prices sym <- sub("\\..*$", "", names(x)[1]) Ad(to.monthly(x, indexAt = 'lastof', drop.time = TRUE, name = sym)) } CAGR <- function(x, m){ # Function to compute the CAGR given simple returns # # args: # x = xts of simple returns # m = periods per year (i.e. monthly = 12, daily = 252) # # Returns the Compound Annual Growth Rate x <- na.omit(x) cagr <- apply(x, 2, function(x, m) prod(1 + x)^(1 / (length(x) / m)) - 1, m = m) return(cagr) } SimpleMomentumTest <- function(xts.ret, xts.rank, n = 1, ret.fill.na = 3){ # returns a list containing a matrix of individual asset returns # and the comnbined returns # args: # xts.ret = xts of one period returns # xts.rank = xts of ranks # n = number of top ranked assets to trade # ret.fill.na = number of return periods to fill with NA # # Returns: # returns an xts object of simple returns # trade the top n asset(s) # if the rank of last period is less than or equal to n, # then I would experience the return for this month. # lag the rank object by one period to avoid look ahead bias lag.rank <- lag(xts.rank, k = 1, na.pad = TRUE) n2 <- nrow(lag.rank[is.na(lag.rank[,1]) == TRUE]) z <- max(n2, ret.fill.na) # for trading the top ranked asset, replace all ranks above n # with NA to set up for element wise multiplication to get # the realized returns lag.rank <- as.matrix(lag.rank) lag.rank[lag.rank > n] <- NA # set the element to 1 for assets ranked <= to rank lag.rank[lag.rank <= n] <- 1 # element wise multiplication of the # 1 period return matrix and lagged rank matrix mat.ret <- as.matrix(xts.ret) * lag.rank # average the rows of the mat.ret to get the # return for that period vec.ret <- rowMeans(mat.ret, na.rm = TRUE) vec.ret[1:z] <- NA # convert to an xts object vec.ret <- xts(x = vec.ret, order.by = index(xts.ret)) f <- list(mat = mat.ret, ret = vec.ret, rank = lag.rank) return(f) } currency("USD") symbols <- c("XLY", "XLP", "XLE", "XLF", "XLV", "XLI", "XLK", "XLB", "XLU", "EFA")#, "TLT", "IEF", "SHY") stock(symbols, currency = "USD", multiplier = 1) # create new environment to store symbols symEnv <- new.env() # getSymbols and assign the symbols to the symEnv environment getSymbols(symbols, from = '2002-09-01', to = '2012-10-20', env = symEnv) # xts object of the monthly adjusted close prices symbols.close <- do.call(merge, eapply(symEnv, MonthlyAd)) # monthly returns monthly.returns <- ROC(x = symbols.close, n = 1, type = "discrete", na.pad = TRUE) ############################################################################# # rate of change and rank based on a single period for 3, 6, 9, and 12 months ############################################################################# roc.three <- ROC(x = symbols.close , n = 3, type = "discrete") rank.three <- RankRB(roc.three) roc.six <- ROC(x = symbols.close , n = 6, type = "discrete") rank.six <- RankRB(roc.six) roc.nine <- ROC(x = symbols.close , n = 9, type = "discrete") rank.nine <- RankRB(roc.nine) roc.twelve <- ROC(x = symbols.close , n = 12, type = "discrete") rank.twelve <- RankRB(roc.twelve) num.assets <- 4 # simple momentum test based on 3 month ROC to rank case1 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.three, n = num.assets, ret.fill.na = 15) # simple momentum test based on 6 month ROC to rank case2 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.six, n = num.assets, ret.fill.na = 15) # simple momentum test based on 9 month ROC to rank case3 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.nine, n = num.assets, ret.fill.na = 15) # simple momentum test based on 12 month ROC to rank case4 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.twelve, n = num.assets, ret.fill.na = 15) returns <- cbind(case1$ret, case2$ret, case3$ret, case4$ret) colnames(returns) <- c("3-Month", "6-Month", "9-Month", "12-Month") charts.PerformanceSummary(R = returns, Rf = 0, geometric = TRUE, main = "Momentum Cumulative Return: Top 4 Assets") table.Stats(returns) CAGR(returns, m = 12) print("End")

Nice post. Your ranking code from prior post was helpful. Glad to see you are back on blog.

How would you take account commission in the above code?

mike

Hey Ross,

I have a quick question for you, but let me begin by saying, excellent post! In fact, excellent series on Momentum with R.

On with my question. So, in this exercise, you’ve constructed portfolios based on past returns; 3, 6, 9 and 12 month returns. Then, from what I understand, you take a long position in the top asset (or in two other cases, the top 4 and top 8 assets) and hold this position for 1 month only. What I am wondering is what sort of results you’d get if you were to, say, hold the position for 3, 6, 9, or 12 months, instead of just 1 month. This kind of idea may be of interest to the investor with a longer term perspective.

Is there any chance you could do another post using momentum with R taking into account this slight variant of the exercise?

Cheers,

GW

Hi GW,

This is something I could do in a later post. It is not easily doable the way I wrote the functions.

Ross

nice post,

I’ve been trying to replicate this for the Brazilian markets (Ibovespa), but unfortunately yahoo data lacks treatment for stock splits and/or dividends.

I guess I have lots of work treating the database before testing the models…

getSymbols has an argument for adjusting the data. Also, if you’d like to do things manually, I suppose you can just divide the close by the adjusted close of the first time period, then divide the rest of the data by that same number.