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)

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.

Structural Arb Analysis and Portfolio Management Functionality in R

I want to use this post to replicate an article I found on SeekingAlpha, along with demonstrating PerformanceAnalytics’s ability to do on-the-fly portfolio rebalancing, without having to rewrite return-tracking functionality yourself.

Recently, I read an article written by Harry Long about an ETF trading strategy that went long the XIV for 40% of the portfolio and 60% long TMF, rebalancing weekly. While I initially attempted to back-cast this strategy since before the inception of these two fairly recent ETFs, I’ll have to save that for another blog post. In any case, here’s the link to Mr. Long’s article.

While I have no opinion on where this strategy is going, here’s how to get a very close replication.

#long TMF 60% (leveraged 3x t-bond bull), long XIV 40%
getSymbols("XIV", from="1990-01-01")
getSymbols("TMF", from="1990-01-01")
tmfRets <- Return.calculate(Cl(TMF))
xivRets <- Return.calculate(Ad(XIV))
both <- merge(xivRets, tmfRets, join='inner')
colnames(both) <- c("xiv", "tmf")
portfRets <- Return.rebalancing(both, weights=c(.4, .6),
                               rebalance_on="weeks", geometric=FALSE)
colnames(portfRets) <- "XIVTMF"
getSymbols("SPY", from="1990-01-01")
SPYrets <- diff(log(Cl(SPY)))
charts.PerformanceSummary(merge(portfRets, SPYrets, join='inner'))

Although I dislike using adjusted prices, I was forced to in the case of XIV due to its 1:10 split.

The line to pay attention to (that I recently learned about) is the call to Return.rebalancing. The allocate and drift cycle mechanism is perhaps the foundation of quantitative portfolio management. That is, the “here are my weights at the end of March, what are my returns through June? And then after reallocating at the beginning of July, how do I compute returns through September?” and so on. Suffice to say, this one line takes care of all of that. The single line of weights coupled with the rebalance_on argument specifies a consistently rebalanced portfolio at the specified time frequency. If the user has a series of evolving weights (E.G. from portfolio optimization), then that xts should be passed into the weights argument, and the rebalance_on argument left as its default NA, which would then let the function rebalance at the specified time intervals.

So, how does our replication do?

Here’s the output from the performance charts.

The strategy’s equity curve looks highly similar to the second equity curve from the article, essentially showing a very close if not dead on replication.

And here are the portfolio statistics with SPY for comparison over the same period:

> SharpeRatio.annualized(merge(portfRets, SPYrets, join='inner'))
                                portfolio.returns SPY.Close
Annualized Sharpe Ratio (Rf=0%)          1.461949 0.8314073
> Return.annualized(merge(portfRets, SPYrets, join='inner'))
                  portfolio.returns SPY.Close
Annualized Return         0.3924696 0.1277928
> maxDrawdown(merge(portfRets, SPYrets, join='inner'))
               portfolio.returns SPY.Close
Worst Drawdown         0.2796041 0.2070681

In short, definitely a strong strategy. However, a rise in rates coupled with a spike in volatility (stagflation) would seriously hurt this particular configuration of this strategy, and the author, Harry Long, says as much in his article.

In any case, it isn’t every day that authors actually publish working strategies, especially ones that work as well as this one does. 28% drawdown does seem a bit rough, but on a whole, this is definitely evidence that the author knows what he’s talking about, given when he published the book first outlining this (or a similar, haven’t read it) strategy, as this is effectively all out-of-sample performance, if this is the same strategy suggested in the book. However, what’s even more impressive is that the author’s article was as clear as it was, and was able to be replicated so closely. I strive to provide the same clarity in my trading posts, which is why I include all the code and data I use to obtain my results. Additionally, it helps that the R xts/zoo->PerformanceAnalytics/quantstrat/PortfolioAnalytics libraries contain code that is more easily read.

In the future, I’ll go into some of the pitfalls of trying to back-cast this strategy, and a possible solution I may have found through quandl data.

Thanks for reading.

Nuts and Bolts of Quantstrat, Part IV

This post will provide an introduction to the way that rules work in quantstrat. It will detail market orders along with order-sizing functions (limit orders will be saved for a later date). After this post, readers should be able to understand the strategies written in my blog posts, and should be able to write their own. Unlike indicators and signals, rules usually call one function, which is called “ruleSignal” (there is a function that is specifically designed for rebalancing strategies, but it’s possible to do that outside the bounds of quantstrat). For all intents and purposes, this one function handles all rule executions. However, that isn’t to say that rules cannot be customized, as the ruleSignal function has many different arguments that can take in one of several values, though not all permutations will be explored in this post. Let’s take a look at some rules:

