Overall Statistics
Total Trades
375
Average Win
3.52%
Average Loss
-2.27%
Compounding Annual Return
9.223%
Drawdown
32.400%
Expectancy
0.219
Net Profit
96.747%
Sharpe Ratio
0.391
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.55
Alpha
4.738
Beta
-336.139
Annual Standard Deviation
0.212
Annual Variance
0.045
Information Ratio
0.326
Tracking Error
0.212
Treynor Ratio
0
Total Fees
$184530.10
import numpy as np
from QuantConnect.Python import PythonQuandl
from QuantConnect.Data.Custom import *
# from expiry import calendar
import datetime
import pandas as pd
from datetime import timedelta
from collections import deque
import statsmodels.api as sm
from decimal import Decimal

class TermStructureOfVixAlgorithm(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2011, 1, 1)   # Set Start Date
        self.SetEndDate(2018, 9, 1)     # Set End Date
        self.SetCash(10000000)          # Set Strategy Cash
        


        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)

        
        # Add VIX futures contract data 
        self.AddFuture(Futures.Indices.VIX).SetFilter(timedelta(0), timedelta(days=180))
        # Add E-mini S&P500 futures contract data 
        self.AddFuture(Futures.Indices.SP500EMini).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)])
            self.price_ES.append(row[str(self.es1)])
            self.days_to_maturity.append((row['expiry']-index).days)
        
        stockPlot = Chart("Trade")
        stockPlot.AddSeries(Series("VIX", SeriesType.Line, 0))
        stockPlot.AddSeries(Series("VIX Futures", SeriesType.Line, 0))
        stockPlot.AddSeries(Series("Buy", SeriesType.Scatter, 0))
        stockPlot.AddSeries(Series("Sell", SeriesType.Scatter, 0))
        stockPlot.AddSeries(Series("Daily Roll", SeriesType.Scatter, 0))
        stockPlot.AddSeries(Series("Hedge Ratio", SeriesType.Scatter, 0))

    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)[0]
            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)[0]
        
        if data.ContainsKey(self.vx1) and data.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
                self.Plot("Trade", "VIX", self.Securities[self.vix].Price)
                self.Plot("Trade", "VIX Futures", self.Securities[self.vx1].Price)
                self.Plot("Trade", "Daily Roll", daily_roll)
                
                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.Plot("Trade", "Sell", self.Securities[self.vx1].Price)
                        self.SetHoldings(self.front_VX.Symbol, -0.5)
                        self.SetHoldings(self.front_ES.Symbol, -0.5*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.Plot("Trade", "Buy", self.Securities[self.vx1].Price)
                        self.SetHoldings(self.front_VX.Symbol, 0.5)
                        self.SetHoldings(self.front_ES.Symbol, 0.5*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)))
        
        self.Plot("Trade", "Hedge Ratio", hedge_ratio)
        
        return hedge_ratio

    def get_expiry_calendar(self):
        # import the futures expiry calendar
        url = "https://www.dropbox.com/s/5k4rbuzfsfn3w0h/expiry.csv?dl=1"
        df_date = pd.read_csv(url, index_col = 'date')
        # 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('01-01-2011', '04-19-2019')
        # populate the date index and backward fill the dataframe    
        return df_date.reindex(idx, method='bfill')

class QuandlVix(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "vix Close"

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