-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmain.py
More file actions
267 lines (211 loc) · 10.8 KB
/
Copy pathmain.py
File metadata and controls
267 lines (211 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import time
import Model
import config
import random
import itertools
import Containers
import multiprocessing as mp
from itertools import combinations
from UtilFuncs import CorrelationFuncs
from OrderManagement import OrderInput
from dataclasses import dataclass
class CopulasAlgorithm(QCAlgorithm):
def Initialize(self):
# Set Start Date so that backtest has 5+ years of data
self.SetStartDate(2015, 1, 1)
# No need to set End Date as the final submission will be tested
# up until the review date
# Set $1m Strategy Cash to trade significant AUM
self.SetCash(config.PortfolioParameters.INIT_FUNDS)
self.max_account_risk = config.PortfolioParameters.MAX_ACCOUNT_RISK
# Use the Alpha Streams Brokerage Model, developed in conjunction with
# funds to model their actual fees, costs, etc.
# Please do not add any additional reality modelling, such as Slippage, Fees, Buying Power, etc.
self.SetBrokerageModel(AlphaStreamsBrokerageModel())
# Define symbols used for Manual Universe Selection
self.symbols = self.load_symbols()
# Use Manual Universe Selection
self.SetUniverseSelection(ManualUniverseSelectionModel(self.symbols))
# Initialise Universe Settings
self._set_universe_settings()
# order execution setting
self._ASYNC_ORDER = config.OrderParameters.ASYNC
# set the model type we are going to use
self.Model = Model.BivariateNonParametricCopula
# Initialise member variable for copulas container
self.copulas = None
self.pairs_dict = None
# Intialise dictionary to contain rolling windows for each pair
self.last_prices = {}
# initialise loggers
self.initialise_loggers()
# Schedule model initialisation - Every week on Saturday at midnight
self.Schedule.On(self.DateRules.Every(DayOfWeek.Saturday),
self.TimeRules.At(0,0),
self.initialise_models)
# keep track of the day
self.day = None
# In your initialize method:
# Note - use single quotation marks: ' instead of double "
# Chart - Master Container for the Chart:
spread_plot = Chart('Spread plot')
comb = combinations(self.symbols,2)
for i, pair in enumerate(list(comb)):
if i > 10:
break
spread_plot.AddSeries(Series(str(pair), SeriesType.Line, 0))
self.AddChart(spread_plot)
def log_and_debug(self, msg):
self.Log(msg)
self.Debug(msg)
def initialise_loggers(self):
'''
We use a classmethod for any custom class objects that requires logging
within the QC algorithm framework to store the algorithm instance as a
class variable
'''
self.Model.register_QC(algo=self)
def _set_universe_settings(self):
'''
Setup for Universe
'''
self.UniverseSettings.Resolution = config.ModelParameters.RESOLUTION
# self.UniverseSettings.Leverage =
# self.UniverseSettings.FillForward =
# self.UniverseSettings.MinimumTimeInUniverse =
# self.UniverseSettings.ExtendedMarketHours =
def _get_historical_data(self, symbols, lookback, resolution, ohlcv):
'''
Get historical data for our algorithm
'''
df = self.History(symbols, lookback, resolution)
return Containers.Data(df, ohlcv=ohlcv)
def load_symbols(self):
'''
Load pre-defined sets into the instruments universe
Returns:
Tickers: list of tickers used in this universe
'''
# Symbols list
symbols = []
# SPY ETFs
self.SP500_Tickers = ["SPY","XLK", "VGT", "IYW", "IGV"]
for ticker in self.SP500_Tickers:
# Add the tickers to algorithm
symbol = self.AddEquity(ticker, self.UniverseSettings.Resolution)
# record symbols used
symbols.append(ticker)
return symbols
def initialise_models(self):
'''
Return a CopulaModel object for each asset data
'''
# initialise dictionary for containing copula models
self.copulas = Containers.ModelContainer()
Data = self._get_historical_data(self.symbols,
config.ModelParameters.LOOKBACK,
config.ModelParameters.RESOLUTION,
config.ModelParameters.OHLCV)
self.log_and_debug(f"Received {len(Data)} datapoints ranging from {Data.index[0]} to {Data.index[-1]}")
# Find the pairs that satisfy our correlation criteria
# Criteria: Kendall Tau correlation > threshold
corr = Data.get_correlations()
pairs_dict = CorrelationFuncs.correlation_above_thresh(corr, config.ModelParameters.CORRELATION_THRESHOLD)
# fit the model
model_generator = Model.ModelFactory()
if not config.ModelParameters.FIT_MULTIPROCESS:
t = time.time()
for pair in pairs_dict.keys():
data_packet1 = Data.get_returns(pair[0])
data_packet2 = Data.get_returns(pair[1])
copula_model = model_generator.get_model(data_packet1, data_packet2)
self.copulas[pair] = copula_model
elapsed = time.time() - t
self.log_and_debug(f"{elapsed}s taken to fit {len(pairs_dict)} models")
else:
# allocate cpu resource
num_workers = len(pairs_dict) if mp.cpu_count() > len(pairs_dict) else mp.cpu_count()
self.Debug(f"Using {num_workers} processses to fit {len(pairs_dict)} models")
# dispatch workers from pool
async_results = []
with mp.Pool() as pool:
for pair in pairs_dict.keys():
data_packet1 = Data.get_returns(pair[0])
data_packet2 = Data.get_returns(pair[1])
get_proc = pool.apply_async(func=model_generator.get_model,
args=(data_packet1, data_packet2))
async_results.append((pair, get_proc))
# wait for pool to return results
t = time.time()
for res in async_results:
pair, model_getter = res
copula_model = model_getter.get()
self.copulas[pair] = copula_model
elapsed = time.time() - t
self.log_and_debug(f"{elapsed}s taken to fit {len(pairs_dict)} models")
def OnData(self, data):
'''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
Arguments:
data: Slice object keyed by symbol containing the stock data
'''
if not self.copulas:
return
# daily trades only
if self.Time.day == self.day:
return
for pair in self.copulas.keys():
sym1, sym2 = pair
close1 = data.Bars[sym1].Close
close2 = data.Bars[sym2].Close
self.Plot('Spread plot', str(pair), close1 - close2)
# TODO: We could have a rolling window of prices from which we calculate the returns
# From this we calculate the ECDF and copula. Using some distance measure from the
# current and new distribution, we can quantify how much the distribution has changed
# and therefore judge the optimal frequency of refitting needed to keep the model up-to-date.
# Use just a dict to retain the last price for now.
CALC_MI = True
# handle the case when we first encounter this pair
try:
last_close1 = self.last_prices[sym1]
last_close2 = self.last_prices[sym2]
except KeyError:
self.last_prices[sym1] = close1
self.last_prices[sym2] = close2
CALC_MI = False
if CALC_MI:
d1 = close1 - last_close1
u = self.copulas[pair].price_to_marginal(price=d1, symbol=sym1)
d2 = close2 - last_close2
v = self.copulas[pair].price_to_marginal(price=d2, symbol=sym2)
grid_width = 0.001
mi_u_v, mi_v_u = self.copulas[pair].mispricing_index(u,v,grid_width)
# u over-priced and v under-priced
u_overpriced = mi_u_v > config.TradeParameters.MI_UPPER_THRESH
v_underpriced = mi_v_u < config.TradeParameters.MI_LOWER_THRESH
# u under-priced and v over-priced
u_underpriced = mi_u_v < config.TradeParameters.MI_LOWER_THRESH
v_overpriced = mi_v_u > config.TradeParameters.MI_UPPER_THRESH
if u_overpriced and v_underpriced:
self.Debug(f"{self.Time}: Sell u and Buy v - C(U|V) : {mi_u_v}, C(V|U) : {mi_v_u}")
# ratio for equivalent exposure to each leg of pair - price(sym1) / price(sym2)
p_ratio = close1 / close2
u_quantity = self.CalculateOrderQuantity(sym1, self.max_account_risk)
v_quantity = -p_ratio * u_quantity
order_status_u = self.MarketOrder(sym1, u_quantity, self._ASYNC_ORDER)
order_status_v = self.MarketOrder(sym2, v_quantity, self._ASYNC_ORDER)
elif u_underpriced and v_overpriced:
self.Debug(f"{self.Time}: Buy u and Sell v - C(U|V) : {mi_u_v}, C(V|U) : {mi_v_u}")
p_ratio = close1 / close2
u_quantity = -self.CalculateOrderQuantity(sym1, self.max_account_risk)
v_quantity = -p_ratio * u_quantity
order_status_u = self.MarketOrder(sym1, u_quantity, self._ASYNC_ORDER)
order_status_v = self.MarketOrder(sym2, v_quantity, self._ASYNC_ORDER)
self.day = self.Time.day
def OnOrderEvent(self, orderEvent):
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if orderEvent.Status == OrderStatus.Filled:
self.Log("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent))
elif orderEvent.Status == OrderStatus.Invalid:
self.Log("Invalid {0}: {1}: {2}".format(self.Time, order.Type, orderEvent))
else:
pass