| 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')