Overall Statistics
Total Trades
171
Average Win
2.81%
Average Loss
-1.03%
Compounding Annual Return
9.745%
Drawdown
17.400%
Expectancy
0.861
Net Profit
264.925%
Sharpe Ratio
0.608
Probabilistic Sharpe Ratio
11.337%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
2.72
Alpha
0.049
Beta
0.095
Annual Standard Deviation
0.094
Annual Variance
0.009
Information Ratio
-0.166
Tracking Error
0.16
Treynor Ratio
0.599
Total Fees
$1937.72
Estimated Strategy Capacity
$5900000.00
Lowest Capacity Asset
VIXY UT076X30D0MD
Portfolio Turnover
1.05%
from AlgorithmImports import *
import numpy as np
import pandas as pd
from arch import arch_model

class CombiningVIXFuturesTermStructureStrategySP500Index(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(500000)
        self.tickers = ['VIXY', 'SPY']
        self.period = 120
        self.data = {}
        self.garch_fit_counter = 0
        self.days_to_fit = 120

        # Initialize default GARCH parameters
        self.garch_params_spy = {'alpha_0': 0.0002, 'alpha_1': 0.1, 'beta_1': 0.8}
        self.garch_params_vix = {'alpha_0': 0.0003, 'alpha_1': 0.2, 'beta_1': 0.7}

        #futures
        # self.vix_future = self.AddFuture(Futures.Indices.VIX, Resolution.Daily)

        for ticker in self.tickers:
            self.AddEquity(ticker, Resolution.Daily).Symbol
            self.data[ticker] = RollingWindow[float](self.period)

        self.vix = self.AddData(CBOE, 'VIX', Resolution.Daily).Symbol
        self.vix3M = self.AddData(CBOE, 'VIX3M', Resolution.Daily).Symbol

        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 10), self.FitGarchModel)

    def OnData(self, data: Slice) -> None:
        for ticker in self.tickers:
            if ticker in data and data[ticker]:
                self.data[ticker].Add(data[ticker].Value)

        if all(x in data and data[x] for x in [self.vix, self.vix3M]):
            if all(self.data[x].IsReady for x in self.tickers):
                vix = data[self.vix].Value
                vix3m = data[self.vix3M].Value

                vix_volatility = self.calculate_volatility(list(self.data['VIXY'])[::-1], is_spx=False)
                market_volatility = self.calculate_volatility(list(self.data['SPY'])[::-1], is_spx=True)
                total_volatility = 1 / vix_volatility + 1 / market_volatility

                w = (1.0 / vix_volatility) / total_volatility

                if vix3m >= vix:
                    if not self.Portfolio['VIXY'].IsShort:
                        self.SetHoldings('VIXY', -w)
                else:
                    if not self.Portfolio['VIXY'].IsLong:
                        self.SetHoldings('VIXY', w)
        else:
            self.Liquidate()
 
    def FitGarchModel(self):
        self.garch_fit_counter += 1
        if self.garch_fit_counter >= self.days_to_fit:
            for ticker in self.tickers:
                if self.data[ticker].IsReady:
                    returns = self.CalculateReturns(list(self.data[ticker]))
                    model = arch_model(returns, vol='Garch', p=1, q=1)
                    res = model.fit(update_freq=5, disp='off')
                    if ticker == 'SPY':
                        self.garch_params_spy = {'alpha_0': res.params['omega'], 'alpha_1': res.params['alpha[1]'], 'beta_1': res.params['beta[1]']}
                    else:
                        self.garch_params_vix = {'alpha_0': res.params['omega'], 'alpha_1': res.params['alpha[1]'], 'beta_1': res.params['beta[1]']}

            self.garch_fit_counter = 0
    # def FitGarchModel(self):
        
    #     # Increment the counter
    #     self.garch_fit_counter += 1
    #     # Perform fitting every 20 days
    #     if self.garch_fit_counter >= self.days_to_fit:
    #         if self.data['SPY'].IsReady:
    #             returns_spy = self.CalculateReturns(list(self.data['SPY']))
    #             # Fit the GARCH model for SPY and update self.garch_params_spy

    #         if self.data['VIXY'].IsReady:
    #             returns_vix = self.CalculateReturns(list(self.data['VIXY']))
    #             # Fit the GARCH model for VIXY and update self.garch_params_vix
            
    #         self.garch_fit_counter = 0

    def CalculateReturns(self, values):
        return pd.Series((np.array(values)[1:] - np.array(values)[:-1]) / np.array(values)[:-1])

    def calculate_volatility(self, values, min_observations=2, is_spx=True):
        returns = pd.Series((np.array(values)[1:] - np.array(values)[:-1]) / np.array(values)[:-1])
        volatilities = pd.Series(index=returns.index, dtype=float)

        if is_spx:
            params = self.garch_params_spy
        else:
            params = self.garch_params_vix

        for i in range(len(returns)):
            if i < min_observations:
                volatilities.iloc[i] = returns.iloc[:i+1].std()
            else:
                epsilon_t_minus_1 = returns.iloc[i-1]
                sigma_previous = volatilities.iloc[i-1]
                sigma_squared = params['alpha_0'] + params['alpha_1'] * epsilon_t_minus_1**2 + params['beta_1'] * sigma_previous**2
                volatilities.iloc[i] = np.sqrt(sigma_squared)

        return volatilities.iloc[-1]