Overall Statistics |
Total Orders 160 Average Win 4.16% Average Loss -3.01% Compounding Annual Return 11.798% Drawdown 35.600% Expectancy 0.072 Start Equity 1000000 End Equity 1112867.76 Net Profit 11.287% Sharpe Ratio 0.279 Sortino Ratio 0.182 Probabilistic Sharpe Ratio 25.093% Loss Rate 55% Win Rate 45% Profit-Loss Ratio 1.38 Alpha 0 Beta 0 Annual Standard Deviation 0.351 Annual Variance 0.123 Information Ratio 0.436 Tracking Error 0.351 Treynor Ratio 0 Total Fees $16067.35 Estimated Strategy Capacity $10000000.00 Lowest Capacity Asset VX YPDDEQD90YQX Portfolio Turnover 30.86% |
#region imports from AlgorithmImports import * #endregion general_setting = { "lookback": 60, "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": 2, }
Notebook too long to render.
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 from scipy.stats import jarque_bera class CalendarSpread(QCAlgorithm): 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) 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.coefs = [] self.entry1 = 1.557283 self.entry2 = 2.118554 self.entry3 = -0.948035 self.entry4 = -1.825264 self.exit1 = 0.824238 self.exit2 = -0.471902 self.ratio_60 = {} self.quantile25_60_30_pos = {} self.quantile50_60_30_pos = {} self.quantile75_60_30_pos = {} self.quantile25_60_30_neg = {} self.quantile50_60_30_neg = {} self.quantile75_60_30_neg = {} 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']) statistic, pvalue_jb = jarque_bera(spread) # test_passed1 = p_value1 <= self.p_threshold # self.debug(f"p value is {p_value1}") if pvalue_jb < 0.05: print("reject H0: Data do not follow Normal Distribution") else: print("cannot reject H0: Data follow Normal Distribution") return [p_value1, zscore1, slope1, spread, pvalue_jb] 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: if self.roll_signal == False and self.time.hour < 17 and self.time.hour > 8: 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] n = (last_spread-mean)/sigma self.coefs.append(n) self.ratio_60[self.time] = n if len(self.coefs) >= 24 * 30: self.coefs = self.coefs[-24 * 30:] self.pos_coefs = [i for i in self.coefs if i > 0] self.neg_coefs = [i for i in self.coefs if i < 0] if len(self.pos_coefs) > 24 * 10: pos_quantile = np.quantile( self.pos_coefs, [0.25,0.5,0.75]) self.entry1 = pos_quantile[1] self.entry2 = pos_quantile[2] self.exit1 = pos_quantile[0] self.quantile25_60_30_pos[self.time] = pos_quantile[0] self.quantile50_60_30_pos[self.time] = pos_quantile[1] self.quantile75_60_30_pos[self.time] = pos_quantile[2] if len(self.neg_coefs) > 24 * 10: neg_quantile = np.quantile( self.neg_coefs, [0.25,0.5,0.75]) self.entry3 = neg_quantile[1] self.entry4 = neg_quantile[0] self.exit2 = neg_quantile[2] self.quantile25_60_30_neg[self.time] = neg_quantile[0] self.quantile50_60_30_neg[self.time] = neg_quantile[1] self.quantile75_60_30_neg[self.time] = neg_quantile[2] # 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 n > self.entry1 and (n < self.entry2): 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 if (n > self.entry2): 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.debug(f"enter position: z score is {stats[1][-1]}") elif n < self.entry3 and n > self.entry4: self.set_holdings(self.first_vix_contract.symbol, self.wt_1, tag = f'spread < mean - {round(abs(n),2)}*sigma (diversion)') self.set_holdings(self.second_vix_contract.symbol, -self.wt_2, tag = f'spread < mean - {round(abs(n),2)}*sigma (diversion)') self.prev_cap = self.portfolio.total_portfolio_value self.large_diff = False # self.debug(f"enter position: z score is {stats[1][-1]}") self.diversion = True elif n < self.entry4: self.set_holdings(self.first_vix_contract.symbol, -self.wt_1, tag = f'spread < mean - {round(abs(n),2)}*sigma (mean reversion)') self.set_holdings(self.second_vix_contract.symbol, self.wt_2, tag = f'spread < mean - {round(abs(n),2)}*sigma (mean reversion)') self.prev_cap = self.portfolio.total_portfolio_value self.large_diff = False # self.debug(f"enter position: z score is {stats[1][-1]}") self.trade_signal = False except: return else: # exit signal stats = self.stats() sigma = stats[3].std() mean = stats[3].mean() last_spread = stats[3][-1] n = (last_spread-mean)/sigma self.wt_1 = 1/(1+stats[2]) self.wt_2 = 1 - self.wt_1 self.coefs.append(n) self.ratio_60[self.time] = n if len(self.coefs) >= 24 * 30: self.coefs = self.coefs[-24 * 30:] self.pos_coefs = [i for i in self.coefs if i > 0] self.neg_coefs = [i for i in self.coefs if i < 0] if len(self.pos_coefs) > 24 * 10: pos_quantile = np.quantile( self.pos_coefs, [0.25,0.5,0.75]) self.entry1 = pos_quantile[1] self.entry2 = pos_quantile[2] self.exit1 = pos_quantile[0] self.quantile25_60_30_pos[self.time] = pos_quantile[0] self.quantile50_60_30_pos[self.time] = pos_quantile[1] self.quantile75_60_30_pos[self.time] = pos_quantile[2] if len(self.neg_coefs) > 24 * 10: neg_quantile = np.quantile( self.neg_coefs, [0.25,0.5,0.75]) self.entry3 = neg_quantile[1] self.entry4 = neg_quantile[0] self.exit2 = neg_quantile[2] self.quantile25_60_30_neg[self.time] = neg_quantile[0] self.quantile50_60_30_neg[self.time] = neg_quantile[1] self.quantile75_60_30_neg[self.time] = neg_quantile[2] # 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 # 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.liquidate(tag = 'rollover; Win') else: self.liquidate(tag = 'rollover; Loss') self.prev_cap = None self.large_diff = None return # Take Profit / Stop Loss # if self.prev_cap : # if self.portfolio.total_portfolio_value> 1.1 * self.prev_cap: # self.liquidate(tag = 'Take Profit') # self.prev_cap = None # self.large_diff = None # return # elif self.portfolio.total_portfolio_value< 0.93 * self.prev_cap: # self.liquidate(tag = 'Stop Loss') # self.prev_cap = None # self.large_diff = None # return if self.diversion == True: if (n > self.entry2 and self.large_diff == True): if self.portfolio.total_portfolio_value>= self.prev_cap: self.liquidate(tag = 'Diversion; Win') else: self.liquidate(tag = 'Diversion; Loss') 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 elif (n < self.entry4 and self.large_diff == False): if self.portfolio.total_portfolio_value>= self.prev_cap: self.liquidate(tag = 'Diversion; Win') else: self.liquidate(tag = 'Diversion; Loss') 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 # elif : # if self.portfolio.total_portfolio_value>= self.prev_cap: # self.liquidate(tag = 'Diversion; Win') # else: # self.liquidate(tag = 'Diversion; Loss') # 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 # sigma = stats[3].std() # mean = stats[3].mean() # last_spread = stats[3][-1] # 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.debug(f"exit position: z score is {stats[1][-1]}") # self.diversion = False else: if ( n < self.exit1 and self.large_diff == True): if self.portfolio.total_portfolio_value>= self.prev_cap: self.liquidate(tag = 'Mean Reversion; Win') else: self.liquidate(tag = 'Mean Reversion; Loss') self.diversion = None self.prev_cap = None self.large_diff = None # self.debug(f"exit position: z score is {stats[1][-1]}") elif (n > self.exit2 and self.large_diff == False): if self.portfolio.total_portfolio_value>= self.prev_cap: self.liquidate(tag = 'Mean Reversion; Win') else: self.liquidate(tag = 'Mean Reversion; Loss') self.prev_cap = None self.large_diff = None self.diversion = None # 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.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.ratio_60: # df = pd.DataFrame.from_dict(self.ratio_60, orient='index',columns=['ratio']) # file_name = 'CalendarSpread/ratio_60' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile25_60_30_pos: # df = pd.DataFrame.from_dict(self.quantile25_60_30_pos, orient='index',columns=['quantile25_pos']) # file_name = 'CalendarSpread/quantile25_60_30_pos' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile50_60_30_pos: # df = pd.DataFrame.from_dict(self.quantile50_60_30_pos, orient='index',columns=['quantile50_pos']) # file_name = 'CalendarSpread/quantile50_60_30_pos' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile75_60_30_pos: # df = pd.DataFrame.from_dict(self.quantile75_60_30_pos, orient='index',columns=['quantile75_pos']) # file_name = 'CalendarSpread/quantile75_60_30_pos' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile25_60_30_neg: # df = pd.DataFrame.from_dict(self.quantile25_60_30_neg, orient='index',columns=['quantile25_neg']) # file_name = 'CalendarSpread/quantile25_60_30_neg' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile50_60_30_neg: # df = pd.DataFrame.from_dict(self.quantile50_60_30_neg, orient='index',columns=['quantile50_neg']) # file_name = 'CalendarSpread/quantile50_60_30_neg' # self.object_store.SaveBytes(file_name, pickle.dumps(df)) # if self.quantile75_60_30_neg: # df = pd.DataFrame.from_dict(self.quantile75_60_30_neg, orient='index',columns=['quantile75_neg']) # file_name = 'CalendarSpread/quantile75_60_30_neg' # 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) 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_vix_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_vix_contract.symbol, -qty) # self.liquidate(tag = 'margin call')