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.

9 thoughts on “Structural Arb Analysis and Portfolio Management Functionality in R

  1. Pingback: The Whole Street’s Daily Wrap for 9/30/2014 | The Whole Street

    • Paolo,

      Good comment. However, I do use geometric returns–that’s what the charts.PerformanceSummary charts do. So if I did it prior to then, I would be double-compounding.


      • Ilya,

        thanks for your comment. charts.PerformanceSummary is indeed using geometric chaining by default but then I don’t see the reason for calculating arithmetic ones in the first place.

        Furthermore I think you should use discrete returns for SPY rather than log in order to compare with portfRets. To make it clear return type has not to be confused with chaining method as explained in ?Return.calculate…therefore I would use discrete/simple returns with geometric chaining (which are actually the default methods as most of the other PerformanceAnalytics functions).

        As a last note please note that in the last Performance Analytics release Return.rebalancing as been included into Return.portfolio…not sure if it will keep on working going forward.

      • I’m calculating discrete returns to go from price to return space. Regarding Return.rebalancing/Return.portfolio, noted. I use Return.rebalancing at the moment, and if I ever need to do anything differently, I’ll let readers know.

  2. Ilya, your comment “if I did it prior to then, I would be double-compounding” made me doubtful…actually if we use geometric=FALSE in Return.rebalancing() we don’t get drifting weights between weeks (which is a desirable property for realistic results) but we would be double compounding indeed when passing the result to charts.PerformanceSummary(). What do you think about this?

    Btw you can check if weights are drifting by upgrading to the most recent PerformanceAnalytics version and running:

    portfRets <- Return.portfolio(both, weights=c(.4, .6), rebalance_on="weeks", geometric=FALSE)
    portfRetsGeom <- Return.portfolio(both, weights=c(.4, .6), rebalance_on="weeks", geometric=TRUE)
    portfRetsGeom $BOP.Weight

  3. After further checking I can confirm:
    1. Return.rebalancing and charts.PerformanceSummary work indipendently and therefore you are NOT double compounding by using geometric=TRUE for both…actually the parameter refers to drifting weights in Return.rebalancing() and the choice of the parameter name is a bit misleading
    2. you are using indeed discrete returns for the strategy but comparing with log returns for SPY.

  4. Pingback: It’s Amazing How Well Dumb Things [Get Marketed] | QuantStrat TradeR

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s