Combining FAA and Stepwise Correlation

Since I debuted the stepwise correlation algorithm, I suppose the punchline that people want to see is: does it actually work?

The short answer? Yes, it does.

A slightly longer answer: it works, With the caveat that having a better correlation algorithm that makes up 25% of the total sum of weighted ranks only has a marginal (but nevertheless positive) effect on returns. Furthermore, when comparing a max decorrelation weighting using default single-pass correlation vs. stepwise, the stepwise gives a bumpier ride, but one with visibly larger returns. Furthermore, for this universe, the difference between starting at the security ranked highest by the momentum and volatility components, or with the security that has the smallest aggregate correlation to all securities, is very small. Essentially, from my inspection, the answer to using stepwise correlation is: “it’s a perfectly viable alternative, if not better.”

Here are the functions used in the script:

require(quantmod)
require(PerformanceAnalytics)

stepwiseCorRank <- function(corMatrix, startNames=NULL, stepSize=1, bestHighestRank=FALSE) {
  #edge cases
  if(dim(corMatrix)[1] == 1) {
    return(corMatrix)
  } else if (dim(corMatrix)[1] == 2) {
    ranks <- c(1.5, 1.5)
    names(ranks) <- colnames(corMatrix)
    return(ranks)
  }
  
  if(is.null(startNames)) {
    corSums <- rowSums(corMatrix)
    corRanks <- rank(corSums)
    startNames <- names(corRanks)[corRanks <= stepSize]
  }
  nameList <- list()
  nameList[[1]] <- startNames
  rankList <- list()
  rankCount <- 1
  rankList[[1]] <- rep(rankCount, length(startNames))
  rankedNames <- do.call(c, nameList)
  
  while(length(rankedNames) < nrow(corMatrix)) {
    rankCount <- rankCount+1
    subsetCor <- corMatrix[, rankedNames]
    if(class(subsetCor) != "numeric") {
      subsetCor <- subsetCor[!rownames(corMatrix) %in% rankedNames,]
      if(class(subsetCor) != "numeric") {
        corSums <- rowSums(subsetCor)
        corSumRank <- rank(corSums)
        lowestCorNames <- names(corSumRank)[corSumRank <= stepSize]
        nameList[[rankCount]] <- lowestCorNames
        rankList[[rankCount]] <- rep(rankCount, min(stepSize, length(lowestCorNames)))
      } else { #1 name remaining
        nameList[[rankCount]] <- rownames(corMatrix)[!rownames(corMatrix) %in% names(subsetCor)]
        rankList[[rankCount]] <- rankCount
      }
    } else {  #first iteration, subset on first name
      subsetCorRank <- rank(subsetCor)
      lowestCorNames <- names(subsetCorRank)[subsetCorRank <= stepSize]
      nameList[[rankCount]] <- lowestCorNames
      rankList[[rankCount]] <- rep(rankCount, min(stepSize, length(lowestCorNames)))
    }    
    rankedNames <- do.call(c, nameList)
  }
  
  ranks <- do.call(c, rankList)
  names(ranks) <- rankedNames
  if(bestHighestRank) {
    ranks <- 1+length(ranks)-ranks
  }
  ranks <- ranks[colnames(corMatrix)] #return to original order
  return(ranks)
}


FAAreturns <- function(prices, monthLookback = 4,
                       weightMom=1, weightVol=.5, weightCor=.5, 
                       riskFreeName="VFISX", bestN=3,
                       stepCorRank = FALSE, stepStartMethod=c("best", "default")) {
  stepStartMethod <- stepStartMethod[1]
  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))
    
    if(length(momentum) > 1) {
      
      #perform ranking
      if(!stepCorRank) {
        sumCors <- -colSums(cor(returnsData, use="complete.obs"))
        stats <- data.frame(cbind(momentum, vol, sumCors))
        ranks <- data.frame(apply(stats, 2, rank))
        weightRankSum <- weightMom*ranks$momentum + weightVol*ranks$vol + weightCor*ranks$sumCors
        names(weightRankSum) <- rownames(ranks)
      } else {
        corMatrix <- cor(returnsData, use="complete.obs")
        momRank <- rank(momentum)
        volRank <- rank(vol)
        compositeMomVolRanks <- weightMom*momRank + weightVol*volRank
        maxRank <- compositeMomVolRanks[compositeMomVolRanks==max(compositeMomVolRanks)]
        if(stepStartMethod=="default") {
          stepCorRanks <- stepwiseCorRank(corMatrix=corMatrix, startNames = NULL, 
                                          stepSize = 1, bestHighestRank = TRUE)
        } else {
          stepCorRanks <- stepwiseCorRank(corMatrix=corMatrix, startNames = names(maxRank), 
                                          stepSize = 1, bestHighestRank = TRUE)
        }
        weightRankSum <- weightMom*momRank + weightVol*volRank + weightCor*stepCorRanks
      }
      
      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) <- names(totalRank)
      
    } else if(length(momentum) == 1) { #only one security had positive momentum 
      longs <- 1/bestN
      names(longs) <- names(momentum)
    } 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)
  colnames(strategyReturns) <- paste(monthLookback, weightMom, weightVol, weightCor, sep="_")
  return(strategyReturns)
}

