An Attempt At Replicating Flexible Asset Allocation (FAA)

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.

29 thoughts on “An Attempt At Replicating Flexible Asset Allocation (FAA)

  1. 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.

  2. 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.

  3. Pingback: The Whole Street’s Daily Wrap for 10/20/2014 | The Whole Street

  4. 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

  5. Pingback: Flexible Asset Allocation With Conditional Correlations | CSSA

  6. Pingback: Introducing Stepwise Correlation Rank | QuantStrat TradeR

  7. 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.

      • 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!

  8. Pingback: A Walk-Forward Attempt on FAA | QuantStrat TradeR

  9. Pingback: Combining FAA and Stepwise Correlation | QuantStrat TradeR

  10. 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

  11. 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)

  12. 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.

  13. Thank you for sharing. May I ask how to code different lookback for volatility and correlation. For instance, 433 = momentum 4 months lookback, volatility 3 months lookback and correlation 3 months lookback.

    Thank you so much.

    • You’d do some things with endpoints. E.G. subset <- returns[(endpoints[i]+1):endpoints[i+12]], but then you might do something like subset_four <- returns[(endpoints[i+8]+1):endpoints[i+12]], and so on. You need to use endpoints subsetting.

Leave a comment