Volatility Stat-Arb Shenanigans

This post deals with an impossible-to-implement statistical arbitrage strategy using VXX and XIV. The strategy is simple: if the average daily return of VXX and XIV was positive, short both of them at the close. This strategy makes two assumptions of varying dubiousness: that one can “observe the close and act on the close”, and that one can short VXX and XIV.

So, recently, I decided to play around with everyone’s two favorite instruments on this blog–VXX and XIV, with the idea that “hey, these two instruments are diametrically opposed, so shouldn’t there be a stat-arb trade here?”

So, in order to do a lick-finger-in-the-air visualization, I implemented Mike Harris’s momersion indicator.

momersion <- function(R, n, returnLag = 1) {
  momentum <- sign(R * lag(R, returnLag))
  momentum[momentum < 0] <- 0
  momersion <- runSum(momentum, n = n)/n * 100
  colnames(momersion) <- "momersion"
  return(momersion)
}

And then I ran the spread through it.


xiv <- xts(read.zoo("longXIV.txt", format="%Y-%m-%d", sep=",", header=TRUE))
vxx <- xts(read.zoo("longVXX.txt", format="%Y-%m-%d", sep=",", header=TRUE))

xivRets <- Return.calculate(Cl(xiv))
vxxRets <- Return.calculate(Cl(vxx))

volSpread <- xivRets + vxxRets
volSpreadMomersion <- momersion(volSpread, n = 252)
plot(volSpreadMomersion)

In other words, this spread is certainly mean-reverting at just about all times.

And here is the code for the results from 2011 onward, from when the XIV and VXX actually started trading.

#both sides
sig <- -lag(sign(volSpread))
longShort <- sig * volSpread
charts.PerformanceSummary(longShort['2011::'], main = 'long and short spread')

#long spread only
sig <- -lag(sign(volSpread))
sig[sig < 0] <- 0
longOnly <- sig * volSpread
charts.PerformanceSummary(longOnly['2011::'], main = 'long spread only')


#short spread only
sig <- -lag(sign(volSpread))
sig[sig > 0] <- 0
shortOnly <- sig * volSpread
charts.PerformanceSummary(shortOnly['2011::'], main = 'short spread only')

threeStrats <- na.omit(cbind(longShort, longOnly, shortOnly))["2011::"]
colnames(threeStrats) <- c("LongShort", "Long", "Short")
rbind(table.AnnualizedReturns(threeStrats), CalmarRatio(threeStrats))

Here are the equity curves:

Long-short:

Long-only:

Short-only:

With the following statistics:

                          LongShort      Long    Short
Annualized Return          0.115400 0.0015000 0.113600
Annualized Std Dev         0.049800 0.0412000 0.027900
Annualized Sharpe (Rf=0%)  2.317400 0.0374000 4.072100
Calmar Ratio               1.700522 0.0166862 7.430481

In other words, the short side is absolutely amazing as a trade–except for the one small fact of having it be impossible to actually execute, or at least as far as I’m aware. Anyhow, this was simply a for-fun post, but hopefully it served some purpose.

Thanks for reading.

NOTE: I am currently contracting and am looking to network in the Chicago area. You can find my LinkedIn here.

