Overall Statistics
Total Orders
142
Average Win
3.43%
Average Loss
-2.54%
Compounding Annual Return
34.422%
Drawdown
30.500%
Expectancy
0.176
Start Equity
1000000
End Equity
1291888.12
Net Profit
29.189%
Sharpe Ratio
0.828
Sortino Ratio
0.571
Probabilistic Sharpe Ratio
46.693%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.35
Alpha
0
Beta
0
Annual Standard Deviation
0.261
Annual Variance
0.068
Information Ratio
1.038
Tracking Error
0.261
Treynor Ratio
0
Total Fees
$16047.59
Estimated Strategy Capacity
$3200000.00
Lowest Capacity Asset
VX YOEWRGKCCK4P
Portfolio Turnover
29.78%
#region imports
from AlgorithmImports import *
#endregion


general_setting = {
    "lookback": 100,
    "lookback_RESOLUTION": "HOUR",

    "ratio_method": "Regression",

    "Take_Profit_pct": 0.3,
    "Stop_Loss_pct": 0.08,

    "p_value_threshold_entry": 0.0001,
    "p_value_threshold_exit": 0.00001,
    "rollover_days": 3, 

}
from AlgorithmImports import *
from QuantConnect.DataSource import *

from config import general_setting
import pickle

import numpy as np
import pandas as pd
import math
import statsmodels.api as sm
from pandas.tseries.offsets import BDay
from pykalman import KalmanFilter
from statsmodels.tsa.stattools import coint, adfuller

class CalendarSpread(QCAlgorithm):

#%% initialize
    def initialize(self) -> None:
        self.SetTimeZone(TimeZones.NEW_YORK)

        self.set_start_date(2024, 1, 1) 
        # self.set_end_date(2024,9,10)
        self.set_cash(1000000) 
        self.universe_settings.asynchronous = True

        self.zscore_df = {}
        self.note1_price = {}
        self.note2_price = {}


        # Requesting  data
        # Futures.Currencies.EUR
        # Futures.Currencies.MICRO_EUR
        # Futures.Financials.Y_2_TREASURY_NOTE
        # Futures.Financials.Y_5_TREASURY_NOTE
        # Futures.Indices.MICRO_NASDAQ_100_E_MINI
        # Futures.Indices.SP_500_E_MINI
        # Futures.Indices.VIX

        future_vix = self.add_future(Futures.Indices.VIX, resolution = Resolution.HOUR, extended_market_hours=True,leverage = 2) 
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        future_vix.set_filter(0, 180)
        self.future_vix_symbol = future_vix.symbol

        self.first_vix_contract = None
        self.second_vix_contract = None
        self.third_vix_contract = None

        self.first_vix_expiry = None
        self.second_vix_expiry = None
        self.third_vix_expiry = None

        self.lookback  = general_setting['lookback']
        self.p_threshold_entry = general_setting['p_value_threshold_entry']
        self.p_threshold_exit = general_setting['p_value_threshold_exit']
        self.rollover_days = general_setting['rollover_days']

        self.wt_1 = None
        self.wt_2 = None

        self.roll_signal = False
        self.Margin_Call = False

        self.prev_cap = None

        self.large_diff = None
        self.backwardation = False

        self.diversion = None
        self.freeze = 0

        self.spread = {}
        self.timing = {}
        self.timing_coef = {}

        self.start_time = None

        self.entry_time = None
        self.coefs = []

        self.close = None

