Overall Statistics
Total Orders
210
Average Win
0.03%
Average Loss
0.00%
Compounding Annual Return
0.119%
Drawdown
0.200%
Expectancy
1.465
Start Equity
10000000
End Equity
10059850.98
Net Profit
0.599%
Sharpe Ratio
-5.055
Sortino Ratio
-5.065
Probabilistic Sharpe Ratio
12.888%
Loss Rate
74%
Win Rate
26%
Profit-Loss Ratio
8.51
Alpha
-0.008
Beta
0.01
Annual Standard Deviation
0.001
Annual Variance
0
Information Ratio
-0.981
Tracking Error
0.106
Treynor Ratio
-0.691
Total Fees
$645.43
Estimated Strategy Capacity
$5100000000.00
Lowest Capacity Asset
VX VF1TI9X216G9
Portfolio Turnover
0.07%
#region imports
from AlgorithmImports import *

from collections import deque
import statsmodels.api as sm
#endregion


class TermStructureOfVixAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2012, 1, 1)   # Set Start Date
        self.set_end_date(2017, 1, 1)     # Set End Date
        self.set_cash(10000000)          # Set Strategy Cash

        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.daily_precise_end_time = False
        vix = self.add_index("VIX", Resolution.DAILY)
        self.vix = vix.symbol 
        self.vix_multiplier = vix.symbol_properties.contract_multiplier
        self.vx1 = self.add_future(Futures.Indices.VIX,
                resolution=Resolution.DAILY,
                extended_market_hours=True,
                data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO,
                data_mapping_mode=DataMappingMode.LAST_TRADING_DAY,
                contract_depth_offset=0
            )
        self.vx1_multiplier = self.vx1.symbol_properties.contract_multiplier
        self.es1 = self.add_future(Futures.Indices.SP_500_E_MINI,
                resolution=Resolution.DAILY,
                extended_market_hours=True,
                data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO,
                data_mapping_mode=DataMappingMode.LAST_TRADING_DAY,
                contract_depth_offset=0
            )
        self.es1_multiplier = self.es1.symbol_properties.contract_multiplier
        
        # the rolling window to save the front month VX future price
        self.price_vx = RollingWindow[float](252)
        # the rolling window to save the front month ES future price
        self.price_es = RollingWindow[float](252)
        # the rolling window to save the time-to-maturity of the contract
        self.days_to_maturity = RollingWindow[float](252)
        
        stock_plot = Chart("Trade")
        stock_plot.add_series(Series("VIX", SeriesType.LINE, 0))
        stock_plot.add_series(Series("VIX Futures", SeriesType.LINE, 0))
        stock_plot.add_series(Series("Buy", SeriesType.SCATTER, 0))
        stock_plot.add_series(Series("Sell", SeriesType.SCATTER, 0))
        stock_plot.add_series(Series("Daily Roll", SeriesType.SCATTER, 0))
        stock_plot.add_series(Series("Hedge Ratio", SeriesType.SCATTER, 0))

        self.set_warm_up(253, Resolution.DAILY)

    def on_data(self, data):
        if data.bars.contains_key(self.vx1.symbol) and data.bars.contains_key(self.es1.symbol):
            # update the rolling window price and time-to-maturity series every day
            vx1_price = data.bars[self.vx1.symbol].close
            self.price_vx.add(float(data.bars[self.vx1.symbol].close))
            self.price_es.add(float(data.bars[self.es1.symbol].close))
            self.days_to_maturity.add((self.vx1.mapped.id.date-self.time).days)
            
        if (self.is_warming_up or not self.price_vx.is_ready or 
            not self.price_es.is_ready or not self.days_to_maturity.is_ready or 
            (self.vx1.mapped.id.date - self.time).days == 0):
            return

        if data.bars.contains_key(self.vx1.symbol) and data.bars.contains_key(self.es1.symbol):
            # calculate the daily roll
            daily_roll = (
                (vx1_price*self.vx1_multiplier - self.securities[self.vix].price*self.vix_multiplier)
                / (self.vx1.mapped.id.date - self.time).days
            )
            self.plot("Trade", "VIX", vx1_price)
            self.plot("Trade", "VIX Futures", vx1_price)
            self.plot("Trade", "Daily Roll", daily_roll)
            
            if not self.portfolio[self.vx1.mapped].invested:
                # Short if the contract is in contango with adaily roll greater than 0.10 
                if daily_roll > 0.1:
                    hedge_ratio = self._calculate_hedge_ratio(data.bars[self.es1.symbol].close)
                    self.plot("Trade", "Sell", vx1_price)
                    qty = self.calculate_order_quantity(self.vx1.mapped, -1) // self.vx1.symbol_properties.contract_multiplier
                    if qty:
                        self.market_order(self.vx1.mapped, qty)
                    qty = self.calculate_order_quantity(self.es1.mapped, -1*hedge_ratio) // self.es1.symbol_properties.contract_multiplier
                    if qty:
                        self.market_order(self.es1.mapped, qty)
                # Long if the contract is in backwardation with adaily roll less than -0.10
                elif daily_roll < -0.1:
                    hedge_ratio = self._calculate_hedge_ratio(data.bars[self.es1.symbol].close)
                    self.plot("Trade", "Buy", vx1_price)
                    qty = self.calculate_order_quantity(self.vx1.mapped, 1) // self.vx1.symbol_properties.contract_multiplier
                    if qty:
                        self.market_order(self.vx1.mapped, qty)
                    qty = self.calculate_order_quantity(self.es1.mapped, 1*hedge_ratio) // self.es1.symbol_properties.contract_multiplier
                    if qty:
                        self.market_order(self.es1.mapped, qty)
            
            # exit if the daily roll being less than 0.05 if holding short positions                 
            if self.portfolio[self.vx1.mapped].is_short and daily_roll < 0.05:
                self.liquidate()
                return
            
            # exit if the daily roll being greater than -0.05 if holding long positions                    
            if self.portfolio[self.vx1.mapped].is_long and daily_roll > -0.05:
                self.liquidate()
                return
                
        if self.vx1.mapped and self.es1.mapped:
            # if these exit conditions are not triggered, trades are exited two days before it expires
            if self.portfolio[self.vx1.mapped].invested and self.portfolio[self.es1.mapped].invested: 
                if (self.vx1.mapped.id.date-self.time).days <= 2 or (self.es1.mapped.id.date-self.time).days <= 2:
                    self.liquidate()
                    
    def _calculate_hedge_ratio(self, es1_price):
        price_vx = np.array(list(self.price_vx))[::-1]/self.vx1_multiplier
        price_es = np.array(list(self.price_es))[::-1]/self.es1_multiplier
        delta__v_x = np.diff(price_vx)
        res__e_s = np.diff(price_es)/price_es[:-1]*100
        tts = np.array(list(self.days_to_maturity))[::-1][1:]
        df = pd.DataFrame({"delta__v_x":delta__v_x, "SPRET":res__e_s, "product":res__e_s*tts}).dropna()
        # remove rows with zero value
        df = df[(df != 0).all(1)]
        y = df['delta__v_x'].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 = (beta_1 + beta_2*((self.vx1.mapped.id.date-self.time).days))/float(es1_price)
        
        self.plot("Trade", "Hedge Ratio", hedge_ratio)
        
        return hedge_ratio