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.
Files changed (43) hide show
  1. PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
  2. PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
  3. PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
  4. PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
  5. PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
  6. algo_engine/__init__.py +41 -0
  7. algo_engine/apps/__init__.py +17 -0
  8. algo_engine/apps/backtest/__init__.py +20 -0
  9. algo_engine/apps/backtest/doc_server.py +331 -0
  10. algo_engine/apps/backtest/tester.py +254 -0
  11. algo_engine/apps/backtest/web_app.py +127 -0
  12. algo_engine/apps/bokeh_server.py +205 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +39 -0
  15. algo_engine/backtest/__init__.py +19 -0
  16. algo_engine/backtest/__main__.py +51 -0
  17. algo_engine/backtest/metrics.py +179 -0
  18. algo_engine/backtest/replay.py +261 -0
  19. algo_engine/backtest/sim_match.py +295 -0
  20. algo_engine/base/__init__.py +40 -0
  21. algo_engine/base/console_utils.py +1070 -0
  22. algo_engine/base/finance_decimal.py +258 -0
  23. algo_engine/base/market_buffer.py +571 -0
  24. algo_engine/base/market_utils.py +3092 -0
  25. algo_engine/base/market_utils_nt.py +188 -0
  26. algo_engine/base/market_utils_posix.py +3004 -0
  27. algo_engine/base/technical_analysis.py +406 -0
  28. algo_engine/base/telemetrics.py +78 -0
  29. algo_engine/base/trade_utils.py +709 -0
  30. algo_engine/engine/__init__.py +28 -0
  31. algo_engine/engine/algo_engine.py +901 -0
  32. algo_engine/engine/event_engine.py +53 -0
  33. algo_engine/engine/market_engine.py +370 -0
  34. algo_engine/engine/trade_engine.py +2037 -0
  35. algo_engine/monitor/__init__.py +15 -0
  36. algo_engine/monitor/advanced_data_interface.py +239 -0
  37. algo_engine/profile/__init__.py +121 -0
  38. algo_engine/profile/cn.py +175 -0
  39. algo_engine/strategy/__init__.py +44 -0
  40. algo_engine/strategy/strategy_engine.py +440 -0
  41. algo_engine/utils/__init__.py +3 -0
  42. algo_engine/utils/commit_regularizer.py +49 -0
  43. 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,3 @@
1
+ from .data_utils import ts_indices, fake_data, fake_daily_data
2
+
3
+ __all__ = ['ts_indices', 'fake_data', 'fake_daily_data']
@@ -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.")