#%% stats
    def stats(self):
        # Request Historical Data
        df_vix1 = self.History(self.first_vix_contract.symbol, timedelta(self.lookback), Resolution.HOUR).rename(columns = {'close':'first'})
        df_vix2 = self.History(self.second_vix_contract.symbol, timedelta(self.lookback), Resolution.HOUR).rename(columns = {'close':'second'})
        # df_vix3 = self.History(self.third_vix_contract.symbol,timedelta(self.lookback), Resolution.HOUR).rename(columns = {'close':'third'})

        df_merge = pd.merge(df_vix1, df_vix2, on = ['time'], how = 'inner')

        vix1_log = np.array(df_merge['first'].apply(lambda x: math.log(x))) 
        vix2_log = np.array(df_merge['second'].apply(lambda x: math.log(x))) 
        # vix3_log = np.array(df_Gold3.apply(lambda x: math.log(x))) 

        # 1st & 2nd

    
        X1 = sm.add_constant(vix1_log)
        Y1 = vix2_log
        model1 = sm.OLS(Y1, X1)
        results1 = model1.fit()
        sigma1 = math.sqrt(results1.mse_resid)
        slope1 = results1.params[1]
        intercept1 = results1.params[0]
        res1 = results1.resid
        zscore1 = res1/sigma1

        adf1 = adfuller(res1)

        p_value1 = adf1[1]

        # spread = res1[len(res1)-1]
        df_merge['spread'] = df_merge['second'] - df_merge['first']
        
        spread = np.array(df_merge['spread'])
        # test_passed1 = p_value1 <= self.p_threshold
        # self.debug(f"p value is {p_value1}")

        return [p_value1, zscore1, slope1, spread]

    
    #%%
    def on_data(self, slice: Slice) -> None:
        # Entry signal
        # if self.time.minute == 0 or self.time.minute ==10 or self.time.minute == 20 or self.time.minute==30 or self.time.minute == 40 or self.time.minute == 50:
        
                            
                # Take Profit / Stop Loss    
        if self.freeze > 0:
            if self.freeze < 48:
                self.freeze += 1

            else:
                self.freeze = 0
            return
        
        if self.roll_signal == False and self.time.hour < 17 and self.time.hour > 9:
            if not self.portfolio.Invested:

                chain = slice.futures_chains.get(self.future_vix_symbol)
                if chain:
                    contracts = [i for i in chain ]
                    
                    e = [i.expiry for i in contracts]
                    e = sorted(list(set(sorted(e, reverse = True))))
                    # e = [i.expiry for i in contracts if i.expiry- self.Time> timedelta(5)]
                    # self.debug(f"the first contract is {e[0]}, the length of e is {len(e)}")
                    # expiry = e[0]
                    
                    try:
                        self.first_vix_contract = [contract for contract in contracts if contract.expiry == e[0]][0]
                        self.second_vix_contract = [contract for contract in contracts if contract.expiry == e[1]][0]
                        # self.third_gold_contract = [contract for contract in contracts if contract.expiry == e[2]][0]
                        self.first_vix_expiry = e[0]
                        self.second_vix_expiry = e[1]
                        # self.third_gold_expiry = e[2]

                        stats = self.stats()
                        sigma = stats[3].std()
                        mean = stats[3].mean()
                        last_spread = stats[3][-1]
                        self.spread[self.time] = last_spread
                        
                        # self.zscore_df[self.time] = stats[1][-1]
                        # self.note1_price[self.time] = self.Securities[self.first_vix_contract.symbol].Price
                        # self.note2_price[self.time] = self.Securities[self.second_vix_contract.symbol].Price                       
                        # self.plot('z_score_plot','z_score',stats[1][-1] )
                        # self.plot('p_value_plot','p_value', stats[0])
                        # self.plot('p_value_plot','p_value', stats[0] )
                        # self.plot('spread_plot','spread', stats[3] )

                        # if (self.first_vix_expiry.date() - self.time.date()).days > self.rollover_day:
                        self.trade_signal = True
                        
                        # else:
                        #     self.trade_signal = False

                        if self.trade_signal and ((self.first_vix_expiry.date() - self.time.date()).days > self.rollover_days):
                            
                            self.wt_1 = 1/(1+stats[2])
                            self.wt_2 = 1 - self.wt_1



                            # if stats[3]<0:
                            if  last_spread > mean + 1.78*sigma and (last_spread < mean + 2.42*sigma):
                                n = (last_spread-mean)/sigma
                                self.set_holdings(self.first_vix_contract.symbol, -self.wt_1, tag = f'spread  = mean + {round(n,2)}*sigma (diversion)')
                                self.set_holdings(self.second_vix_contract.symbol,  self.wt_2, tag = f'spread = mean + {round(n,2)}*sigma  (diversion)')
                                self.prev_cap = self.portfolio.total_portfolio_value
                                self.large_diff = True
                                self.diversion = True
                                self.timing[self.time] = 'entry-diversion'
                                self.timing_coef[self.time] = round(n, 2)
                                self.start_time = self.time
                                self.coefs = []
                                self.coefs.append(n)
                                self.entry_time = self.time

                            if  (last_spread > mean + 2.42*sigma):
                                n = (last_spread-mean)/sigma
                                self.set_holdings(self.first_vix_contract.symbol, self.wt_1, tag = f'spread  = mean + {round(n,2)}*sigma (mean reversion)')
                                self.set_holdings(self.second_vix_contract.symbol,  -self.wt_2, tag = f'spread = mean + {round(n,2)}*sigma  (mean reversion)')
                                self.prev_cap = self.portfolio.total_portfolio_value
                                self.large_diff = True
                                self.diversion = False
                                self.timing[self.time] = 'entry-reversion'
                                self.timing_coef[self.time] = round(n, 2)
                                self.start_time = self.time
                                self.entry_time = self.time

                            elif last_spread < mean - 0.94*sigma and last_spread > mean - 1.73*sigma:
                                n = abs((last_spread-mean)/sigma)
                                self.set_holdings(self.first_vix_contract.symbol, self.wt_1, tag = f'spread < mean - {round(n,2)}*sigma (diversion)')
                                self.set_holdings(self.second_vix_contract.symbol, -self.wt_2, tag = f'spread < mean - {round(n,2)}*sigma (diversion)')
                                self.prev_cap = self.portfolio.total_portfolio_value
                                self.large_diff = False
                                self.diversion = True

                                self.timing[self.time] = 'entry-diversion'
                                self.timing_coef[self.time] = round(-n, 2)
                                self.start_time = self.time
                                self.coefs = []
                                self.coefs.append(-n)
                                self.entry_time = self.time

                            elif last_spread < mean - 1.73*sigma:
                                n = abs((last_spread-mean)/sigma)
                                self.set_holdings(self.first_vix_contract.symbol, -self.wt_1, tag = f'spread < mean - {round(n,2)}*sigma (mean reversion)')
                                self.set_holdings(self.second_vix_contract.symbol, self.wt_2, tag = f'spread < mean - {round(n,2)}*sigma (mean reversion)')
                                self.prev_cap = self.portfolio.total_portfolio_value
                                self.large_diff = False
                                self.diversion = False
                                self.timing[self.time] = 'entry-diversion'
                                self.timing_coef[self.time] = round(n, 2)
                                self.start_time = self.time
                                self.entry_time = self.time

                            self.trade_signal = False

                    except:
                        return

            else:
                # exit signal

                # self.zscore_df[self.time] = stats[1][-1]
                # self.note1_price[self.time] = self.Securities[self.first_vix_contract.symbol].Price
                # self.note2_price[self.time] = self.Securities[self.second_vix_contract.symbol].Price
                # self.plot('p_value_plot','p_value', stats[0])
                # self.plot('z_score_plot','z_score',stats[1][-1] )
                # self.plot('spread_plot','spread', stats[3] )
                # self.debug(f'mean is {mean}, sigma is {sigma}, last_spread is {last_spread}')     


                stats = self.stats()
                sigma = stats[3].std()
                mean = stats[3].mean()
                last_spread = stats[3][-1]
                n = (last_spread-mean)/sigma
                
                self.spread[self.time] = last_spread 
                self.coefs.append(n)

                            # if self.prev_cap :


                # Roll over
                if ((self.first_vix_expiry.date() - self.time.date()).days < self.rollover_days) and self.time.hour == 10 :
                    self.roll_signal = True
                    if self.portfolio.total_portfolio_value>= self.prev_cap:
                        self.close = self.liquidate(tag = 'rollover; Win')
                    else:
                        self.close = self.liquidate(tag = 'rollover; Loss')
                    self.prev_cap = None
                    self.large_diff = None
                    self.timing[self.time] = 'exit-rollover'
                    stats = self.stats()
                    sigma = stats[3].std()
                    mean = stats[3].mean()
                    last_spread = stats[3][-1]
                    n = (last_spread-mean)/sigma
                    self.timing_coef[self.time] = round(n, 2)

                    return


                # # Take profit & Stop Loss
                # if self.portfolio.total_portfolio_value> 1.05 * self.prev_cap:
                #     self.liquidate(tag = 'Take Profit')
                #     self.prev_cap = None
                #     self.large_diff = None
                #     return

                # elif self.portfolio.total_portfolio_value< 0.96 * self.prev_cap:
                #     self.liquidate(tag = 'Stop Loss')
                #     self.prev_cap = None
                #     self.large_diff = None
                #     # self.roll_signal = True
                #     self.freeze = 1
                #     return

                # if (self.time.date() - self.entry_time.date()).days > 18:

                #     if self.portfolio.total_portfolio_value>= self.prev_cap:
                #         self.liquidate(tag = 'Too Long to Exit; Win')
                #     else:
                #         self.liquidate(tag = 'Too Long to Exit; Loss')
                #         self.freeze = 1

                #     self.prev_cap = None
                #     self.large_diff = None
                #     self.diversion = None
                #     # self.roll_signal = True

                #     return








                if self.diversion == True:
                    stats = self.stats()
                    sigma = stats[3].std()
                    mean = stats[3].mean()
                    last_spread = stats[3][-1]
                    self.wt_1 = 1/(1+stats[2])
                    self.wt_2 = 1 - self.wt_1

                    if (last_spread > mean + 2.42 * sigma and self.large_diff == True):
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Diversion(Peak); Win')
                        else:
                            self.close = self.liquidate(tag = 'Diversion(Peak); Loss')

                        n = (last_spread-mean)/sigma
                        self.set_holdings(self.first_vix_contract.symbol, self.wt_1, tag = f'spread  = mean + {round(n,2)}*sigma (mean_revesion)')
                        self.set_holdings(self.second_vix_contract.symbol,  -self.wt_2, tag = f'spread = mean + {round(n,2)}*sigma (mean_reversion)')
                        self.prev_cap = self.portfolio.total_portfolio_value
                        self.large_diff = True
                        self.diversion = False
                        
                        self.timing[self.time] = 'exit2entry-diversion2reversion'
                        self.timing_coef[self.time] = round(n, 2)
                        self.start_time = None
                        return

                    # elif (last_spread < mean + 1.78 * sigma and self.large_diff == True and (self.time - self.start_time).seconds//3600 > 6):
                    # elif (n < np.mean(self.coefs) and self.large_diff == True and (self.time - self.start_time).days > 4):
                    #     if self.portfolio.total_portfolio_value>= self.prev_cap:
                    #         self.liquidate(tag = 'Diversion; Win')
                    #     else:
                    #         self.liquidate(tag = 'Diversion; Loss')

                    #     n = (last_spread-mean)/sigma
                    #     self.set_holdings(self.first_vix_contract.symbol, self.wt_1, tag = f'spread  = mean + {round(n,2)}*sigma (mean_revesion)')
                    #     self.set_holdings(self.second_vix_contract.symbol,  -self.wt_2, tag = f'spread = mean + {round(n,2)}*sigma (mean_reversion)')
                    #     self.prev_cap = self.portfolio.total_portfolio_value

                    #     self.large_diff = True
                    #     self.diversion = False

                    #     self.timing[self.time] = 'exit2entry-diversion2reversion'
                    #     self.timing_coef[self.time] = round(n, 2)
                    #     self.start_time = None
                    #     return

                    elif (last_spread < mean - 1.73*sigma and self.large_diff == False):
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Diversion(Bottom); Win')
                        else:
                            self.close = self.liquidate(tag = 'Diversion(Bottom); Loss')

                        n = (last_spread-mean)/sigma
                        self.set_holdings(self.first_vix_contract.symbol, -self.wt_1, tag = f'spread  = mean - {abs(round(n,2))}*sigma (mean_revesion)')
                        self.set_holdings(self.second_vix_contract.symbol,  self.wt_2, tag = f'spread = mean - {abs(round(n,2))}*sigma (mean_reversion)')
                        self.prev_cap = self.portfolio.total_portfolio_value
                        self.large_diff = False
                        self.diversion = False

                        self.timing[self.time] = 'exit2entry-diversion2reversion'
                        self.timing_coef[self.time] = round(n, 2)
                        self.start_time = None
                        return

                    # elif (n > np.mean(self.coefs) and self.large_diff == False and (self.time - self.start_time).days > 1):
                    #     if self.portfolio.total_portfolio_value>= self.prev_cap:
                    #         self.liquidate(tag = 'Diversion; Win')
                    #     else:
                    #         self.liquidate(tag = 'Diversion; Loss')
                    #     n = (last_spread-mean)/sigma
                    #     self.set_holdings(self.first_vix_contract.symbol, -self.wt_1, tag = f'spread  = mean - {abs(round(n,2))}*sigma (mean_revesion)')
                    #     self.set_holdings(self.second_vix_contract.symbol,  self.wt_2, tag = f'spread = mean - {abs(round(n,2))}*sigma (mean_reversion)')
                    #     self.prev_cap = self.portfolio.total_portfolio_value
                    #     self.large_diff = False
                    #     self.diversion = False

                    #     self.timing[self.time] = 'exit2entry-diversion2reversion'
                    #     self.timing_coef[self.time] = round(n, 2)
                    #     self.start_time = None
                    #     return


                else:
                    stats = self.stats()
                    sigma = stats[3].std()
                    mean = stats[3].mean()
                    last_spread = stats[3][-1]
                    self.wt_2 = 1/(1+stats[2])
                    self.wt_1 = 1 - self.wt_2

                    if (last_spread < mean + 1.15 * sigma and self.large_diff == True):
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Mean Reversion; Win')
                        else:
                            self.close = self.liquidate(tag = 'Mean Reversion; Loss')
                        
                        
                        self.diversion = None
                        self.prev_cap = None
                        self.large_diff = None
                        n = (last_spread-mean)/sigma
                        self.timing[self.time] = 'exit-reversion'
                        self.timing_coef[self.time] = round(n, 2)

                        return
                        
                    elif (last_spread > mean - 0.38*sigma and self.large_diff == False):
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Mean Reversion; Win')
                        else:
                            self.close = self.liquidate(tag = 'Mean Reversion; Loss')


                        self.prev_cap = None
                        self.large_diff = None
                        self.diversion = None

                        n = (last_spread-mean)/sigma
                        self.timing[self.time] = 'exit-reversion'
                        self.timing_coef[self.time] = round(n, 2)

                    return


                if not self.large_diff:
                    if n > 0:
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Wrong Direction (n > 0); Win')
                        else:
                            self.close = self.liquidate(tag = 'Wrong Direction (n > 0); Loss')
                        return


                if self.large_diff:
                    if n < -0.3:
                        if self.portfolio.total_portfolio_value>= self.prev_cap:
                            self.close = self.liquidate(tag = 'Wrong Direction (n < 0); Win')
                        else:
                            self.close = self.liquidate(tag = 'Wrong Direction (n < 0); Loss')
                        return


        else:

            stats = self.stats()
            # self.zscore_df[self.time] = stats[1][-1]
            # self.note1_price[self.time] = self.Securities[self.first_vix_contract.symbol].Price
            # self.note2_price[self.time] = self.Securities[self.second_vix_contract.symbol].Price
            # self.plot('z_score_plot','z_score',stats[1][-1] )            
            # self.plot('p_value_plot','p_value', stats[0])

            if self.first_vix_expiry.date() < self.time.date():
                self.roll_signal = False

        # if self.zscore_df:
        #     df = pd.DataFrame.from_dict(self.zscore_df, orient='index',columns=['zscore'])
        #     file_name = 'CalendarSpread/zscore_df'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))


        # if self.note1_price:
        #     df = pd.DataFrame.from_dict(self.note1_price, orient='index',columns=['price1'])
        #     file_name = 'CalendarSpread/note1_df'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))

        # if self.note2_price:
        #     df = pd.DataFrame.from_dict(self.note2_price, orient='index',columns=['price2'])
        #     file_name = 'CalendarSpread/note2_df'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))

        # if self.timing:
        #     df = pd.DataFrame.from_dict(self.timing, orient='index',columns=['point_tag'])
        #     file_name = 'CalendarSpread/timing_point'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))


        # if self.timing_coef:
        #     df = pd.DataFrame.from_dict(self.timing_coef, orient='index',columns=['point_coef'])
        #     file_name = 'CalendarSpread/timing_coef'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))

        # if self.spread:
        #     df = pd.DataFrame.from_dict(self.spread, orient='index',columns=['spread'])
        #     file_name = 'CalendarSpread/spread'
        #     self.object_store.SaveBytes(file_name, pickle.dumps(df))

    def OnOrderEvent(self, orderEvent):

        
        if orderEvent.Status != OrderStatus.Filled:
            return
        

        # Webhook Notification    
        symbol = orderEvent.symbol
        price = orderEvent.FillPrice
        quantity = orderEvent.quantity
        
        a = { "text": f"[Calendar Arbitrage Paper order update] \nSymbol: {symbol} \nPrice: {price} \nQuantity: {quantity}" }
        payload = json.dumps(a)
        self.notify.web("https://hooks.slack.com/services/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", payload)

        if self.portfolio.Invested:
            stats = self.stats()
            sigma = stats[3].std()
            mean = stats[3].mean()
            last_spread = stats[3][-1]
            n = (last_spread - mean)/sigma
            if self.diversion:
                diversion = 'Diversion Trades'
            else:
                diversion = 'Reversion Trades'

            b = { "text": f"[Trade details] Entry: Spread = mean + ({n})*sigma -- {diversion} " }

        if not self.portfolio.Invested:
            tag = self.close.tag
            b = { "text": f"[Trade details] Exit: {tag} " }
            self.debug(f"test: exit massage is {tag}")


        payload = json.dumps(b)
        self.notify.web("https://hooks.slack.com/services/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", payload)


    def on_margin_call(self, requests):
        self.debug('Margin Call is coming')
        self.Margin_Call =  True

        a = { "text": f"[Calendar Spread Margin Call update]Margin Call is coming" }
        payload = json.dumps(a)
        self.notify.web("https://hooks.slack.com/services/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", payload)

        return requests

    def OnOrderEvent(self, orderEvent):

        
        if orderEvent.Status != OrderStatus.Filled:
            return

        if self.Margin_Call:
            qty = orderEvent.quantity
            symbol = orderEvent.symbol
            
            self.Margin_Call = False
            self.debug(f'Hit margin call, the qty is {qty}')

            if symbol == self.first_vix_contract.symbol:
                self.debug(f'if come here, symbol is {symbol}, qty is {qty}')
                self.market_order(self.second_es_contract.symbol, -qty)

            if symbol == self.second_vix_contract.symbol:
                self.debug(f'if come here, symbol is {symbol}, qty is {qty}')
                self.market_order(self.first_es_contract.symbol, -qty)

            # self.liquidate(tag = 'margin call')