Since the people at Alpha Architect were so kind as to feature my blog in a post, I figured I’d investigate an idea that I first found out about from their site–namely, flexible asset allocation. Here’s the SSRN, and the corresponding Alpha Architect post.
Here’s the script I used for this replication, which is completely self-contained.
require(PerformanceAnalytics) require(quantmod) mutualFunds <- c("VTSMX", #Vanguard Total Stock Market Index "FDIVX", #Fidelity Diversified International Fund "VEIEX", #Vanguard Emerging Markets Stock Index Fund "VFISX", #Vanguard Short-Term Treasury Fund "VBMFX", #Vanguard Total Bond Market Index Fund "QRAAX", #Oppenheimer Commodity Strategy Total Return "VGSIX" #Vanguard REIT Index Fund ) #mid 1997 to end of 2012 getSymbols(mutualFunds, from="1997-06-30", to="2012-12-31") tmp <- list() for(fund in mutualFunds) { tmp[[fund]] <- Ad(get(fund)) } #always use a list hwne intending to cbind/rbind large quantities of objects adPrices <- do.call(cbind, args = tmp) colnames(adPrices) <- gsub(".Adjusted", "", colnames(adPrices)) FAAreturns <- function(prices, monthLookback = 4, weightMom=1, weightVol=.5, weightCor=.5, riskFreeName="VFISX", bestN=3) { returns <- Return.calculate(prices) monthlyEps <- endpoints(prices, on = "months") riskFreeCol <- grep(riskFreeName, colnames(prices)) tmp <- list() dates <- list() for(i in 2:(length(monthlyEps) - monthLookback)) { #subset data priceData <- prices[monthlyEps[i]:monthlyEps[i+monthLookback],] returnsData <- returns[monthlyEps[i]:monthlyEps[i+monthLookback],] #perform computations momentum <- data.frame(t(t(priceData[nrow(priceData),])/t(priceData[1,]) - 1)) priceData <- priceData[, momentum > 0] #remove securities with momentum < 0 returnsData <- returnsData[, momentum > 0] momentum <- momentum[momentum > 0] names(momentum) <- colnames(returnsData) vol <- as.numeric(-sd.annualized(returnsData)) #sumCors <- -colSums(cor(priceData[endpoints(priceData, on="months")])) sumCors <- -colSums(cor(returnsData, use="complete.obs")) stats <- data.frame(cbind(momentum, vol, sumCors)) if(nrow(stats) > 1) { #perform ranking ranks <- data.frame(apply(stats, 2, rank)) weightRankSum <- weightMom*ranks$momentum + weightVol*ranks$vol + weightCor*ranks$sumCors totalRank <- rank(weightRankSum) #find top N values, from http://stackoverflow.com/questions/2453326/fastest-way-to-find-second-third-highest-lowest-value-in-vector-or-column #thanks to Dr. Rob J. Hyndman upper <- length(names(returnsData)) lower <- max(upper-bestN+1, 1) topNvals <- sort(totalRank, partial=seq(from=upper, to=lower))[c(upper:lower)] #compute weights longs <- totalRank %in% topNvals #invest in ranks length - bestN or higher (in R, rank 1 is lowest) longs <- longs/sum(longs) #equal weight all candidates longs[longs > 1/bestN] <- 1/bestN #in the event that we have fewer than top N invested into, lower weights to 1/top N names(longs) <- rownames(ranks) } else if(nrow(stats) == 1) { #only one security had positive momentum longs <- 1/bestN names(longs) <- rownames(stats) } else { #no securities had positive momentum longs <- 1 names(longs) <- riskFreeName } #append removed names (those with momentum < 0) removedZeroes <- rep(0, ncol(returns)-length(longs)) names(removedZeroes) <- names(returns)[!names(returns) %in% names(longs)] longs <- c(longs, removedZeroes) #reorder to be in the same column order as original returns/prices longs <- data.frame(t(longs)) longs <- longs[, names(returns)] #append lists tmp[[i]] <- longs dates[[i]] <- index(returnsData)[nrow(returnsData)] } weights <- do.call(rbind, tmp) dates <- do.call(c, dates) weights <- xts(weights, order.by=as.Date(dates)) weights[, riskFreeCol] <- weights[, riskFreeCol] + 1-rowSums(weights) strategyReturns <- Return.rebalancing(R = returns, weights = weights, geometric = FALSE) return(strategyReturns) } replicaAttempt <- FAAreturns(adPrices) bestN4 <- FAAreturns(adPrices, bestN=4) N3vol1cor1 <- FAAreturns(adPrices, weightVol = 1, weightCor = 1) minRisk <- FAAreturns(adPrices, weightMom = 0, weightVol=1, weightCor=1) pureMomentum <- FAAreturns(adPrices, weightMom=1, weightVol=0, weightCor=0) maxDecor <- FAAreturns(adPrices, weightMom=0, weightVol=0, weightCor=1) momDecor <- FAAreturns(adPrices, weightMom=1, weightVol=0, weightCor=1) all <- cbind(replicaAttempt, bestN4, N3vol1cor1, minRisk, pureMomentum, maxDecor, momDecor) colnames(all) <- c("Replica Attempt", "N4", "vol_1_cor_1", "minRisk", "pureMomentum", "maxDecor", "momDecor") charts.PerformanceSummary(all, colorset=c("black", "red", "blue", "green", "darkgrey", "purple", "orange")) stats <- data.frame(t(rbind(Return.annualized(all)*100, maxDrawdown(all)*100, SharpeRatio.annualized(all)))) stats$Return_To_Drawdown <- stats[,1]/stats[,2]
Here’s the formal procedure:
Using the monthly endpoint functionality in R, every month, looking over the past four months, I computed momentum as the most recent price over the first price in the observed set (that is, the price four months ago) minus one, and instantly removed any funds with a momentum less than zero (this was a suggestion from Mr. David Varadi of CSS Analytics, with whom I’ll be collaborating in the near future). Next, with the pared down universe, I ranked the funds by momentum, by annualized volatility (the results are identical with just standard deviation), and by the sum of the correlations with each other. Since volatility and correlation are worse at higher values, I multiplied each by negative one. Next, I invested in the top N funds every period, or if there were fewer than N funds with positive momentum, each remaining fund received a weight of 1/N, with the rest eventually being placed into the “risk-free” asset, in this case, VFISX. All price and return data were daily adjusted (as per the SSRN paper) data.
However, my results do not match the paper’s (or Alpha Architect’s) in that I don’t see the annualized returns breaking 20%, nor, most importantly, do I see the single-digit drawdowns. I hope my code is clear for every step as to what the discrepancy may be, but that aside, let me explain what the idea is.
The idea is, from those that are familiar with trend following, that in addition to seeking return through the momentum anomaly (stacks of literature available on the simple idea that what goes up will keep going up to an extent), that there is also a place for risk management. This comes in the form of ranking correlation and volatility, and giving different weights to each individual component rank (that is, momentum has a weight of 1, correlation .5, and volatility .5). Next, the weighted sum of the ranks is then also ranked (so two layers of ranking) for a final aggregate rank.
Unfortunately, when it comes to the implementation, the code has to be cluttered with some data munging and edge-case checking, which takes a little bit away from the readability. To hammer a slight technical tangent home, in R, whenever one plans on doing iterated appending (E.G. one table that’s repeatedly appended), due to R copying an object on assignment when doing repeated rbinding or cbinding, but simply appending the last iteration onto a list object, outside of tiny data frames, it’s always better to use a list and only call rbind/cbind once at the end. The upside to data frames is that they’re much easier to print out to a console and to do vectorized operations on. However, lists are more efficient when it comes to iteration.
In any case, here’s an examination of some variations of this strategy.
The first is a simple attempt at replication (3 of 7 securities, 1 weight to momentum, .5 to volatility and correlation each). The second is that same setting, just with the top four securities instead of the top three. A third one is with three securities, but double the weighting to the two risk metrics (vol & cor). The next several are conceptual permutations–a risk minimization profile that puts no weight on the actual nature of momentum (analogous to what the Smart Beta folks would call min-vol), a pure momentum strategy (disregard vol and cor), a max decorrelation strategy (all weight on correlation), and finally, a hybrid of momentum and max decorrelation.
Here is the performance chart:
Overall, this looks like evidence of robustness, given that I fundamentally changed the nature of the strategies in quite a few cases, rather than simply tweaked the weights here or there. The momentum/decorrelation hybrid is a bit difficult to see, so here’s a clearer image for how it compared with the original strategy.
Overall, a slightly smoother ride, though slightly lower in terms of returns. Here’s the table comparing all seven variations:
> stats Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. Return_To_Drawdown Replica Attempt 14.43802 13.156252 1.489724 1.0974268 N4 12.48541 10.212778 1.492447 1.2225281 vol_1_cor_1 12.86459 12.254390 1.608721 1.0497944 minRisk 11.26158 9.223409 1.504654 1.2209786 pureMomentum 13.88501 14.401121 1.135252 0.9641619 maxDecor 11.89159 11.685492 1.434220 1.0176368 momDecor 14.03615 10.951574 1.489358 1.2816563
Overall, there doesn’t seem to be any objectively best variant, though pure momentum is definitely the worst (as may be expected, otherwise the original paper wouldn’t be as meaningful). If one is looking for return to max drawdown, then the momentum/max decorrelation hybrid stands out, though the 4-security variant and minimum risk variants also work (though they’d have to be leveraged a tiny bit to get the annualized returns to the same spot). On Sharpe Ratio, the variant with double the original weighting on volatility and correlation stands out, though its return to drawdown ratio isn’t the greatest.
However, the one aspect that I take away from this endeavor is that the number of assets were relatively tiny, and the following statistic:
> SharpeRatio.annualized(Return.calculate(adPrices)) VTSMX FDIVX VEIEX VFISX VBMFX QRAAX VGSIX Annualized Sharpe Ratio (Rf=0%) 0.2520994 0.3569858 0.2829207 1.794041 1.357554 -0.01184516 0.3062336
Aside from the two bond market funds, which are notorious for lower returns for lower risk, the Sharpe ratios of the individual securities are far below 1. The strategy itself, on the other hand, has very respectable Sharpe ratios, working with some rather sub-par components.
Simply put, consider running this asset allocation heuristic on your own set of strategies, as opposed to pre-set funds. Furthermore, it is highly likely that the actual details of the ranking algorithm can be improved, from different ranking metrics (add drawdown?) to more novel concepts such as stepwise correlation ranking/selection.
Thanks for reading.
Thanks for sharing the code and post. I briefly looked over the paper, and the results seem to match yours. Except, the results with CAGR > 20% and good DDs are using 2X leverage and optimizing some of the weight parameters around Calmar Ratio. I have been surprised/fooled/mystified by reading literature in the past (with > 20% CAGR) — that excluded to mention leverage (often happens in futures trend following)… It is nice that they mentioned it.
Pat, my pleasure. I thought I was having an error somewhere. Nice to know that I have the idea correct.
Thanks for this and all other great posts.
Can you maybe create an example strategy using “Furthermore, it is highly likely that the actual details of the ranking algorithm can be improved, from different ranking metrics (add drawdown?) to more novel concepts such as stepwise correlation ranking/selection.” I am interested how drawdown and especially how “stepwise correlation ranking/selection” would be implemented in R.
Not sure if there’s a stepwise correlation algorithm in R yet. However, drawdown is already in R, and I’ve used it time and again in my blog already. It’s the maxDrawdown function.
Pingback: The Whole Street’s Daily Wrap for 10/20/2014 | The Whole Street
Hi Ilya,
This is excellent work – thank you!. The code clear and your commentary is very helpful. I think it would be interesting to see a walk-forward analysis from optimizing the LB and LMV weights on an annual basis. Perhaps this is something you could do if you had time. Again, thank you for the post (and other posts)!
-GeraldM
Gerald, what do you mean by LB and LMV?
Apologies for my abbreviations (and unnoticed typo). I was referring to optimizing the Look Back (LB) as well as the Momentum, Correlation and Volatility weights (MCV, LMV was a typo which only confused things more).
Pingback: Flexible Asset Allocation With Conditional Correlations | CSSA
Pingback: Introducing Stepwise Correlation Rank | QuantStrat TradeR
Would you be willing to post the R code which shows the fund holdings/weights at each rebalance for the strategy replication?
Scott,
You can edit the function to return the following:
out <- list(weights, strategyReturns)
return(out)
And then the weights will be the first element of the returned list.
Hi Ilya
Dumb question but where what line in your original script would this follow?
Also, have you run the same test in R using conditional correlations as described here: https://cssanalytics.wordpress.com/2014/10/27/flexible-asset-allocation-with-conditional-correlations/
One final question, overall your code has worked great is very helpful. However, the table showing Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. Return_To_Drawdown never generates for me. Is there a package I could be missing or is there an additional step I need to take when running the script?
The package is PerformanceAnalytics. To answer your question about conditional correlation, continue reading.
I have the performanceanalytics package loaded but still cannot get the table to display. Is there an additional step I need to take? The performance chart loads fine.
Scott, You need to use the statsTable command – as in:
statsTable$ReturnDrawdownRatio <- statsTable[,1]/statsTable[,2]
statsTable
This is not shown in the code about but is shown in Ilya's other posts. You might want to look at the vignette for PerformanceAnalytics because you can do a lot more with it than show here.
Also, Ilya has done a wonderful job with this work and has incorporated Stepwise Correlation (and walk forward analysis) for this algorithm. They are in his other posts.
Cheers!
Pingback: A Walk-Forward Attempt on FAA | QuantStrat TradeR
Pingback: Combining FAA and Stepwise Correlation | QuantStrat TradeR
Hey Ilya, I was just playing with your code and I changed the funds that your are and replaced with 7 random funds. I made sure that the datas were complete (I didnt use funds that were created after 1997). So the things I changed in the code is
1.
mutualFunds <- c( ………..) for
mutualFunds <- c("FUSEX", #Fidelity Spartan 500
"FDIVX", #Fidelity Diversified International Fund
"FEMKX", #Fidelity Emerging Markets Fund
"PTSHX", #Pimco short term
"PTTRX", #PIMCO total return fund
"QRAAX", #Oppenheimer Commodity Strategy Total Return
"VGSIX" #Vanguard REIT Index Fund
)
2. in the line 26 I put
riskFreeName="PTSHX" instead of riskFreeName="VFISX"
Is there anything else I should change from the original code?
After I execute lines 101 to 107 I get that error message for each lines:
Error in dimnames(cd) <- list(as.character(index(x)), colnames(x)) :
'dimnames' applied to non-array
What am I missing here?
Thanks a Lot
Frank,
Here’s my sessionInfo.
I changed the names to the funds you gave me, and everything worked for me.
> sessionInfo()
R version 3.1.1 (2014-07-10)
Platform: x86_64-pc-linux-gnu (64-bit)
locale:
[1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=C LC_COLLATE=C LC_MONETARY=C
[6] LC_MESSAGES=C LC_PAPER=C LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=C LC_IDENTIFICATION=C
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] IKTrading_1.0 roxygen2_2.2.2 digest_0.6.3
[4] Rcpp_0.11.2 class_7.3-11 reshape2_1.2.2
[7] ggplot2_1.0.0 lattice_0.20-29 downloader_0.3
[10] quantstrat_0.8.2 foreach_1.4.1 blotter_0.8.19
[13] PerformanceAnalytics_1.4.3541 FinancialInstrument_1.1.9 quantmod_0.4-1
[16] Defaults_1.1-1 TTR_0.22-0.1 xts_0.9-7
[19] zoo_1.7-11 devtools_1.3
loaded via a namespace (and not attached):
[1] MASS_7.3-33 RCurl_1.95-4.1 brew_1.0-6 codetools_0.2-8 colorspace_1.2-4 evaluate_0.5.5
[7] grid_3.1.1 gtable_0.1.2 httr_0.2 iterators_1.0.6 memoise_0.1 munsell_0.4.2
[13] parallel_3.1.1 plyr_1.8 proto_0.3-10 scales_0.2.4 stringr_0.6.2 tools_3.1.1
[19] whisker_0.3-2
So you might want to update your libraries.
So you used the names I used and you only changed the the few lines I changed?
I’m a complete R newbie would uninstalling R and reinstalling it ”update my libraries” or hopefully fix this (lol)
Here’s the code I ran, with the latest iteration of my IKTrading package:
mutualFunds <- c("FUSEX", #Fidelity Spartan 500
"FDIVX", #Fidelity Diversified International Fund
"FEMKX", #Fidelity Emerging Markets Fund
"PTSHX", #Pimco short term
"PTTRX", #PIMCO total return fund
"QRAAX", #Oppenheimer Commodity Strategy Total Return
"VGSIX" #Vanguard REIT Index Fund
)
#mid 1997 to end of 2012
getSymbols(mutualFunds, from="1997-06-30", to="2014-10-30")
tmp <- list()
for(fund in mutualFunds) {
tmp[[fund]] <- Ad(get(fund))
}
#always use a list hwne intending to cbind/rbind large quantities of objects
adPrices <- do.call(cbind, args = tmp)
colnames(adPrices) <- gsub(".Adjusted", "", colnames(adPrices))
original <- FAAreturns(adPrices, stepCorRank=FALSE, riskFreeName="PTSHX")
This works for me.
thanks
is it possible that IKTrading is not compatible with r 3.1.2?
No.