The FAAreturns function has been modified to transplant the stepwise correlation algorithm I discussed earlier. Essentially, the chunk of code that performs the ranking inside the function got a little bit larger, and some new arguments to the function have been introduced.

First off, there’s the option to use the stepwise correlation algorithm in the first place–namely, the stepCorRank defaulting to FALSE (the default settings replicate the original FAA idea demonstrated in the first post on this idea). However, the argument that comes next, the stepStartMethod argument does the following:

Using the “default” setting, the algorithm will start off using the security that is simply least correlated among the securities (that is, the lowest sum of correlations among securities). However, the “best” setting instead will use the weighted sum of ranks using the prior two factors (momentum and volatility). This argument defaults to using the best security (aka the one best ranked prior by the previous two factors), as opposed to the default. At the end of the day, I suppose the best way of illustrating functionality is with some examples of taking this piece of engineering out for a spin. So here goes!

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

original <- FAAreturns(adPrices, stepCorRank=FALSE)
originalSWCbest <- FAAreturns(adPrices, stepCorRank=TRUE)
originalSWCdefault <- FAAreturns(adPrices, stepCorRank=TRUE, stepStartMethod="default")
stepMaxDecorBest <- FAAreturns(adPrices, weightMom=.05, weightVol=.025, 
                               weightCor=1, riskFreeName="VFISX", 
                               stepCorRank = TRUE, stepStartMethod="best")
stepMaxDecorDefault <- FAAreturns(adPrices, weightMom=.05, weightVol=.025, 
                                  weightCor=1, riskFreeName="VFISX", 
                                  stepCorRank = TRUE, stepStartMethod="default")
w311 <- FAAreturns(adPrices, weightMom=3, weightVol=1, weightCor=1, stepCorRank=TRUE)
originalMaxDecor <- FAAreturns(adPrices, weightMom=0, weightVol=1, stepCorRank=FALSE)
tmp <- cbind(original, originalSWCbest, originalSWCdefault, 
             stepMaxDecorBest, stepMaxDecorDefault, w311, originalMaxDecor)
names(tmp) <- c("original", "originalSWCbest", "originalSWCdefault", "SMDB", 
                "SMDD", "w311", "originalMaxDecor")
charts.PerformanceSummary(tmp, colorset=c("black", "orange", "blue", "purple", "green", "red", "darkred"))


statsTable <- data.frame(t(rbind(Return.annualized(tmp)*100, maxDrawdown(tmp)*100, SharpeRatio.annualized(tmp))))
statsTable$ReturnDrawdownRatio <- statsTable[,1]/statsTable[,2]

Same seven securities as the original paper, with the following return streams:

Original: the FAA original replication
originalSWCbest: original weights, stepwise correlation algorithm, using the best security as ranked by momentum and volatility as a starting point.
originalSWCdefault: original weights, stepwise correlation algorithm, using the default (minimum sum of correlations) security as a starting point.
stepMaxDecorBest: a max decorrelation algorithm that sets the momentum and volatility weights at .05 and .025 respectively, compared to 1 for correlation, simply to get the best starting security through the first two factors.
stepMaxDecorDefault: analogous to originalSWCdefault, except with the starting security being defined as the one with minimum sum of correlations.
w311: using a weighting of 3, 1, and 1 on momentum, vol, and correlation, respectively, while using the stepwise correlation rank algorithm, starting with the best security (the default for the function), since I suspected that not weighing momentum at 1 or higher was the reason any other equity curves couldn’t top out above the paper’s original.
originalMaxDecor: max decorrelation using the original 1-pass correlation matrix

Here is the performance chart:

Here’s the way I interpret it:

Does David Varadi’s stepwise correlation ranking algorithm help performance? From this standpoint, the answers lead to yes. Using the original paper’s parameters, the performance over the paper’s backtest period is marginally better in terms of the equity curves. Comparing max decorrelation algorithms (SMDB and SMDD stand for stepwise max decorrelation best and default, respectively), the difference is even more clear.

However, I was wondering why I could never actually outdo the original paper’s annualized return, and out of interest, decided to more heavily weigh the momentum ranking than the original paper eventually had it set at. The result is a bumpier equity curve, but one that has a higher annualized return than any of the others. It’s also something that I didn’t try in my walk-forward example (though interested parties can simply modify the original momentum vector to contain a 1.5 weight, for instance).

Here’s the table of statistics for the different permutations:

> statsTable
                   Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. ReturnDrawdownRatio
original                    14.43802       13.15625                        1.489724            1.097427
originalSWCbest             14.70544       13.15625                        1.421045            1.117753
originalSWCdefault          14.68145       13.37059                        1.457418            1.098041
SMDB                        13.55656       12.33452                        1.410072            1.099075
SMDD                        13.18864       11.94587                        1.409608            1.104033
w311                        15.76213       13.85615                        1.398503            1.137555
originalMaxDecor            11.89159       11.68549                        1.434220            1.017637

At the end of the day, all of the permutations exhibit solid results, and fall along different ends of the risk/return curve. The original settings exhibit the highest Sharpe Ratio (barely), but not the highest annualized return to max drawdown ratio (which surprisingly, belongs to the setting that overweights momentum).

