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.

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]

<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 Return19.67%
VolatilityN/A
Beta0.053
Sharpe RatioN/A
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate45%

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"

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading