Trading VIX ETFs v2
Log in to collectAcademic paper
Strategy in a nutshell
The strategy trades SPY and SH for S&P 500 exposure and VXX and XIV for short-term VIX futures exposure. It calculates the relative difference between front-month VIX futures and spot VIX to identify contango or backwardation:
Contango (futures > spot): Buy XIV and hedge with SH.
Backwardation (futures < spot): Buy VXX and hedge with SPY.
Positions are closed when the relative basis crosses predetermined sell thresholds. The strategy performs best with a 0% hedge ratio, but alternative hedge levels can be applied to adjust risk-return profiles.
Economic rationale
VIX futures consistently trade at a premium to spot VIX because VIX is non-tradable and investors pay for volatility protection. Research (Simon & Campasano, 2014) shows that VIX futures prices revert toward the spot index: futures above VIX tend to fall, while those below tend to rise. This mispricing, driven by risk aversion and volatility hedging demand, creates persistent opportunities for systematic trading.
Backtest performance
Full Python code
from QuantConnect.Python import PythonQuandl
from AlgorithmImports import *
class TradingVIXETFsv2(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.vixy = self.AddEquity('VIXY', Resolution.Minute).Symbol
# Vix futures data.
self.vix_future = self.AddFuture(Futures.Indices.VIX, Resolution.Minute)
# Vix spot.
self.vix_spot = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
self.vix_future.SetFilter(timedelta(0), timedelta(30))
# Vix futures active contract updated on expiration.
self.active_contract = None
self.Schedule.On(self.DateRules.EveryDay(self.vixy), self.TimeRules.AfterMarketOpen(self.vixy, 1), self.Rebalance)
def Rebalance(self):
# split data error prevention
if self.Time.year == 2021 and self.Time.month == 5:
self.Liquidate()
return
if self.active_contract:
if self.Securities.ContainsKey(self.vix_spot):
spot_price = self.Securities[self.vix_spot].Price
vix_future_price = self.active_contract.LastPrice
if spot_price == 0 or vix_future_price == 0:
return
relative_basis = vix_future_price / spot_price
# BU 8%, SU 6%, BL -8%, SL -6% thresholds.
# Short volatility.
if relative_basis > 1.08:
if not self.Portfolio[self.vixy].IsShort and self.Securities[self.vixy].Price != 0:
self.SetHoldings(self.vixy, -1)
if relative_basis >= 1.06 and relative_basis <= 1.08 and self.Portfolio[self.vixy].IsLong:
self.Liquidate(self.vixy)
if relative_basis < 1.06 and relative_basis > 0.94:
if self.Portfolio[self.vixy].Invested:
self.Liquidate(self.vixy)
if relative_basis <= 0.94 and relative_basis >= 0.92 and self.Portfolio[self.vixy].IsShort:
self.Liquidate(self.vixy)
# Long volatility.
if not self.Portfolio[self.vixy].IsLong and relative_basis < 0.92:
if self.Securities[self.vixy].Price != 0:
self.SetHoldings(self.vixy, 1)
def OnData(self, slice):
chains = [x for x in slice.FutureChains]
cl_chain = None
if len(chains) > 0:
cl_chain = chains[0]
else:
return
if cl_chain.Value.Contracts.Count >= 1:
contracts = [i for i in cl_chain.Value]
contracts = sorted(contracts, key = lambda x: x.Expiry)
near_contract = contracts[0]
self.active_contract = near_contract