To wrap this analysis up (since there are other strategies that I wish to replicate), here is the out-of-sample performance of these seven strategies (to Oct 30, 2014):

Maybe not the greatest thing in the world considering the S&P has made some spectacular returns in 2013, but nevertheless, the momentum variant strategies established new equity highs fairly recently, and look to be on their way up from their latest slight drawdown. Here are the statistics for 2013-2014:

statsTable <- data.frame(t(rbind(Return.annualized(tmp["2013::"])*100, maxDrawdown(tmp["2013::"])*100, SharpeRatio.annualized(tmp["2013::"]))))
statsTable$ReturnDrawdownRatio <- statsTable[,1]/statsTable[,2]

> statsTable
                   Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. ReturnDrawdownRatio
original                    9.284678       8.259298                       1.1966581           1.1241485
originalSWCbest             8.308246       9.657667                       0.9627413           0.8602746
originalSWCdefault          8.916144       8.985685                       1.0861781           0.9922609
SMDB                        6.406438       9.657667                       0.8366559           0.6633525
SMDD                        5.641980       5.979313                       0.7840507           0.9435833
w311                        8.921268       9.025100                       1.0142871           0.9884953
originalMaxDecor            2.888778       6.670709                       0.4244202           0.4330542

So, the original parameters are working solidly, the stepwise correlation algorithm seems to be in a slight rut, and the variants without any emphasis on momentum simply aren’t that great (they were created purely as illustrative tools to begin with). Whether you prefer to run FAA with these securities, or with trading strategies of your own, my only caveat is that transaction costs haven’t been taken into consideration (from what I hear, interactive brokers charges you $1 per transaction, so it shouldn’t make a world of a difference), but beyond that, I believe these last four posts have shown that FAA is something that works. While it doesn’t always work perfectly (EG the S&P 500 had a very good 2013), the logic is sound, and the results are solid, even given some rather plain-vanilla type securities.

In any case, I think I’ll conclude with the fact that FAA works, and the stepwise correlation algorithm provides a viable alternative to computing your weights. I’ll update my IKTrading package with some formal documentation regarding this algorithm soon.

Thanks for reading.

A Walk-Forward Attempt on FAA

So in the first post about FAA, I was requested to make a walk-forward test of FAA. While the results here aren’t good, I’d like to share the general process anyway.

Here’s the additional code I wrote, assuming the first post‘s code is still in your environment (the demo will have the function in the namespace as well):

weightMom <- seq(0, 1, by=.5)
weightVol <- c(0, .5, 1)
weightCor <- c(0, .5, 1)
monthLookback=c(3, 4, 6, 10)
permutations <- expand.grid(weightMom, weightVol, weightCor, monthLookback)
colnames(permutations) <- c("wMom", "wVol", "wCor", "monthLookback")

require(doMC)
registerDoMC(detectCores())
t1 <- Sys.time()
out <- foreach(i = 1:nrow(permutations), .combine = cbind) %dopar% {
  FAAreturns(prices=adPrices, 
             monthLookback = permutations$monthLookback[i], 
             weightMom = permutations$wMom[i], 
             weightCor = permutations$wCor[i], 
             weightVol=permutations$wVol[i])
}
t2 <- Sys.time()
print(t2-t1)

out <- out["1998-10::"] #start at 1999 due to NAs with data

FAAwalkForward <- function(portfolios, applySubset = apply.quarterly, applyFUN = Return.cumulative) {
  metrics <- applySubset(portfolios, applyFUN)
  weights <- list()
  for(i in 1:nrow(metrics)) {
    row <- metrics[i,]
    winners <- row==max(row)
    weight <- winners/rowSums(winners) #equal weight all top performers
    weights[[i]] <- weight
  }
  weights <- do.call(rbind, weights)
  returns <- Return.rebalancing(portfolios, weights)
  return(returns)
}

WFQtrRets <- FAAwalkForward(portfolios = out, applySubset = apply.quarterly, applyFUN = Return.cumulative)
WFYrRets <- FAAwalkForward(portfolios = out, applySubset = apply.yearly, applyFUN = Return.cumulative)
WFMoRets <- FAAwalkForward(portfolios = out, applySubset = apply.monthly, applyFUN = Return.cumulative)

WF <- cbind(WFQtrRets, WFYrRets, WFMoRets)
colnames(WF) <- c("quarterly", "annually", "monthly")
WF <- WF["1999::"]

original <- FAAreturns(adPrices)
original <- original["1999::"]
WF <- cbind(WF, original)
colnames(WF)[4] <- "original"
charts.PerformanceSummary(WF)


Return.annualized(WF)
maxDrawdown(WF)
SharpeRatio.annualized(WF)

So what I did was take about a hundred permutations, and compute them all in parallel using the doMC package (Windows uses a different parallel architecture, but this post explains a more OS-agnostic method). Next, I wrote a small function that would compute a metric for all of the permutations for some period (monthly, quarterly, annually), and equal-weight all of the maximum configurations, which were in some cases, more than one. This would be the strategy’s holdings over the next period, and repeat this iteration to the end. I started in late 1998, as I used a 10-month lookback period for one of the monthly lookback settings, while the data starts in mid 1997.

Here are the results:

