Contango-Backwardation VIX Futures Hedge Strategy
Log in to collectAcademic 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
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"