
“The strategy trades VIX futures in contango/backwardation, hedging with S&P 500 futures, using daily roll thresholds and regression-based hedge ratios, holding positions for five days.“
ASSET CLASS: futures | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: Exploiting ,Term Structure , VIX Futures
I. STRATEGY IN A NUTSHELL
This strategy trades VIX futures while hedging with E-mini S&P 500 futures. The investor sells (buys) the nearest VIX futures with at least ten trading days to maturity when the VIX term structure is in contango (backwardation) with a daily roll greater than 0.10 (less than -0.10) points, holding positions for five days. The daily roll, defined as the difference between the front VIX futures price and the VIX divided by days to settlement, estimates potential profits assuming a linear basis decline. Hedge ratios are calculated using regressions of VIX futures price changes against S&P 500 futures returns and time-to-settlement.
II. 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.
III. SOURCE PAPER
The VIX Futures Basis: Evidence and Trading Strategies [Click to Open PDF]
- David P. Simon and Jim Campasano. Professor of Finance ,Bentley University.Doctoral Candidate Isenberg School of Management ,University of Massachusetts.
<Abstract>
This study demonstrates that the VIX futures basis does not have significant forecast power for the change in the spot VIX from 2006 through 2011 but does have forecast power for VIX futures price changes. The study then demonstrates the profitability of shorting VIX futures contracts when the basis is in contango and buying VIX futures contracts when the basis is in backwardation with the market exposure of these positions hedged with mini-S&P 500 futures positions. The results indicate that these trading strategies are highly profitable and robust to transaction costs and out of sample hedge ratio forecasts. Overall, the analysis supports the view that the VIX futures basis does not accurately reflect the mean-reverting properties of the VIX spot index but rather reflects a risk premium that can be harvested.


V. BACKTEST PERFORMANCE
| Annualised Return | 19.67% |
| Volatility | N/A |
| Beta | 0.053 |
| Sharpe Ratio | N/A |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 45% |
V. FULL PYTHON CODE
import numpy as np
import pandas as pd
import statsmodels.api as sm
from collections import deque
class ExploitingTermStructureVIXFutures(QCAlgorithm):
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"