> Return.annualized(WF)
                  quarterly  annually   monthly  original
Annualized Return 0.1303674 0.1164749 0.1204314 0.1516936
> maxDrawdown(WF)
               quarterly  annually   monthly  original
Worst Drawdown 0.1666639 0.1790334 0.1649651 0.1315625
> SharpeRatio.annualized(WF)
                                quarterly annually  monthly original
Annualized Sharpe Ratio (Rf=0%)  1.257753 1.025863 1.194066 1.519912

In short, using a walk-forward with FAA seriously harmed the performance.

After seeing these disappointing results, I mulled over as to the why, and here’s my intuition:

A) FAA uses a ranking algorithm, which loses the nuance of slightly changing this weight or that, leading to many identical configurations for a given time period. That some of these or all of these configurations are only the best for that period is a very real possibility.

B) Given that there are so few securities, it’s quite possible that the walk-forward process is simply chasing performance–that is, switching into a configuration right after it had its one moment in the sun. Considering that the original strategy is fairly solid to begin with, it certainly seems to be better to pick a decent configuration and stick with it.

C) One last thought that stuck with me is that the original FAA strategy meets all the qualifications *for* a walk-forward strategy already. It is a monthly-rebalanced algorithm that seeks to maximize an objective function (rank of the weighted sum of ranks of momentum, volatility, and correlation) in a robust fashion–it’s simply that instead of strategy configurations, the inputs were seven mutual funds. To me, in fact, the more I think about it, the more FAA looks like an extremely solid walk-forward framework, simply presented as a strategy in and of itself.

In any case, while the results were far from spectacular, I’m hoping that the code has given others some ideas about how to conduct some generalized returns-based walk-forward testing on their own strategies.

On one last note, if any readers have ideas that they’d like me to investigate, I’m always open to input and new ideas.

Thanks for reading.

Introducing Stepwise Correlation Rank

So in the last post, I attempted to replicate the Flexible Asset Allocation paper. I’d like to offer a thanks to Pat of Intelligent Trading Tech (not updated recently, hopefully this will change) for helping me corroborate the results, so that I have more confidence there isn’t an error in my code.

One of the procedures the authors of the FAA paper used is a correlation rank, which I interpreted as the average correlation of each security to the others.

The issue, pointed out to me in a phone conversation I had with David Varadi is that when considering correlation, shouldn’t the correlations the investor is concerned about be between instruments within the portfolio, as opposed to simply all the correlations, including to instruments not in the portfolio? To that end, when selecting assets (or possibly features in general), conceptually, it makes more sense to select in a stepwise fashion–that is, start off at a subset of the correlation matrix, and then rank assets in order of their correlation to the heretofore selected assets, as opposed to all of them. This was explained in Mr. Varadi’s recent post.

Here’s a work in progress function I wrote to formally code this idea:

stepwiseCorRank <- function(corMatrix, startNames=NULL, stepSize=1, bestHighestRank=FALSE) {
  #edge cases
  if(dim(corMatrix)[1] == 1) {
    return(corMatrix)
  } else if (dim(corMatrix)[1] == 2) {
    ranks <- c(1.5, 1.5)
    names(ranks) <- colnames(corMatrix)
    return(ranks)
  }
  if(is.null(startNames)) {
    corSums <- rowSums(corMatrix)
    corRanks <- rank(corSums)
    startNames <- names(corRanks)[corRanks <= stepSize]
  }
  nameList <- list()
  nameList[[1]] <- startNames
  rankList <- list()
  rankCount <- 1
  rankList[[1]] <- rep(rankCount, length(startNames))
  rankedNames <- do.call(c, nameList)
  
  while(length(rankedNames) < nrow(corMatrix)) {
    rankCount <- rankCount+1
    subsetCor <- corMatrix[, rankedNames]
    if(class(subsetCor) != "numeric") {
      subsetCor <- subsetCor[!rownames(corMatrix) %in% rankedNames,]
      if(class(subsetCor) != "numeric") {
        corSums <- rowSums(subsetCor)
        corSumRank <- rank(corSums)
        lowestCorNames <- names(corSumRank)[corSumRank <= stepSize]
        nameList[[rankCount]] <- lowestCorNames
        rankList[[rankCount]] <- rep(rankCount, min(stepSize, length(lowestCorNames)))
      } else { #1 name remaining
        nameList[[rankCount]] <- rownames(corMatrix)[!rownames(corMatrix) %in% names(subsetCor)]
        rankList[[rankCount]] <- rankCount
      }
    } else {  #first iteration, subset on first name
      subsetCorRank <- rank(subsetCor)
      lowestCorNames <- names(subsetCorRank)[subsetCorRank <= stepSize]
      nameList[[rankCount]] <- lowestCorNames
      rankList[[rankCount]] <- rep(rankCount, min(stepSize, length(lowestCorNames)))
    }    
    rankedNames <- do.call(c, nameList)
  }
  
  ranks <- do.call(c, rankList)
  names(ranks) <- rankedNames
  if(bestHighestRank) {
    ranks <- 1+length(ranks)-ranks
  }
  ranks <- ranks[colnames(corMatrix)] #return to original order
  return(ranks)
}

So the way the function works is that it takes in a correlation matrix, a starting name (if provided), and a step size (that is, how many assets to select per step, so that the process doesn’t become extremely long when dealing with larger amounts of assets/features). Then, it iterates–subset the correlation matrix on the starting name, and find the minimum value, and add it to a list of already-selected names. Next, subset the correlation matrix columns on the selected names, and the rows on the not selected names, and repeat, until all names have been accounted for. Due to R’s little habit of wiping out labels when a matrix becomes a vector, I had to write some special case code, which is the reason for two nested if/else statements (the first one being for the first column subset, and the second being for when there’s only one row remaining).

Also, if there’s an edge case (1 or 2 securities), then there is some functionality to handle those trivial cases.

Here’s a test script I wrote to test this function out:

require(PerformanceAnalytics)
require(quantmod)

#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))

adRets <- Return.calculate(adPrices)

subset <- adRets["2012"]
corMat <- cor(subset)

tmp <- list()
for(i in 1:length(mutualFunds)) {
  rankRow <- stepwiseCorRank(corMat, startNames=mutualFunds[i])
  tmp[[i]] <- rankRow
}
rankDemo <- do.call(rbind, tmp)
rownames(rankDemo) <- mutualFunds
origRank <- rank(rowSums(corMat))
rankDemo <- rbind(rankDemo, origRank)
rownames(rankDemo)[8] <- "Original (VBMFX)"

heatmap(-rankDemo, Rowv=NA, Colv=NA, col=heat.colors(8), margins=c(6,6))

Essentially, using the 2012 year of returns for the 7 FAA mutual funds, I compared how different starting securities changed the correlation ranking sequence.

Here are the results:

               VTSMX FDIVX VEIEX VFISX VBMFX QRAAX VGSIX
VTSMX              1     6     7     4     2     3     5
FDIVX              6     1     7     4     2     5     3
VEIEX              6     7     1     4     2     3     5
VFISX              2     6     7     1     3     4     5
VBMFX              2     6     7     4     1     3     5
QRAAX              5     6     7     4     2     1     3
VGSIX              5     6     7     4     2     3     1
Non-Sequential     5     6     7     2     1     3     4

In short, the algorithm is rather robust to starting security selection, at least judging by this small example. However, comparing VBMFX start to the non-sequential ranking, we see that VFISX changes from rank 2 in the non-sequential to rank 4, with VTSMX going from rank 5 to rank 2. From an intuitive perspective, this makes sense, as both VBMFX and VFISX are bond funds, which have a low correlation with the other 5 equity-based mutual funds, but a higher correlation with each other, thus signifying that the algorithm seems to be working as intended, at least insofar as this small example demonstrates. Here’s a heatmap to demonstrate this in visual form.

The ranking order (starting security) is the vertical axis, and the horizontal are the ranks, from white being first, to red being last. Notice once again that the ranking orders are robust in general (consider each column of colors descending), but each particular ranking order is unique.

So far, this code still has to be tested in terms of its applications to portfolio management and asset allocation, but for those interested in such an idea, it’s my hope that this provides a good reference point.

Thanks for reading.

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.

Structural “Arbitrage”: Trading the Equity Curve

The last post demonstrated that far from being a world-beating, absolutely amazing strategy, that Harry Long’s Structural “Arbitrage”, was in fact a very risky strategy whose drawdowns were comparable to that of the S&P 500 itself during the financial crisis. Although the annualized returns were fairly solid, the drawdowns themselves were in the realm of unacceptable. One low-hanging fruit that came to mind to try and improve the performance of the strategy is to trade the equity curve of the strategy with an SMA. Some call the 200-day SMA (aka 10 month) strategy the “Ivy” strategy, after Mebane Faber’s book, that I recommend anyone give a read-through.

In any case, picking up where the last post left off, I decided to use the returns of the strategy using the 60/40 non-adjusted TLT (that is, the simple returns on the close of TLT)-XIV configuration.

Here’s the continuation of the script:

applyWeeklySMA <- function(rets, n=200) {
  cumRets <- cumprod(1+rets)
  sma <- SMA(cumRets, n=n)
  smaCrosses <- xts(rep(NA, length(sma)), order.by=index(cumRets))
  smaCrosses[cumRets > sma & lag(cumRets) < sma] <- 1
  smaCrosses[cumRets < sma & lag(cumRets) > sma] <- 0
  smaCrosses <- na.locf(smaCrosses)
  weights <- xts(rep(NA, length(sma)), order.by=index(cumRets))
  weights[endpoints(sma, "weeks")] <- smaCrosses[endpoints(sma, "weeks")]
  weights <- lag(weights)
  weights <- na.locf(weights)
  weights[is.na(weights)] <- 1
  weightedRets <- rets*weights
  return(weightedRets)
}

tmp <- list()
for(i in seq(from=100, to=200, by=20)) {
  tmp[[i]] <- applyWeeklySMA(stratTest, n=i)
}
tmp <- do.call(cbind, tmp)
colnames(tmp) <- paste0("SMA_", seq(from=100, to=200, by=20))
origStratAsBM <- merge(tmp, stratTest)
colnames(origStratAsBM)[7] <- "No_SMA"
charts.PerformanceSummary(origStratAsBM, colorset=c("black", "blue", "red", "orange", "green", "purple", "darkgray"), 
                          main="SMAs and original strategy")

