Quant BuffetRelax, Not Over Thinking

Trading VIX ETFs v2

Log in to collect

Academic 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

Annualised return69%
Volatility39%
Beta-0.243
Sharpe ratio2.11
Sortino ratio0.239
Maximum drawdown-24.5%
Win rate53%

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