Overall Statistics
Total Trades
506
Average Win
10.58%
Average Loss
-7.81%
Compounding Annual Return
-20.284%
Drawdown
94.800%
Expectancy
0.062
Net Profit
-92.012%
Sharpe Ratio
-0.153
Probabilistic Sharpe Ratio
0.000%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
1.35
Alpha
-0.063
Beta
0.053
Annual Standard Deviation
0.383
Annual Variance
0.146
Information Ratio
-0.368
Tracking Error
0.405
Treynor Ratio
-1.111
Total Fees
$4625.00
Estimated Strategy Capacity
$9000000.00
Lowest Capacity Asset
ES XFH59UK0MYO1
# 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.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"