Return.annualized(origStratAsBM)
SharpeRatio.annualized(origStratAsBM)
maxDrawdown(origStratAsBM)

returnRisk <- data.frame(t(rbind(Return.annualized(origStratAsBM), maxDrawdown(origStratAsBM))))
chart.RiskReturnScatter(R=returnRisk, method="nocalc", add.sharpe=NA, main=NA)

The first function simply applies an n-day SMA (default 200), and stays in the strategy for a week if the Friday’s close is above the SMA, and starts off in the strategy on day 1 (by contrast, an MA crossover strategy in quantstrat would need to actually wait for the first positive crossover). The rest of it is just getting the returns. Essentially, it’s a very simplified example of what quantstrat does. Of course, none of the trading analytics are available through this function, though since it’s in returns space, all return analytics can be done quite simply.

In any case, here is the performance chart corresponding to testing six different MA settings (100, 120, 140, 160, 180, 200) and the benchmark (no filter)

The gray (original strategy) is basically indistinguishable from the MA filters from a return perspective. In fact, applying the MA filter in many cases results in lower returns.

What do the annualized metrics tell us?

> Return.annualized(origStratAsBM)
                    SMA_100   SMA_120   SMA_140   SMA_160   SMA_180   SMA_200    No_SMA
Annualized Return 0.1757805 0.1923969 0.1926832 0.2069332 0.1850422 0.2291408 0.2328424
> SharpeRatio.annualized(origStratAsBM)
                                  SMA_100   SMA_120   SMA_140   SMA_160   SMA_180  SMA_200    No_SMA
Annualized Sharpe Ratio (Rf=0%) 0.8433103 0.9143868 0.9169305 0.9769476 0.8839841 1.058095 0.8780168
> maxDrawdown(origStratAsBM)
                 SMA_100   SMA_120   SMA_140   SMA_160   SMA_180   SMA_200    No_SMA
Worst Drawdown 0.5044589 0.4358926 0.4059265 0.3943257 0.4106122 0.3886326 0.5040189

Overall, the original strategy has the highest overall returns, but pays for the marginally higher returns with even higher marginal drawdowns. So, the basic momentum filter marginally improved the strategy. Here’s another way to look at that sentiment using a modified risk-return chart (by default, it takes in returns and charts annualized return vs. annualized standard deviations, for the portfolio management world out there).

In short, none of the configurations really turned the lemons that was the massive drawdown into lemonade. At best, you had around 40% drawdown, which is still very much in the realm of unacceptable. While the drawdowns are certainly very high, overall, the reward to risk in terms of maximum drawdown is still pedestrian, especially when considering that the worst drawdowns can last for years. After all, given a system with small returns but smaller drawdowns still, such a system can simply be leveraged to obtain the proper reward to risk profile as per the risk appetite of a given investor.

Overall, I’ll wrap up this investigation here. What initially appeared to be a very interesting strategy from Seeking Alpha instead simply showed results for a particularly short period of time. While this longer period of time may not be long enough for some people’s tests, it covers both up markets and down markets. Overall, while the initial replication looked promising, looking over a longer time horizon painted a much different picture. Now it’s time to move on to replicating other ideas.

Thanks for reading.

Structural “Arbitrage”: a Working Long-History Backtest

For this post, I would like to give my sincere thanks to Mr. Helmuth Vollmeier, for providing the long history daily data of XIV. It is tremendously helpful. Also, I put arbitrage in quotations now, for reasons we’ll see in this post.

To begin, here’s a script I wrote to create this backtest.

require(downloader)
download("https://dl.dropboxusercontent.com/s/jk6der1s5lxtcfy/XIVlong.TXT",
         destfile="longXIV.txt")
XIV <- read.csv("longXIV.txt", header=TRUE, stringsAsFactors=FALSE)
head(XIV)
XIV <- xts(XIV[,2:5], order.by=as.Date(XIV$Date))
XIVrets <- Return.calculate(Cl(XIV))
getSymbols("TLT", from="1990-01-01")
TLTrets <- Return.calculate(Cl(TLT))
adTltRets <- Return.calculate(Ad(TLT))
both <- merge(XIVrets, TLTrets, join='inner')
bothAd <- merge(XIVrets, adTltRets, join='inner')
stratTest <- Return.rebalancing(both, weights=c(.4, 1.8),
                                rebalance_on="weeks", geometric=FALSE)
adStratTest <- Return.rebalancing(bothAd, weights=c(.4, 1.8),
                                  rebalance_on="weeks", geometric=FALSE)
bothStrats <- merge(stratTest, adStratTest)
colnames(bothStrats) <- c("Close TLT", "Adjusted TLT")
getSymbols("SPY", from="1990-01-01")
ClSPY <- Return.calculate(Cl(SPY))
AdSPY <- Return.calculate(Ad(SPY))
SPYs <- cbind(ClSPY, AdSPY)
stratsAndBMs <- merge(bothStrats, SPYs, join='inner')
charts.PerformanceSummary(stratsAndBMs)

First of all, in order to download files that start off with the https stem, users will need to install the “downloader” package from CRAN. So a simple

