Overall Statistics Total Trades 505 Average Win 5.92% Average Loss -7.57% Compounding Annual Return -11.472% Drawdown 97.000% Expectancy -0.166 Net Profit -67.804% Sharpe Ratio 0.126 Probabilistic Sharpe Ratio 0.044% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 0.78 Alpha -0.056 Beta 1.454 Annual Standard Deviation 0.587 Annual Variance 0.345 Information Ratio -0.028 Tracking Error 0.553 Treynor Ratio 0.051 Total Fees \$5261.40
# https://quantpedia.com/strategies/exploiting-term-structure-of-vix-futures/
#
# 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)

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.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.SetFilter(timedelta(0), timedelta(days=180))

es_data.SetFilter(timedelta(0), timedelta(days=180))

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:
if chain.Key.Value == 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)
if chain.Key.Value == 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)

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)

model = sm.OLS(y, X).fit()
beta_1 = model.params
beta_2 = model.params

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"
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 = "vix Close"

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