25 thoughts on “Volatility Stat-Arb Shenanigans

  1. You may be able to implement that strategy in practice if you semd the order a few seconds before the close. Now, if you add borrow costs, plus transaction costs, plus bid ask spread, not sure you will have such a return. If you do 100 trades per year, and the total cost per transaction is 5bps, your return will go to 1.5%…

    I believe what you are actually capturing here is variations of the price of the ETF vs their NAV, which is naturally readjusted the following day? Then you can do a similar trade with two different ETF that track the same index: long one and short the other. If there is a return that day in one direction, take the oposite direction the following day, and here you have another strategy with great Sharpe ratio… You go to bed thinking you are a future millionaire, and then you remember to add the costs… Damn it!

  2. Pingback: Quantocracy's Daily Wrap for 10/09/2015 | Quantocracy

  3. Hi,

    Judging (only) by a one line remark in the following article, it looks like this might be the exact strategy that blew up
    Spruce Alpha in August:

    “The Spruce Alpha Fund seeks to generate high alpha, low beta, and low correlation returns
    by identifying daily-resetting, highly-levered ETFs experiencing volatility decay, and shorting
    them in bull and bear pairs,” the fund’s performance reports state.”
    http://www.businessinsider.com/spruce-alpha-loses-48-percent-2015-10

    One possible explanation is that, during peak chaos in the August 24 premarket/just-after-open/maybe-later, for whichever vix long/short etf pair they were double-shorting, the etf autorized participants weren’t able to arbitrage them into line (at least with each other), which caused Spruce Alpha’s double unrealized P&L to become a large enough loss that their maximum risk and/or loss limits were triggered, and they liquidated at a loss.

    This pdf from BlackRock goes into details about how and why many etfs went way out of whack at various points on Aug24. Basically, all the halts, heavy market order pressure (stops, panic, etc), and illiquidity converged to cause chaos in various etfs for nontrivial periods of time. The pdf:

    http://www.blackrock.com/corporate/en-us/literature/whitepaper/viewpoint-us-equity-market-structure-october-2015.pdf

    So finally, my comment to you is, it would be very interesting to see the PerformanceSummary(longShort) at 1m (or whatever is needed) resolution, AND that uses the low values (instead of close). Not sure if you have this data, and maybe some pairs (when i say pair, i mean a long/short vix etf pair) had more divergence than others on Aug24, so not sure if VXX/XIV was the one they (Spruce Capital) was in (maybe UVXY/SVXY, etc).

    Maybe it would be possible to see if this is how they blew up? If the max drawdown is >=-48% for some bar, that just might be Spruce Capital getting the chop.

  4. Oh wait right, there’s no need for higher resolution data if this hypothetical event occurred during normal market hours. Just using the “low” rather than “close” data in the return ohlc data would suffice.

  5. Wow so I sucked it up and did it, the UVXY/SVXY pair could be how it happened. Here is adaptation of your code for UVXY/SVXY, and only plotting the “longShort” (Spruce Alpha’s double short position).

    Just under -50% drawdown across the +/- 1 week window round Augut 24.

    (plot) http://i.imgur.com/FAZz9t2.png
    (src) http://sprunge.us/JDKZ

    require(quantmod)
    require(PerformanceAnalytics)
    UVXY <- getSymbols("UVXY", from="2015-08-01", to="2015-09-01", auto.assign=FALSE)
    SVXY <- getSymbols("SVXY", from="2015-08-01", to="2015-09-01", auto.assign=FALSE)
    UVXYRets <- Return.calculate(Cl(UVXY))
    SVXYRets <- Return.calculate(Cl(SVXY))
    volSpread <- UVXYRets + SVXYRets
    sig <- -lag(sign(volSpread))
    longShort <- sig * volSpread
    charts.PerformanceSummary(longShort, main = 'long and short spread')

    • Not sure that was the pair, since it was always horrible historically. If you take that strategy and use the adjusted prices, you’ll see that it gets chopped up for basically no returns over time.

      But yes, any given pair can certainly come unglued–and usually, it’s not just one pair at once, if many people follow similar models.

  6. Aargh wait, I made a mistake here.

    SVXY is only a 1x ETF, not 2x like UVXY.

    So taking that into account only leads to a -10% drawdown, rather than a -50%, across the end of August.

  7. Pingback: IMHO BEST LINKS FROM QUANTOCRACY FOR THE WEEK 5 OCT 15 — 11 OCT 15 | Quantitative Investor Blog

  8. These days, going long VIX is betting against policies of central banks. Selling vix can help raise equity prices (inline with some central banks’ policy makers’ views). Gone are the days of relatively good price discovery…

  9. Pingback: Implementations of the Momersion Indicator | Price Action Lab Blog

  10. Pingback: Best Links of the Week | Quantocracy

  11. Hi Ilya,

    Thank you for an alternative implementation of momersion. There is one discrepancy though:

    If we take the original definition of Momersion as
    Mc = Count of all r(i) × r(i-1) > 0
    MRc = Count of all r(i) × r(i-1) < 0
    Momersion(n) = 100 × Mc/(Mc+MRc)

    And compare it to your implementation
    momentum <- sign(R * lag(R, returnLag))
    momentum[momentum < 0] <- 0
    momersion <- runSum(momentum, n = n)/n * 100

    We can see that in origial version if r(i) × r(i-1) == 0 we do NOT count it, while in your implementation it does count. Not a huge difference though and it does converge to original version quickly.

  12. You can replicate the strategy with a rolling basket of futures. Assuming the futures trade at $20 (average) it requires a minimum notional of $20 * 30 futures * 1,000 multiplier = $600,000 per basket. As the strategy can go in your face for some time, say 10 baskets, and futures can climb say to $60, you may need a max capital of $18,000,000.
    You can perhaps reduce the basket to 5 futures, increasing further your tracking error.

Leave a reply to uiop Cancel reply