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