PyAlgoEngine 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
- PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
- PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
- PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
- PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
- algo_engine/__init__.py +41 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +331 -0
- algo_engine/apps/backtest/tester.py +254 -0
- algo_engine/apps/backtest/web_app.py +127 -0
- algo_engine/apps/bokeh_server.py +205 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +39 -0
- algo_engine/backtest/__init__.py +19 -0
- algo_engine/backtest/__main__.py +51 -0
- algo_engine/backtest/metrics.py +179 -0
- algo_engine/backtest/replay.py +261 -0
- algo_engine/backtest/sim_match.py +295 -0
- algo_engine/base/__init__.py +40 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/market_buffer.py +571 -0
- algo_engine/base/market_utils.py +3092 -0
- algo_engine/base/market_utils_nt.py +188 -0
- algo_engine/base/market_utils_posix.py +3004 -0
- algo_engine/base/technical_analysis.py +406 -0
- algo_engine/base/telemetrics.py +78 -0
- algo_engine/base/trade_utils.py +709 -0
- algo_engine/engine/__init__.py +28 -0
- algo_engine/engine/algo_engine.py +901 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/market_engine.py +370 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +239 -0
- algo_engine/profile/__init__.py +121 -0
- algo_engine/profile/cn.py +175 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +440 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- algo_engine/utils/data_utils.py +251 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
from . import LOGGER
|
|
8
|
+
from ..backtest import SimMatch, ProgressiveReplay
|
|
9
|
+
from ..base import MarketData, TradeReport, TradeInstruction, TransactionSide
|
|
10
|
+
from ..engine import PositionManagementService, TOPIC, EVENT_ENGINE
|
|
11
|
+
|
|
12
|
+
LOGGER = LOGGER.getChild('Strategy')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StrategyEngineTemplate(object, metaclass=abc.ABCMeta):
|
|
16
|
+
def __init__(self, position_tracker: PositionManagementService):
|
|
17
|
+
self.position_tracker = position_tracker
|
|
18
|
+
|
|
19
|
+
def __call__(self, **kwargs):
|
|
20
|
+
if 'market_data' in kwargs:
|
|
21
|
+
self.on_market_data(market_data=kwargs['market_data'])
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def on_market_data(self, market_data: MarketData, **kwargs): ...
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def on_report(self, report: TradeReport, **kwargs): ...
|
|
28
|
+
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def on_order(self, order: TradeInstruction, **kwargs): ...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def mds(self):
|
|
34
|
+
return self.position_tracker.dma.mds
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def dma(self):
|
|
38
|
+
return self.position_tracker.dma
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def risk_profile(self):
|
|
42
|
+
return self.position_tracker.dma.risk_profile
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def balance(self):
|
|
46
|
+
return self.position_tracker.dma.risk_profile.balance
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def inventory(self):
|
|
50
|
+
return self.position_tracker.dma.risk_profile.balance.inventory
|
|
51
|
+
|
|
52
|
+
@cached_property
|
|
53
|
+
def lock(self):
|
|
54
|
+
from . import REPLAY_LOCK
|
|
55
|
+
return REPLAY_LOCK
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StrategyEngine(StrategyEngineTemplate):
|
|
59
|
+
def __init__(self, position_tracker: PositionManagementService, **kwargs):
|
|
60
|
+
super().__init__(position_tracker=position_tracker)
|
|
61
|
+
|
|
62
|
+
self.event_engine = kwargs.pop('event_engine', EVENT_ENGINE)
|
|
63
|
+
self.topic_set = kwargs.pop('topic_set', TOPIC)
|
|
64
|
+
|
|
65
|
+
self._on_market_data = []
|
|
66
|
+
self._on_report = []
|
|
67
|
+
self._on_order = []
|
|
68
|
+
self._on_eod = []
|
|
69
|
+
self._on_bod = []
|
|
70
|
+
self.subscription = set()
|
|
71
|
+
|
|
72
|
+
self.attach_strategy(strategy=kwargs.pop('strategy', None))
|
|
73
|
+
self.add_handler(**kwargs)
|
|
74
|
+
|
|
75
|
+
def __call__(self, **kwargs):
|
|
76
|
+
if 'market_data' in kwargs:
|
|
77
|
+
self.on_market_data(market_data=kwargs['market_data'])
|
|
78
|
+
|
|
79
|
+
if self.lock.locked():
|
|
80
|
+
self.lock.release()
|
|
81
|
+
|
|
82
|
+
def add_handler(self, **kwargs):
|
|
83
|
+
if 'on_market_data' in kwargs:
|
|
84
|
+
self._on_market_data.append(kwargs['on_market_data'])
|
|
85
|
+
|
|
86
|
+
if 'on_report' in kwargs:
|
|
87
|
+
self._on_report.append(kwargs['on_report'])
|
|
88
|
+
|
|
89
|
+
if 'on_order' in kwargs:
|
|
90
|
+
self._on_order.append(kwargs['on_order'])
|
|
91
|
+
|
|
92
|
+
if 'on_eod' in kwargs:
|
|
93
|
+
self._on_eod.append(kwargs['on_eod'])
|
|
94
|
+
|
|
95
|
+
if 'on_bod' in kwargs:
|
|
96
|
+
self._on_bod.append(kwargs['on_bod'])
|
|
97
|
+
|
|
98
|
+
def remove_handler(self, **kwargs):
|
|
99
|
+
if 'on_market_data' in kwargs:
|
|
100
|
+
self._on_market_data.remove(kwargs['on_market_data'])
|
|
101
|
+
|
|
102
|
+
if 'on_report' in kwargs:
|
|
103
|
+
self._on_report.remove(kwargs['on_report'])
|
|
104
|
+
|
|
105
|
+
if 'on_order' in kwargs:
|
|
106
|
+
self._on_order.remove(kwargs['on_order'])
|
|
107
|
+
|
|
108
|
+
if 'on_eod' in kwargs:
|
|
109
|
+
self._on_eod.remove(kwargs['on_eod'])
|
|
110
|
+
|
|
111
|
+
if 'on_bod' in kwargs:
|
|
112
|
+
self._on_bod.remove(kwargs['on_bod'])
|
|
113
|
+
|
|
114
|
+
def add_handler_safe(self, **kwargs):
|
|
115
|
+
if 'on_market_data' in kwargs:
|
|
116
|
+
if (handler := kwargs['on_market_data']) in self._on_market_data:
|
|
117
|
+
LOGGER.warning(f'on_market_data handler {handler} already registered, skipped!')
|
|
118
|
+
else:
|
|
119
|
+
self._on_market_data.append(handler)
|
|
120
|
+
|
|
121
|
+
if 'on_report' in kwargs:
|
|
122
|
+
if (handler := kwargs['on_report']) in self._on_report:
|
|
123
|
+
LOGGER.warning(f'on_report handler {handler} already registered, skipped!')
|
|
124
|
+
else:
|
|
125
|
+
self._on_report.append(handler)
|
|
126
|
+
|
|
127
|
+
if 'on_order' in kwargs:
|
|
128
|
+
if (handler := kwargs['on_order']) in self._on_order:
|
|
129
|
+
LOGGER.warning(f'on_order handler {handler} already registered, skipped!')
|
|
130
|
+
else:
|
|
131
|
+
self._on_order.append(handler)
|
|
132
|
+
|
|
133
|
+
if 'on_eod' in kwargs:
|
|
134
|
+
if (handler := kwargs['on_eod']) in self._on_eod:
|
|
135
|
+
LOGGER.warning(f'on_eod handler {handler} already registered, skipped!')
|
|
136
|
+
else:
|
|
137
|
+
self._on_eod.append(handler)
|
|
138
|
+
|
|
139
|
+
if 'on_bod' in kwargs:
|
|
140
|
+
if (handler := kwargs['on_bod']) in self._on_bod:
|
|
141
|
+
LOGGER.warning(f'on_bod handler {handler} already registered, skipped!')
|
|
142
|
+
else:
|
|
143
|
+
self._on_bod.append(handler)
|
|
144
|
+
|
|
145
|
+
def remove_handler_safe(self, **kwargs):
|
|
146
|
+
if 'on_market_data' in kwargs:
|
|
147
|
+
if (handler := kwargs['on_market_data']) in self._on_market_data:
|
|
148
|
+
self._on_market_data.remove(handler)
|
|
149
|
+
|
|
150
|
+
if 'on_report' in kwargs:
|
|
151
|
+
if (handler := kwargs['on_report']) in self._on_report:
|
|
152
|
+
self._on_report.remove(handler)
|
|
153
|
+
|
|
154
|
+
if 'on_order' in kwargs:
|
|
155
|
+
if (handler := kwargs['on_order']) in self._on_order:
|
|
156
|
+
self._on_order.remove(handler)
|
|
157
|
+
|
|
158
|
+
if 'on_eod' in kwargs:
|
|
159
|
+
if (handler := kwargs['on_eod']) in self._on_eod:
|
|
160
|
+
self._on_eod.remove(handler)
|
|
161
|
+
|
|
162
|
+
if 'on_bod' in kwargs:
|
|
163
|
+
if (handler := kwargs['on_bod']) in self._on_bod:
|
|
164
|
+
self._on_bod.remove(handler)
|
|
165
|
+
|
|
166
|
+
def attach_strategy(self, strategy: object):
|
|
167
|
+
if callable(handler := getattr(strategy, 'on_market_data', None)):
|
|
168
|
+
self._on_market_data.append(handler)
|
|
169
|
+
|
|
170
|
+
if callable(handler := getattr(strategy, 'on_report', None)):
|
|
171
|
+
self._on_report.append(handler)
|
|
172
|
+
|
|
173
|
+
if callable(handler := getattr(strategy, 'on_order', None)):
|
|
174
|
+
self._on_order.append(handler)
|
|
175
|
+
|
|
176
|
+
if callable(handler := getattr(strategy, 'on_eod', None)):
|
|
177
|
+
self._on_eod.append(handler)
|
|
178
|
+
|
|
179
|
+
if callable(handler := getattr(strategy, 'on_bod', None)):
|
|
180
|
+
self._on_bod.append(handler)
|
|
181
|
+
|
|
182
|
+
def subscribe(self, ticker: str):
|
|
183
|
+
self.subscription.add(ticker)
|
|
184
|
+
|
|
185
|
+
def on_market_data(self, market_data: MarketData, **kwargs):
|
|
186
|
+
|
|
187
|
+
if market_data.ticker not in self.subscription:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
for handler in self._on_market_data:
|
|
191
|
+
handler(market_data=market_data, **kwargs)
|
|
192
|
+
|
|
193
|
+
def on_report(self, report: TradeReport, **kwargs):
|
|
194
|
+
|
|
195
|
+
for handler in self._on_report:
|
|
196
|
+
handler(report=report, **kwargs)
|
|
197
|
+
|
|
198
|
+
def on_order(self, order: TradeInstruction, **kwargs):
|
|
199
|
+
|
|
200
|
+
for handler in self._on_order:
|
|
201
|
+
handler(order=order, **kwargs)
|
|
202
|
+
|
|
203
|
+
def register(self, event_engine=None, topic_set=None, auto_register: bool = True):
|
|
204
|
+
if event_engine is None:
|
|
205
|
+
event_engine = self.event_engine
|
|
206
|
+
|
|
207
|
+
if topic_set is None:
|
|
208
|
+
topic_set = self.topic_set
|
|
209
|
+
|
|
210
|
+
if auto_register:
|
|
211
|
+
event_engine.register_handler(topic=topic_set.realtime, handler=self.mds)
|
|
212
|
+
event_engine.register_handler(topic=topic_set.realtime, handler=self.position_tracker.on_market_data)
|
|
213
|
+
event_engine.register_handler(topic=topic_set.on_order, handler=self.balance.on_order)
|
|
214
|
+
event_engine.register_handler(topic=topic_set.on_report, handler=self.balance.on_report)
|
|
215
|
+
|
|
216
|
+
event_engine.register_handler(topic=topic_set.realtime, handler=self.__call__)
|
|
217
|
+
event_engine.register_handler(topic=topic_set.on_order, handler=self.on_order)
|
|
218
|
+
event_engine.register_handler(topic=topic_set.on_report, handler=self.on_report)
|
|
219
|
+
|
|
220
|
+
def unregister(self, event_engine=None, topic_set=None, auto_unregister: bool = True):
|
|
221
|
+
if event_engine is None:
|
|
222
|
+
event_engine = self.event_engine
|
|
223
|
+
|
|
224
|
+
if topic_set is None:
|
|
225
|
+
topic_set = self.topic_set
|
|
226
|
+
|
|
227
|
+
if auto_unregister:
|
|
228
|
+
event_engine.unregister_handler(topic=topic_set.realtime, handler=self.mds)
|
|
229
|
+
event_engine.unregister_handler(topic=topic_set.realtime, handler=self.position_tracker.on_market_data)
|
|
230
|
+
event_engine.unregister_handler(topic=topic_set.on_order, handler=self.balance.on_order)
|
|
231
|
+
event_engine.unregister_handler(topic=topic_set.on_report, handler=self.balance.on_report)
|
|
232
|
+
|
|
233
|
+
event_engine.unregister_handler(topic=topic_set.realtime, handler=self.__call__)
|
|
234
|
+
event_engine.unregister_handler(topic=topic_set.on_order, handler=self.on_order)
|
|
235
|
+
event_engine.unregister_handler(topic=topic_set.on_report, handler=self.on_report)
|
|
236
|
+
|
|
237
|
+
def cancel(self, ticker: str, side: TransactionSide = None, algo_id: str = None, order_id: str = None, **kwargs):
|
|
238
|
+
position_tracker = self.position_tracker
|
|
239
|
+
|
|
240
|
+
if algo_id is not None:
|
|
241
|
+
algo_id = position_tracker.reversed_order_mapping.get(order_id).algo_id
|
|
242
|
+
if algo_id:
|
|
243
|
+
LOGGER.info(f'No algo_id specified, found algo {algo_id} associated with order_id {order_id}! Canceling all trade action associated with algo')
|
|
244
|
+
LOGGER.warning('Strategy should not cancel single trade order, this will break the algo_engine Consistency!')
|
|
245
|
+
|
|
246
|
+
if not algo_id:
|
|
247
|
+
LOGGER.warning(f'No algo_id given! Canceling all {ticker} {side.side_name} algos!')
|
|
248
|
+
|
|
249
|
+
for _algo_id in list(self.algos):
|
|
250
|
+
algo = self.algos.get(_algo_id)
|
|
251
|
+
|
|
252
|
+
if algo is None:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
if algo.ticker == ticker and algo.side.sign == side.sign:
|
|
256
|
+
algo.cancel(**kwargs)
|
|
257
|
+
else:
|
|
258
|
+
algo = self.algos.get(algo_id)
|
|
259
|
+
|
|
260
|
+
if algo is None:
|
|
261
|
+
LOGGER.error(f'{self} have no algo with algo_id {algo_id}! Cancel signal ignored! Manual intervention required!')
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
if algo.ticker == ticker and algo.side.sign == side.sign:
|
|
265
|
+
algo.cancel(**kwargs)
|
|
266
|
+
|
|
267
|
+
def stop(self):
|
|
268
|
+
LOGGER.debug(f'All algo should be self-deactivated on cancel, to be sure {self} will deactivate all the algos!')
|
|
269
|
+
for algo_id in list(self.algos):
|
|
270
|
+
algo = self.algos.get(algo_id)
|
|
271
|
+
|
|
272
|
+
if algo is None:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
algo.is_active = False
|
|
276
|
+
|
|
277
|
+
LOGGER.info(f'{self} canceling all the algos')
|
|
278
|
+
for ticker in self.subscription:
|
|
279
|
+
self.cancel(ticker=ticker)
|
|
280
|
+
|
|
281
|
+
def unwind_pos(self, ticker: str, volume: float, side: TransactionSide = None, limit_price: float = None, algo: str = None, safe=True, **kwargs) -> tuple[float, float]:
|
|
282
|
+
"""
|
|
283
|
+
unwind method provide a safe way to unwind position of given ticker.
|
|
284
|
+
|
|
285
|
+
:param ticker: the given exposure
|
|
286
|
+
:param volume: the target unwinding volume, should be a positive number
|
|
287
|
+
:param side: the trade action side, e.g. if strategy wishes to sell (in order to unwind long position), then side = TransactionSide.Sell_to_Unwind
|
|
288
|
+
:param limit_price: Optional, a limit price
|
|
289
|
+
:param algo: Optional the algo to be used to execute unwinding action
|
|
290
|
+
:param safe: True -> unwind volume should not exceed the exposed volume; False -> can flip position. Default is safe=True
|
|
291
|
+
:param kwargs: other kwargs passing to `algo.launch`
|
|
292
|
+
:return: executed volume, remaining volume
|
|
293
|
+
"""
|
|
294
|
+
position_tracker = self.position_tracker
|
|
295
|
+
exposure = position_tracker.exposure_volume.get(ticker, 0.)
|
|
296
|
+
working_long = position_tracker.working_volume['Long'].get(ticker, 0.)
|
|
297
|
+
working_short = position_tracker.working_volume['Short'].get(ticker, 0.)
|
|
298
|
+
executed, remains = 0., volume
|
|
299
|
+
|
|
300
|
+
if not exposure:
|
|
301
|
+
LOGGER.warning(f'{self} found no {ticker} exposure! Unwind signal ignored! Check PositionManagementService!')
|
|
302
|
+
return executed, remains
|
|
303
|
+
|
|
304
|
+
if side is not None and exposure * side.sign > 0:
|
|
305
|
+
LOGGER.warning(f'{self} found {ticker} exposure {exposure}, however strategy is trying to execute {side.side_name} unwind action! Unwind signal ignored! Check PositionManagementService!')
|
|
306
|
+
return executed, remains
|
|
307
|
+
|
|
308
|
+
# then it must be
|
|
309
|
+
side = TransactionSide.Sell_to_Unwind if exposure > 0 else TransactionSide.Buy_to_Cover
|
|
310
|
+
|
|
311
|
+
if side.sign > 0: # short position, buy action
|
|
312
|
+
working_open = working_short
|
|
313
|
+
working_unwind = working_long
|
|
314
|
+
else: # long position, sell action
|
|
315
|
+
working_open = working_long
|
|
316
|
+
working_unwind = working_short
|
|
317
|
+
|
|
318
|
+
if working_open:
|
|
319
|
+
LOGGER.warning(f'{self} found {ticker} exposure {exposure}, still having {(-side).side_name} order {working_open}! Consider canceling these instruction before unwinding position!')
|
|
320
|
+
|
|
321
|
+
if safe:
|
|
322
|
+
unwind_volume_limit = max(abs(exposure) - abs(working_unwind), 0)
|
|
323
|
+
|
|
324
|
+
if abs(volume) > unwind_volume_limit:
|
|
325
|
+
LOGGER.warning(f'{self} found {ticker} exposure {exposure}, long order {working_long}, short order {working_short}. The unwinding signal {side.sign} {volume} exceed safe unwinding limit {unwind_volume_limit}!')
|
|
326
|
+
|
|
327
|
+
LOGGER.info(f'{self} adjust {ticker} {side.side_name} unwind volume to {volume}, accommodating safe unwind rules!')
|
|
328
|
+
volume = unwind_volume_limit
|
|
329
|
+
|
|
330
|
+
if volume:
|
|
331
|
+
self.open_pos(
|
|
332
|
+
ticker=ticker,
|
|
333
|
+
side=side,
|
|
334
|
+
volume=abs(volume),
|
|
335
|
+
limit_price=limit_price,
|
|
336
|
+
algo=algo,
|
|
337
|
+
**kwargs
|
|
338
|
+
)
|
|
339
|
+
executed += abs(volume)
|
|
340
|
+
remains -= abs(volume)
|
|
341
|
+
|
|
342
|
+
return executed, remains
|
|
343
|
+
|
|
344
|
+
def open_pos(self, ticker: str, volume: float, side: TransactionSide = None, limit_price: float = None, algo: str = None, **kwargs):
|
|
345
|
+
"""
|
|
346
|
+
a method to open position
|
|
347
|
+
:param ticker: the given ticker
|
|
348
|
+
:param volume: the target open volume
|
|
349
|
+
:param side: trade side
|
|
350
|
+
:param limit_price: Optional limit
|
|
351
|
+
:param algo: Optional the specified algo
|
|
352
|
+
:param kwargs: other keyword used in algo
|
|
353
|
+
:return:
|
|
354
|
+
"""
|
|
355
|
+
target_volume = abs(volume)
|
|
356
|
+
|
|
357
|
+
if not target_volume:
|
|
358
|
+
LOGGER.warning(f'Target open amount is {volume}, check the signal!')
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if side is None:
|
|
362
|
+
trade_side = TransactionSide.Buy_to_Long if volume > 0 else TransactionSide.Sell_to_Short
|
|
363
|
+
LOGGER.warning(f'Trade side of open instruction not specified! Presumed to be {trade_side} by the sign of volume!')
|
|
364
|
+
|
|
365
|
+
algo = self.position_tracker.open(
|
|
366
|
+
ticker=ticker,
|
|
367
|
+
target_volume=target_volume,
|
|
368
|
+
trade_side=side,
|
|
369
|
+
algo=algo,
|
|
370
|
+
limit_price=limit_price,
|
|
371
|
+
**kwargs
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return algo
|
|
375
|
+
|
|
376
|
+
def eod(self, market_date: datetime.date, **kwargs):
|
|
377
|
+
|
|
378
|
+
for handler in self._on_eod:
|
|
379
|
+
handler(market_date=market_date, **kwargs)
|
|
380
|
+
|
|
381
|
+
def bod(self, market_date: datetime.date, **kwargs):
|
|
382
|
+
|
|
383
|
+
for handler in self._on_bod:
|
|
384
|
+
handler(market_date=market_date, **kwargs)
|
|
385
|
+
|
|
386
|
+
def back_test(self, start_date: datetime.date, end_date: datetime.date, data_loader: Callable, **kwargs):
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
def back_test_lite(self, start_date: datetime.date, end_date: datetime.date, data_loader: Callable, **kwargs):
|
|
390
|
+
replay = ProgressiveReplay(
|
|
391
|
+
loader=data_loader,
|
|
392
|
+
tickers=list(self.subscription),
|
|
393
|
+
dtype=['TickData', 'TradeData'],
|
|
394
|
+
start_date=start_date,
|
|
395
|
+
end_date=end_date,
|
|
396
|
+
bod=self.bod,
|
|
397
|
+
eod=self.eod,
|
|
398
|
+
tick_size=kwargs.get('progress_tick_size', 0.001),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
sim_match = {}
|
|
402
|
+
multi_threading = kwargs.get('multi_threading', False)
|
|
403
|
+
_start_ts = 0.
|
|
404
|
+
self.event_engine.start()
|
|
405
|
+
|
|
406
|
+
for _market_data in replay:
|
|
407
|
+
_ticker = _market_data.ticker
|
|
408
|
+
|
|
409
|
+
if not _start_ts:
|
|
410
|
+
_start_ts = time.time()
|
|
411
|
+
|
|
412
|
+
if _ticker not in sim_match:
|
|
413
|
+
_ = sim_match[_ticker] = SimMatch(ticker=_ticker)
|
|
414
|
+
_.register(event_engine=self.event_engine, topic_set=self.topic_set)
|
|
415
|
+
|
|
416
|
+
if multi_threading:
|
|
417
|
+
self.lock.acquire()
|
|
418
|
+
self.event_engine.put(topic=self.topic_set.push(market_data=_market_data), market_data=_market_data)
|
|
419
|
+
else:
|
|
420
|
+
self.mds.on_market_data(market_data=_market_data)
|
|
421
|
+
self.position_tracker.on_market_data(market_data=_market_data)
|
|
422
|
+
self.__call__(market_data=_market_data)
|
|
423
|
+
sim_match[_ticker](market_data=_market_data)
|
|
424
|
+
|
|
425
|
+
LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
|
|
426
|
+
|
|
427
|
+
def reset(self):
|
|
428
|
+
self.subscription.clear()
|
|
429
|
+
self._on_market_data.clear()
|
|
430
|
+
self._on_report.clear()
|
|
431
|
+
self._on_order.clear()
|
|
432
|
+
self._on_eod.clear()
|
|
433
|
+
self._on_bod.clear()
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def algos(self):
|
|
437
|
+
return self.position_tracker.algos
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
__all__ = ['StrategyEngine', 'StrategyEngineTemplate']
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import random
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from datetime import datetime,date
|
|
5
|
+
|
|
6
|
+
import pygit2
|
|
7
|
+
|
|
8
|
+
# Path to your local git repository
|
|
9
|
+
repo_path = pathlib.Path(__file__).parents[2]
|
|
10
|
+
|
|
11
|
+
# Open the repository
|
|
12
|
+
repo = pygit2.Repository(repo_path)
|
|
13
|
+
|
|
14
|
+
# Dictionary to store commits by date
|
|
15
|
+
commits_by_date = defaultdict(list)
|
|
16
|
+
|
|
17
|
+
# Collect commits by date
|
|
18
|
+
for commit in repo.walk(repo.head.target, pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_REVERSE):
|
|
19
|
+
commit_date = datetime.fromtimestamp(commit.commit_time).date()
|
|
20
|
+
commits_by_date[commit_date].append(commit)
|
|
21
|
+
|
|
22
|
+
# Iterate over each date and update commits
|
|
23
|
+
for commit_date, commits in commits_by_date.items():
|
|
24
|
+
|
|
25
|
+
if commit_date < date(2024, 3, 1):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
# Generate a random timestamp between 3am and 4am
|
|
29
|
+
random_hour = 3
|
|
30
|
+
random_minute = random.randint(0, 59)
|
|
31
|
+
random_second = random.randint(0, 59)
|
|
32
|
+
random_time = datetime(commit_date.year, commit_date.month, commit_date.day, random_hour, random_minute, random_second)
|
|
33
|
+
|
|
34
|
+
# Convert to timestamp
|
|
35
|
+
new_commit_time = int(random_time.timestamp())
|
|
36
|
+
|
|
37
|
+
# Update commits for this date
|
|
38
|
+
for index, commit in enumerate(commits):
|
|
39
|
+
# Calculate committer and author timestamps and timezones
|
|
40
|
+
committer = pygit2.Signature(commit.committer.name, commit.committer.email, new_commit_time, commit.committer.offset)
|
|
41
|
+
author = pygit2.Signature(commit.author.name, commit.author.email, new_commit_time, commit.author.offset)
|
|
42
|
+
|
|
43
|
+
# Amend the commit
|
|
44
|
+
repo.amend_commit(commit.id, None, author, committer, commit.message, None)
|
|
45
|
+
|
|
46
|
+
# Print for logging or verification
|
|
47
|
+
print(f"Amended commit {commit.id} to {random_time} (index {index + 1} of {len(commits)} for {commit_date})")
|
|
48
|
+
|
|
49
|
+
print("Amendment complete.")
|