install.packages("downloader")

will work just fine, and a thank you to Winston Chang for this package.

Beyond this, the way to turn a data frame to an xts (xts objects are the foundation of almost all serious financial analysis in R) is to pass in a data frame object along with a recognized format for a date. The default date format in R is “yyyy-mm-dd”, while something like 02/20/2014 would be “%mm/%dd/%yyyy”.

After this point, the syntax is the standard fare for computing returns, joining return series, and creating a summary chart. I used both the close and the adjusted price of 3x leveraged TLT (not an absolute replication of TMF, but conceptually very similar), in order to satisfy both the close-price and adjusted-price camps when dealing with return data. I myself prefer close prices, rather than making assumptions about dividend reinvestment, though sometimes splits force the issue.

Here’s a quick glance at the performance comparisons.

While the equity curves for the strategies look good (adjusted in red, close in black), what concerns me more is the drawdown plot. As can be seen, this strategy would have resulted in a protracted and severe drawdown from 2007 through 2010, hitting around 50% total drawdown, which should be far beyond the risk tolerance of…just about anyone. In short, this is far from any arbitrage. From a hypothesis standpoint, if someone were indeed to say “short volatility”, one would expect to see drawdowns in some form in the financial crisis. However, the drawdowns for this strategy are on par with that of the S&P 500 itself, which is to say, pretty bad.

Here’s a closer look at 2007-2010 for the strategies and the corresponding S&P 500 (close returns in green, adjusted in blue):

Basically, the performance is barely distinguishable form the S&P 500 at its worst, which makes this far from an effective strategy at its worst.

Here are the risk/return metrics for the strategies with the benchmarks for comparison:

> Return.annualized(stratsAndBMs)
                  Close.TLT Adjusted.TLT  SPY.Close SPY.Adjusted
Annualized Return 0.2328424    0.3239631 0.05336649    0.0748746
> SharpeRatio.annualized(stratsAndBMs)
                                Close.TLT Adjusted.TLT SPY.Close SPY.Adjusted
Annualized Sharpe Ratio (Rf=0%) 0.8780168     1.226562 0.2672673    0.3752571
> maxDrawdown(stratsAndBMs)
               Close.TLT Adjusted.TLT SPY.Close SPY.Adjusted
Worst Drawdown 0.5040189    0.4256037 0.5647367    0.5518672

Here are the return, drawdown, and Sharpe ratio statistics by year:

> apply.yearly(stratsAndBMs, Return.cumulative)
             Close.TLT Adjusted.TLT    SPY.Close SPY.Adjusted
2004-12-31  0.43091127   0.53673252  0.073541167   0.09027015
2005-12-30  0.38908539   0.50726218  0.030115000   0.04834811
2006-12-29  0.20547671   0.30869571  0.137418681   0.15843582
2007-12-31 -0.12139177  -0.04277199  0.032410676   0.05142241
2008-12-31  0.02308329   0.10593220 -0.382805554  -0.36791039
2009-12-31 -0.15364860  -0.09427527  0.234929078   0.26344690
2010-12-31  0.64545635   0.76914182  0.128409907   0.15053339
2011-12-30  0.37738081   0.47880348 -0.001988072   0.01897321
2012-12-31  0.55343030   0.62319271  0.134741036   0.15991238
2013-12-31  0.01191596   0.06800805  0.296889263   0.32309145
2014-10-01  0.38674137   0.44448550  0.052303861   0.06697777
> apply.yearly(stratsAndBMs, maxDrawdown)
           Close.TLT Adjusted.TLT  SPY.Close SPY.Adjusted
2004-12-31 0.1626657    0.1508961 0.07304589   0.06961279
2005-12-30 0.1578365    0.1528919 0.07321443   0.06960143
2006-12-29 0.2504912    0.2297468 0.07593123   0.07592093
2007-12-31 0.3289898    0.3006318 0.09924591   0.09921458
2008-12-31 0.4309851    0.4236635 0.48396143   0.47582236
2009-12-31 0.3833131    0.3668421 0.27131700   0.27123750
2010-12-31 0.1270308    0.1219816 0.16098842   0.15703744
2011-12-30 0.1628584    0.1627968 0.19423880   0.18608682
2012-12-31 0.1123245    0.1054862 0.09686971   0.09686039
2013-12-31 0.2840916    0.2782082 0.06047736   0.05550422
2014-10-01 0.1065488    0.1023469 0.05696031   0.05698600
> apply.yearly(stratsAndBMs, SharpeRatio.annualized)
             Close.TLT Adjusted.TLT    SPY.Close SPY.Adjusted
2004-12-31  2.90726854    3.7072225  0.893324847   1.10342092
2005-12-30  1.96324189    2.5862100  0.291120531   0.46836705
2006-12-29  0.95902528    1.4533427  1.369071018   1.58940411
2007-12-31 -0.47792925   -0.1693763  0.205060111   0.32457756
2008-12-31  0.07389051    0.3388196 -0.925155310  -0.88807262
2009-12-31 -0.45741108   -0.2815325  0.879806802   0.98927701
2010-12-31  2.31270808    2.7875988  0.714706742   0.83968381
2011-12-30  1.29489799    1.6371371 -0.008639479   0.08243543
2012-12-31  2.12645653    2.3967509  1.061570060   1.26650058
2013-12-31  0.04205873    0.2408626  2.667267167   2.91640716
2014-10-01  2.54201473    2.9678436  0.683911514   0.88274606

