Overall Statistics |
Total Orders 106 Average Win 4.90% Average Loss -3.77% Compounding Annual Return 4.581% Drawdown 35.200% Expectancy 0.061 Start Equity 1000000 End Equity 1038132.6 Net Profit 3.813% Sharpe Ratio 0.027 Sortino Ratio 0.021 Probabilistic Sharpe Ratio 20.966% Loss Rate 54% Win Rate 46% Profit-Loss Ratio 1.30 Alpha 0 Beta 0 Annual Standard Deviation 0.236 Annual Variance 0.056 Information Ratio 0.259 Tracking Error 0.236 Treynor Ratio 0 Total Fees $10704.98 Estimated Strategy Capacity $7000000.00 Lowest Capacity Asset VX YMOVLKIPJ10P Portfolio Turnover 23.40% |
# region imports from AlgorithmImports import * 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 # endregion from config import general_setting class BasicTemplateFuturesAlgorithm(QCAlgorithm): def Initialize(self): self.debug("start calendar spread algo") self.SetStartDate(2023, 10, 8) self.SetCash(1000000) self.universe_settings.resolution = Resolution.MINUTE # lookback frequency settings self.lookback = general_setting['lookback'] self.lookback_RESOLUTION = general_setting['lookback_RESOLUTION'] self.enter = general_setting["enter_level"] self.exit = general_setting["exit_level"] # Subscribe and set our expiry filter for the futures chain future1 = self.AddFuture(Futures.Metals.GOLD, resolution=Resolution.MINUTE) future1.SetFilter(timedelta(0), timedelta(365)) # benchmark = self.AddEquity("SPY") # self.SetBenchmark(benchmark.Symbol) seeder = FuncSecuritySeeder(self.GetLastKnownPrices) self.SetSecurityInitializer(lambda security: seeder.SeedSecurity(security)) self.gold1_contract = None self.gold2_contract = None self.gold3_contract = None self.minute_counter = 0 self.Schedule.On(self.date_rules.every_day(), self.TimeRules.At(18,0), self.reset_minute_counter) # Check Take profit and STOP LOSS every minute def reset_minute_counter(self): self.minute_counter = 0 def stats(self, symbols, method="Regression"): # lookback here refers to market hour, whereas additional extended-market-hour data are also included. if self.lookback_RESOLUTION == "MINUTE": df_Gold1 = self.History(symbols[0], self.lookback, Resolution.MINUTE) df_Gold2 = self.History(symbols[1], self.lookback, Resolution.MINUTE) df_Gold3 = self.History(symbols[2], self.lookback, Resolution.MINUTE) elif self.lookback_RESOLUTION == "HOUR": df_Gold1 = self.History(symbols[0], self.lookback, Resolution.HOUR) df_Gold2 = self.History(symbols[1], self.lookback, Resolution.HOUR) df_Gold3 = self.History(symbols[2], self.lookback, Resolution.HOUR) else: df_Gold1 = self.History(symbols[0], self.lookback, Resolution.DAILY) df_Gold2 = self.History(symbols[1], self.lookback, Resolution.DAILY) df_Gold3 = self.History(symbols[2], self.lookback, Resolution.DAILY) if df_Gold1.empty or df_Gold2.empty: return 0 df_Gold1 = df_Gold1["close"] df_Gold2 = df_Gold2["close"] df_Gold3 = df_Gold3["close"] Gold1_log = np.array(df_Gold1.apply(lambda x: math.log(x))) Gold2_log = np.array(df_Gold2.apply(lambda x: math.log(x))) Gold3_log = np.array(df_Gold3.apply(lambda x: math.log(x))) # Gold1 & Gold2 Regression and ADF test X1 = sm.add_constant(Gold1_log) Y1 = Gold2_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] test_passed1 = p_value1 <= general_setting['p_value_threshold'] self.debug(f"p value is {p_value1}") # p 越小越显著 # Gold1 & Gold3 Regression and ADF test X2 = sm.add_constant(Gold1_log) Y2 = Gold3_log model2 = sm.OLS(Y2, X2) results2 = model2.fit() sigma2 = math.sqrt(results2.mse_resid) slope2 = results2.params[1] intercept2 = results2.params[0] res2 = results2.resid zscore2 = res2/sigma2 adf2 = adfuller(res2) p_value2 = adf2[1] test_passed2 = p_value2 <= general_setting['p_value_threshold'] # Gold1 & Gold3 Regression and ADF test X3 = sm.add_constant(Gold2_log) Y3 = Gold3_log model3 = sm.OLS(Y3, X3) results3 = model3.fit() sigma3 = math.sqrt(results3.mse_resid) slope3 = results3.params[1] intercept3 = results3.params[0] res3 = results3.resid zscore3 = res3/sigma3 adf3 = adfuller(res3) p_value3 = adf3[1] test_passed3 = p_value3 <= general_setting['p_value_threshold'] # Kalman Filtering to get parameters if method == "Kalman_Filter": obs_mat = sm.add_constant(Gold1_log, prepend=False)[:, np.newaxis] trans_cov = 1e-5 / (1 - 1e-5) * np.eye(2) kf = KalmanFilter(n_dim_obs=1, n_dim_state=2, initial_state_mean=np.ones(2), initial_state_covariance=np.ones((2, 2)), transition_matrices=np.eye(2), observation_matrices=obs_mat, observation_covariance=0.5, transition_covariance=0.000001 * np.eye(2)) state_means, state_covs = kf.filter(Gold2_log) slope = state_means[:, 0][-1] intercept = state_means[:, 1][-1] self.printed = True return [test_passed1, zscore1, slope1] def OnData(self,slice): for chain in slice.FutureChains: contracts = list(filter(lambda x: x.Expiry > self.Time + timedelta(90), chain.Value)) if len(contracts) == 0: continue front1 = sorted(contracts, key = lambda x: x.Expiry)[0] front2 = sorted(contracts, key = lambda x: x.Expiry)[1] front3 = sorted(contracts, key = lambda x: x.Expiry)[2] self.Debug (" Expiry " + str(front3.Expiry) + " - " + str(front3.Symbol)) self.gold1_contract = front1.Symbol self.gold2_contract = front2.Symbol self.gold3_contract = front3.Symbol
#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": 2, }
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): 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 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: if self.roll_signal == False: 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() 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] self.debug(f'mean is {mean}, sigma is {sigma}, last_spread is {last_spread}') # 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.15*sigma and (last_spread < mean + 1.78*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 if (last_spread > mean + 1.78*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.debug(f"enter position: z score is {stats[1][-1]}") 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.debug(f"enter position: z score is {stats[1][-1]}") self.diversion = True 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.debug(f"enter position: z score is {stats[1][-1]}") self.trade_signal = False except: return else: # exit signal 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] # 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}') # Roll over if ((self.first_vix_expiry.date() - self.time.date()).days <= self.rollover_days): 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 (last_spread > mean + 1.78 * sigma 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') 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 elif (last_spread < mean - 1.73*sigma 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') 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 - {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.debug(f"exit position: z score is {stats[1][-1]}") # 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 (last_spread < mean -0 * sigma 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 (last_spread > mean + 0*sigma 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 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)) 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_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')