| Overall Statistics |
|
Total Orders 200 Average Win 3.58% Average Loss -2.38% Compounding Annual Return 2.245% Drawdown 40.400% Expectancy 0.050 Start Equity 1000000 End Equity 1021513.96 Net Profit 2.151% Sharpe Ratio 0.041 Sortino Ratio 0.029 Probabilistic Sharpe Ratio 18.360% Loss Rate 58% Win Rate 42% Profit-Loss Ratio 1.50 Alpha 0 Beta 0 Annual Standard Deviation 0.307 Annual Variance 0.094 Information Ratio 0.22 Tracking Error 0.307 Treynor Ratio 0 Total Fees $19890.91 Estimated Strategy Capacity $8800000.00 Lowest Capacity Asset VX YPDDEQD90YQX Portfolio Turnover 37.33% |
#region imports
from AlgorithmImports import *
#endregion
general_setting = {
"lookback": 30,
"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.367589
self.entry2 = 1.882818
self.entry3 = -1.049760
self.entry4 = -1.810735
self.exit1 = 0.749334
self.exit2 = -0.448661
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')