#rules
if(atrOrder) {
  
  add.rule(strategy.st, name="ruleSignal", 
           arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                          orderside="long", replace=FALSE, prefer="Open", 
                          osFUN=osDollarATR, tradeSize=tradeSize, 
                          pctATR=pctATR, atrMod="X"), 
           type="enter", path.dep=TRUE)
} else { 
  add.rule(strategy.st, name="ruleSignal", 
           arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                          orderside="long", replace=FALSE, prefer="Open", 
                          osFUN=osMaxDollar, tradeSize=tradeSize, maxSize=tradeSize), 
           type="enter", path.dep=TRUE)
}

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", 
                        ordertype="market", orderside="long", 
                        replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="filterExit", sigval=TRUE, orderqty="all", 
                        ordertype="market", orderside="long", replace=FALSE, 
                        prefer="Open"), 
         type="exit", path.dep=TRUE)

In this case, the first thing to note is that as quantstrat is an R library, it can also incorporate basic programming concepts into the actual strategy formulation. In this case, depending on a meta-parameter (that is, a parameter not found in the argument of any indicator, signal, or rule) called atrOrder (a boolean), I can choose which rule I wish to add to the strategy configuration.

Next, here’s the format for adding a rule:

1) The call to add.rule
2) The name of the strategy (strategy.st)
3) The name of the strategy function (this is usually “ruleSignal”)
4) The arguments to ruleSignal:

a) The signal column (sigCol)
b) the value that signals a trigger (sigVal)
c) the order type (ordertype)
d) the order side (orderside)
e) to replace any other open signal (replace)
f) The order quantity (orderqty) is no order-sizing function is used.
g) the preferred price (prefer, defaults to Close, but as quantstrat is a next-bar system, I use the open)
h) the order sizing function (osFUN)
i) the arguments to the order-sizing function.
j) There are other arguments to different order types, but we’ll focus on market orders for this post.

5) The rule type (type), which will comprise either “enter” or “exit” for most demos
6) The path.dep argument, which is always TRUE
7) (Not shown) the label for the rule. If you’re interested in writing your demos as quickly as possible, these are not necessary if your entry and exit rules are your absolute final points of logic in your backtest. However, if you wish to look at your orders in detail, or use stop-losses/take-profit orders, then the rules need labels, as well.

While most of the logic to adding your basic rule is almost always boilerplate outside the arguments to ruleSignal, it’s the arguments to ruleSignal that allow users to customize rules.

The sigCol argument is a string that has the exact name of the signal column that you wish to use to generate your entries (or exits) from. This is the same string that went into the label argument of your add.signal function calls. In quantstrat, labels effectively act as logical links between indicators, signals, rules, and more.

The sigVal argument is what value to use to trigger rule logic. Since signal output (so far) is comprised of ones (TRUE) and zeroes (FALSE), I set my sigVal to TRUE. It is possible, however, to make a sigSum rule and then allow the sigVal argument to take other values.

The ordertype argument is the order type. For most of my demos that I’ve presented thus far, I’ve mostly used “market” type orders, which are the simplest. Market orders execute at the next bar after receiving the signal. They do not execute on the signal bar, but the bar after the signal bar. On daily data, this might cause some P/L due to gaps, but on intraday data, the open of the next bar should be very similar to the close of current bar. One thing to note is that using monthly data, quantstrat uses current-bar execution.

The orderside argument takes one of two values–“long” or “short”. This separates rule executions into two bins, such that long sells won’t work on short positions and vice versa. It also serves to add clarity and readability to strategy specifications.

The replace argument functions in the following way: if TRUE, it overrides any other signal on the same day. Generally, I avoid ever setting this to true, as order sets (not shown in this post) exist deliberately to control order replacement. However, for some reason, it defaults to TRUE in quantstrat, so make sure to set it to FALSE whenever you write a strategy.

The orderqty argument applies only when there’s no osFUN specified. It can take a flat value (E.G. 1, 2, 100, etc.), or, when the rule type is “exit”, a quantity of “all”, to flatten a position. In all the sell rules I use in my demos, my strategies do not scale out of positions, but merely flatten them out.