In short, when the strategy is good, it’s terrific. But when it’s bad, it’s terrible. Furthermore, even in good years, the drawdowns are definitely eye-popping, on the order of 10-15% when things are going smoothly, and anywhere between 25%-43% drawdown when they don’t, and those may not paint the whole story, either, as those are single-year max drawdowns, when one drawdown could have spanned years (which it did, in the financial crisis), getting worse than 50%. Indeed, far from an arbitrage, this strategy seems to be a bet on substantial returns usually, with painful drawdowns when incorrect.

For the record, here is the correlation table between the strategy and the benchmark:

> cor(stratsAndBMs)
             Close.TLT Adjusted.TLT SPY.Close SPY.Adjusted
Close.TLT    1.0000000    0.9970102 0.2366392    0.2378570
Adjusted.TLT 0.9970102    1.0000000 0.2384042    0.2395180
SPY.Close    0.2366392    0.2384042 1.0000000    0.9987201
SPY.Adjusted 0.2378570    0.2395180 0.9987201    1.0000000

Of course, this does not mean that the strategy is pure alpha due to the low correlation with the S&P 500, just that the S&P may not be the greatest benchmark to measure it against–after all, this strategy carries a massive amount of risk in its raw form as posted by Harry Long on Seeking Alpha.

Thanks for reading.

A Failed Attempt at Backtesting Structural Arbitrage

One of the things that I wondered about regarding the previous post was how would this strategy have performed in the past, before the inception of XIV?

My first go-around involved me backtesting on the actual VIX index. Unfortunately, there is no instrument that actually perfectly tracks the VIX (EG ala SPY vs. the S&P 500 index). So, one common pitfall with trying to backtest VIX-type strategies is to actually assume that there’s a straightforward proxy to the VIX index. There isn’t. Why? Because of this:


#VIX weekend always up
getSymbols(“^VIX”, from=“1990-01-01”)
VIX <- to.weekly(VIX, OHLC=TRUE)
VIXwknd <- Op(VIX)/lag(Cl(VIX)) - 1
charts.PerformanceSummary(VIXwknd)

Obviously this equity curve is completely unrealistic, meaning it’s impossible to trade an exact replica of the instrument that would have created it.

However, for those seeking some data on VIX futures (that is, the instrument you could trade), on the front month contract, I recently updated my quandClean function in my IKTrading package, as the CBOE_VX contract actually has closing price data (in addition to settle price), so I was able to pull VIX futures data from Quandl. So, if you wish to replicate this analysis, update your installation of IKTrading. Here’s the code:


require(IKTrading)
VIX <- quandClean(“CHRIS/CBOE_VX”, verbose=TRUE)
vix2 <- VIX[“2007-03-26::”]
vix1 <- VIX[“::2007-03-23”]
vix1[,1:4] <- vix1[,1:4]/10
VIX <- rbind(vix1, vix2)
chart_Series(VIX) #2008-05-20 low is wrong
getSymbols(“TLT”, from=“1990-01-01”)
vixRets <- Return.calculate(prices=Cl(VIX))
tltRets <- Return.calculate(prices=Cl(TLT))
both <- merge(vixRets, tltRets, join='inner')
colnames(both) <- c(“vix”, “tlt”)
longRets <- Return.rebalancing(both, weights=c(-.4, 1.8),
rebalance_on=“weeks”, geometric=FALSE)
colnames(longRets) <- c(“VIXTLT”)
charts.PerformanceSummary(longRets)

A quick explanation: Quandl’s VIX data prior to March 26, 2007 is on an order of magnitude larger than the data following it. Why this is the case, I do not know, but whatever the case may be, in order to proceed with the analysis, I divided that section of the data by 10. Still, Quandl’s data quality isn’t the greatest for other instruments, however, and I hope that they take a look at my updated algorithm for their futures data so as to improve its quality, and thereby make it more amenable to backtesting.

In any case, in this instance, rather than long XIV 40% and long TMF 60%, it was long TLT 180% and short VIX 40%.

Here’s the result:

In short, an unambiguous loser. Can we see why? Well, not completely, as XIV doesn’t go back to 2003 (or I’d be able to conduct the back-cast in a fairly straightforward fashion), but here’s an attempt to replicate XIV with transforms on the VIX (both short and inverting)

vixxiv <- merge(vixRets, xivRets, join='inner')
vixxiv$shortVix <- -1*vixxiv[,1]
vixxiv$inverseVix <- 1/(1+vixxiv[,1])-1
charts.PerformanceSummary(vixxiv[,2:4])

Here are the results:

Basically, even with the VX futures from the CBOE, it’s far from straightforward to get a replica of XIV in order to properly backtest the strategy, and without a stream of returns that matches that of XIV, it is very difficult to say how this particular strategy would have played out during the financial crisis when VIX spiked. In any case, while this is hardly evidence of a failed thesis on the part of the original article’s author, it’s difficult to actually verify the validity of a strategy that has less than four years of data.

Thanks for reading.