Quant BuffetRelax, Not Over Thinking

Contango-Backwardation VIX Futures Hedge Strategy

Log in to collect

Academic paper

Strategy in a nutshell

The trading strategy is using VIX futures as a trading vehicle and S&P mini for hedging purposes. The investor sells (buys) the nearest VIX futures with at least ten trading days to maturity when it is in contango (backwardation) with a daily roll greater than 0.10 (less than -0.10) points and holds it for five trading days, hedged against changes in the level of spot VIX by (long) short positions in E-mini S&P 500 futures. The daily roll is defined as the difference between the front VIX futures price and the VIX, divided by the number of business days until the VIX futures contract settles, and measures potential profits assuming that the basis declines linearly until settlement. The hedge ratios are constructed from regressions of VIX futures price changes on a constant and on contemporaneous percentage changes of the front mini-S&P 500 futures contract both alone and multiplied by the number of days to the settlement of the VIX futures contract (see equation 3 on page 12).

Economic rationale

Academic research states that volatility follows a mean-reverting process, which implies that the basis reflects the risk-neutral expected path of volatility. When the VIX futures curve is upward sloped (in contango), the VIX is expected to rise because it is low relative to long-run levels, as reflected by higher VIX futures prices. Likewise, when the VIX futures curve is inverted (in backwardation), the VIX is expected to fall because it is above its long-run levels, as reflected by lower VIX futures prices.

Backtest performance

Annualised return19.67%
Beta0.053
Sharpe ratio-0.153
Maximum drawdown94.8%
Win rate45%

Full Python code

import numpy as np
import pandas as pd
import statsmodels.api as sm
from collections import deque

class ExploitingTermStructureVIXFutures(XXX):

def Initialize(self):
self.SetStartDate(2011, 1, 1)
self.SetCash(100000)

self.vix = self.AddData(QuandlVix, "CBOE/VIX", Resolution.Daily).Symbol              # Add Quandl VIX price (daily)
self.vx1 = self.AddData(QuandlFutures, "CHRIS/CBOE_VX1", Resolution.Daily).Symbol    # Add Quandl VIX front month futures data (daily)
self.es1 = self.AddData(QuandlFutures, "CHRIS/CME_ES1", Resolution.Daily).Symbol     # Add Quandl E-mini S&P500 front month futures data (daily)

vx_data = self.AddFuture(Futures.Indices.VIX)
vx_data.SetFilter(timedelta(0), timedelta(days=180))
vx_data.MarginModel = BuyingPowerModel(5) # leverage

es_data = self.AddFuture(Futures.Indices.SP500EMini)
es_data.SetFilter(timedelta(0), timedelta(days=180))
es_data.MarginModel = BuyingPowerModel(5) # leverage

self.front_VX = None
self.front_ES = None

# request the history to warm-up the price and time-to-maturity 
hist = self.History([self.vx1, self.es1], timedelta(days=450), Resolution.Daily)
settle = hist['settle'].unstack(level=0)

# the rolling window to save the front month VX future price
self.price_VX = deque(maxlen=252)   
# the rolling window to save the front month ES future price
self.price_ES = deque(maxlen=252)   
# the rolling window to save the time-to-maturity of the contract
self.days_to_maturity = deque(maxlen=252) 

expiry_date = self.get_expiry_calendar()
df = pd.concat([settle, expiry_date], axis=1, join='inner')

for index, row in df.iterrows():
    self.price_VX.append(row[str(self.vx1) + ' 2S'])
    self.price_ES.append(row[str(self.es1) + ' 2S'])
    self.days_to_maturity.append((row['expiry']-index).days)
    
self.Schedule.On(self.DateRules.EveryDay(self.vix), self.TimeRules.AfterMarketOpen(self.vix), self.Rebalance)

def OnData(self, data):
# select the nearest VIX and E-mini S&P500 futures with at least 10 trading days to maturity 
# if the front contract expires, roll forward to the next nearest contract
for chain in data.FutureChains:
    future_indices = chain.Key.Value[1:] # First letter in this variable is '/'
    
    if future_indices == Futures.Indices.VIX:
        if self.front_VX is None or ((self.front_VX.Expiry-self.Time).days <= 1):
            contracts = list(filter(lambda x: x.Expiry >= self.Time + timedelta(days = 10), chain.Value))
            self.front_VX = sorted(contracts, key = lambda x: x.Expiry)[0]
    if future_indices == Futures.Indices.SP500EMini:
        if self.front_ES is None or ((self.front_ES.Expiry-self.Time).days <= 1):
            contracts = list(filter(lambda x: x.Expiry >= self.Time + timedelta(days = 10), chain.Value))
            self.front_ES = sorted(contracts, key = lambda x: x.Expiry)[0]