The prefer argument exists for specifying what aspect of a bar a trade will get in on. Quantstrat by default executes at the close of the next bar. I set this argument to “Open” instead to minimize the effect of the next bar transaction.

The osFUN specifies the order-sizing function to use. Unlike the functions passed into the name arguments in quantstrat (for indicators, signals, or rules), the osFUN argument is actually a function object (that is, it’s the actual function, rather than its name) that gets passed in as an argument. Furthermore, and this is critical: all arguments *to* the order-sizing function must be passed into the arguments for ruleSignal. They are covered through the ellipsis functionality that most R functions include. The ellipsis means that additional arguments can be passed in, and these additional arguments usually correspond to functions used inside the original function that’s called. This, of course, has the potential to violate the black-box modular programming paradigm by assuming users know the inner-workings of pre-existing code, but it offers additional flexibility in instances such as these. So, to give an example, in my entry rule that uses the osDollarATR order-sizing function, arguments such as pctATR and tradeSize are not arguments to the ruleSignal function, but to the osDollarATR function. Nevertheless, the point to pass them in when constructing a quantstrat strategy is in the arguments to ruleSignal.

If you do not wish to use an osFUN, simply use a flat quantity, such as 100, or if using exit type orders, use “all” to flatten a position.

Moving outside the arguments to ruleSignal, we have several other arguments:

The type argument takes one of several values–but “enter” and “exit” are the most basic. They do exactly as they state. There are other rule types, such as “chain” (for stop-losses), which have their own mechanics, but for now, know that “enter” and “exit” are the two basic rules you need to get off the ground.

The path.dep argument should always be TRUE for the ruleSignal function.

Finally, add.rule also contains a label argument that I do not often use in my demos, as usually, my rules are the last point of my logic. However, if one wants to do deeper strategy analysis using the order book, then using these labels is critical.

After adding rules, you can simply call applyStrategy and run your backtest. Here’s an explanation of how that’s done:

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

As an explanation, I enclose the applyStrategy call in some code to print how much time the backtest took. Generally, on these twelve years of daily data, a single market may take between several seconds to thirty seconds (if a strategy has hundreds of trades per market).

The next four lines essentially update the objects initialized in order of dependency: first the portfolio, then the account for a given date range (the duration of the backtest), and then compute the end equity.

This concludes the basic nuts and bolts of creating a basic nuts and bolts strategy in quantstrat. On this blog, when I make more use of other features, I’ll dedicate other nuts and bolts sections so that readers can use all of quantstrat’s features more efficiently.

Thanks for reading.

Nuts and Bolts of Quantstrat, Part III

This post will focus on signals in quantstrat.

In comparison to indicators, signals in quantstrat are far more cut-and-dry, as they describe the interaction of indicators with each other–whether that indicator is simply the close price (“Close”), or a computed indicator, there are only so many ways indicators can interact, and the point of signals is to provide the user with a way of describing these relationships–is one greater than another, is the concern only when the cross occurs, does the indicator pass above or below a certain number, etc.

Here’s the code that will provide the example for the demonstration, from the atrDollarComparison strategy:

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "sma"), relationship="gt"),
           label="filter")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),
           label="rsiLtThresh")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("filter", "rsiLtThresh"), cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=sellThresh,
                          relationship="gt", cross=TRUE),
           label="longExit")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "sma"), relationship="lt"),
           label="filterExit")

Adding signals to a strategy has a very similar format to adding indicators. The structure is very similar:

1) The call to add.signal
2) The name of the strategy (again, strategy.st makes this very simple)
3) The name of the signal function (the majority of which are on display in the preceding block of code)
4) The arguments to said signal function, passed in the same way they are to indicators (that is, arguments=list(args)), but which are far more similar compared to indicators
5) The label for the signal column, which is highly similar to the labeling for indicator columns.

The first two steps are identical to the add.indicator step, except with add.signal instead of add.indicator. This is cut and dry.

Beyond this, all of the signal functions I use are presented above. They are:

sigComparison, sigThreshold, sigAND, and sigCrossover.

The arguments for all four are very similar. They contain some measure of columns, a threshold, a relationship between the first and second column (or between the first column and the threshold), and whether the signal should return TRUE for the entire duration of the relationship being true, or only on the first day, with the cross argument.

Relationships are specified with a two or three character identifier: “gt” stands for greater than (E.G. SMA50 > SMA200), “gte” stands for greater than or equal to, “lt” and “lte” work similarly, and “eq” stands for equal to (which may be useful for certain logic statements such as “stock makes a new seven-day low”, which can be programmed by comparing the close to the running seven-day min, and checking for equality).

Here’s an explanation of all four sig functions:

The sigComparison function compares two columns, and will return TRUE (aka 1) so long as the specified relationship comparing the first column to the second holds. E.G. it will return 1 if you specify SMA50 > SMA200 for every timestamp (aka bar, for those using OHLC data) that the 50-day SMA is greater than the 200-day SMA. The sigComparison function is best used for setting up filters (EG the classic Close > SMA200 formation). This function takes two columns, and a relationship comparing the first to the second columns.

The sigCrossover is identical to the above, except only returns TRUE on the timestamp (bar) that the relationship moves from FALSE to TRUE. E.G. going with the above example, you would only see TRUE the day that the SMA50 first crossed over the SMA200. The sigCrossover is useful for setting up buy or sell orders in trend-following strategies.

The sigThreshold signal is identical to the two above signals (depending on whether cross is TRUE or FALSE), but instead uses a fixed quantity to compare one indicator to, passed in via the threshold argument. For instance, one can create a contrived example of an RSI buy order with a sigCrossover signal with an RSI indicator and an indicator that’s nothing but the same identical buy threshold all the way down, or one can use the sigThreshold function wherever oscillator-type indicators or uniform-value type indicators (E.G. indicators transformed with a percent rank), wherever all such indicators are involved.

Lastly, the sigAND signal function, to be pedantic, can also be called colloquially as sigIntersect. It’s a signal function I wrote (from my IKTrading package) that checks if multiple signals (whether two or more) are true at the same time, and like the sigThreshold function, can be set to either return all times that the condition holds, or the first day only. I wrote sigAND so that users would be able to structurally tie up multiple signals, such as an RSI threshold cross coupled with a moving-average filter. While quantstrat does have a function called sigFormula, it involves quoted code evaluation, which I wish to minimize as much as possible. Furthermore, using sigAND allows users to escalate the cross clause, meaning that the signals that are used as columns can be written as comparisons, rather than as crosses. E.G. in this RSI 20/80 filtered on SMA200 strategy, I can simply compare if the RSI is less than 20, and only generate a buy rule at the timestamp after both RSI is less than 20 AND the close is greater than its SMA200. It doesn’t matter whether the close is above SMA200 and the RSI crosses under 20, or if the RSI was under 20, and the close crossed above its SMA200. Either combination will trigger the signal.

One thing to note regarding columns passed as arguments to the signals: quantstrat will do its best to “take an educated guess” regarding which column the user attempts to refer to. For instance, when using daily data, the format may often be along the lines of XYZ.Open XYZ.High XYZ.Low XYZ.Close, so when “Close” is one of the arguments, quantstrat will make its best guess that the user means the XYZ.Close column. This is also, why, once again, I stress that reserved keywords (OHLC keywords, analogous tick data keywords) should not be used in labeling. Furthermore, unlike indicators, whose output will usually be something along the lines of FUNCTION_NAME.userLabel, labels for signals are as-is, so what one passes into the label argument is what one gets.

To put it together, here is the chunk of code again, and the English description of what the signals in the chunk of code do:

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "sma"), relationship="gt"),
           label="filter")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),
           label="rsiLtThresh")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("filter", "rsiLtThresh"), cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=sellThresh,
                          relationship="gt", cross=TRUE),
           label="longExit")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "sma"), relationship="lt"),
           label="filterExit")

1) The first signal checks to see if the “Close” column is greater than (“gt”) the “sma” column (which had a setting of 200), and is labeled “filter”.
2) The second signal checks to see if the “rsi” column is less than (“lt”) the threshold of buyThresh (which was defined earlier as 20), and is labeled as “rsiLtThresh”.
3) The third signal checks when both of the above signals became TRUE for the first time, until one or the other condition becomes false, and is labeled as “longEntry”. NB: the signals themselves do not place the order–I just like to use the label “longEntry” as this allows code in the rules logic to be reused quicker.
4) The fourth signal checks if the “rsi” column crossed over the sell threshold (80), and is labeled as “longExit”.
5) The fifth signal checks if the “Close” column crossed under the “sma” column, and is labeled “filterExit”.

In quantstrat, it’s quite feasible to have multiple signals generate entry orders, and multiple signals generate exit orders. However, make sure that the labels are unique.

The next post will cover rules.

Thanks for reading.