def Rebalance(self):
if self.Securities.ContainsKey(self.vx1) and self.Securities.ContainsKey(self.es1):
    # update the rolling window price and time-to-maturity series every day
    if self.front_VX and self.front_ES:
        self.price_VX.append(float(self.Securities[self.vx1].Price))
        self.price_ES.append(float(self.Securities[self.es1].Price))
        self.days_to_maturity.append((self.front_VX.Expiry-self.Time).days)
    
        # calculate the daily roll
        daily_roll = (self.Securities[self.vx1].Price - self.Securities[self.vix].Price)/(self.front_VX.Expiry-self.Time).days

        if not self.Portfolio[self.front_VX.Symbol].Invested:
            # Short if the contract is in contango with adaily roll greater than 0.10 
            if daily_roll > 0.1:
                hedge_ratio = self.CalculateHedgeRatio()
                self.SetHoldings(self.front_VX.Symbol, -0.4)
                self.SetHoldings(self.front_ES.Symbol, -0.4*hedge_ratio)
            # Long if the contract is in backwardation with adaily roll less than -0.10
            elif daily_roll < -0.1:
                hedge_ratio = self.CalculateHedgeRatio()
                self.SetHoldings(self.front_VX.Symbol, 0.4)
                self.SetHoldings(self.front_ES.Symbol, 0.4*hedge_ratio)
        
        # exit if the daily roll being less than 0.05 if holding short positions                 
        if self.Portfolio[self.front_VX.Symbol].IsShort and daily_roll < 0.05:
            self.Liquidate()
            self.front_VX = None
            self.front_ES = None
            return
        
        # exit if the daily roll being greater than -0.05 if holding long positions                    
        if self.Portfolio[self.front_VX.Symbol].IsLong and daily_roll > -0.05:
            self.Liquidate()
            self.front_VX = None
            self.front_ES = None
            return
        
if self.front_VX and self.front_ES:
    # if these exit conditions are not triggered, trades are exited two days before it expires
    if self.Portfolio[self.front_VX.Symbol].Invested and self.Portfolio[self.front_ES.Symbol].Invested: 
        if (self.front_VX.Expiry-self.Time).days <=2 or (self.front_ES.Expiry-self.Time).days <=2:
            self.Liquidate()
            self.front_VX = None
            self.front_ES = None
            return
        
def CalculateHedgeRatio(self):
price_VX = np.array(self.price_VX)
price_ES = np.array(self.price_ES)
delta_VX = np.diff(price_VX)

res_ES = np.diff(price_ES) / price_ES[:-1]*100
tts = np.array(self.days_to_maturity)[1:]
df = pd.DataFrame({"delta_VX":delta_VX, "SPRET":res_ES, "product":res_ES*tts}).dropna()

# remove rows with zero value
df = df[(df != 0).all(1)]
y = df['delta_VX'].astype(float)
X = df[['SPRET', "product"]].astype(float)
X = sm.add_constant(X)

model = sm.OLS(y, X).fit()
beta_1 = model.params[1]
beta_2 = model.params[2]

hedge_ratio = abs((1000*beta_1 + beta_2*((self.front_VX.Expiry-self.Time).days)*1000)/(0.01*50*float(self.Securities[self.es1].Price)))

return hedge_ratio

def get_expiry_calendar(self):
# import the futures expiry calendar
url = "data.quantpedia.com/backtesting_data/economic/vix_futures_expiration.csv"
csv_string_file = self.Download(url)
dates = csv_string_file.split('\r\n')
dates = [datetime.strptime(x, "%Y-%m-%d") for x in dates]
df_date = pd.DataFrame(dates, index = dates, columns = [ 'expiry'])

# convert the index and expiry column to datetime format
# df_date.index = pd.to_datetime(df_date.index)
df_date['expiry'] = pd.to_datetime(df_date['expiry'])
# idx = pd.date_range('19-01-2005', '16-12-2020')

# populate the date index and backward fill the dataframe    
# return df_date.reindex(idx, method='bfill')
return df_date

class QuandlVix(PythonQuandl):
def __init__(self):
self.ValueColumnName = "close"

class QuandlFutures(PythonQuandl):
def __init__(self):
self.ValueColumnName = "settle"