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,53 @@
1
+ from types import SimpleNamespace
2
+
3
+ import event_engine
4
+ from event_engine import Topic, PatternTopic, EventEngine
5
+
6
+ from . import LOGGER
7
+
8
+ __all__ = ['EVENT_ENGINE', 'TOPIC']
9
+
10
+ event_engine.set_logger(LOGGER.getChild('EventEngine'))
11
+
12
+
13
+ class TopicSet(object):
14
+ on_order = Topic('on_order')
15
+ on_report = Topic('on_report')
16
+ eod = Topic('eod')
17
+ eod_done = Topic('eod_done')
18
+ bod = Topic('bod')
19
+ bod_done = Topic('bod_done')
20
+
21
+ launch_order = PatternTopic('launch_order.{ticker}')
22
+ cancel_order = PatternTopic('cancel_order.{ticker}')
23
+ realtime = PatternTopic('realtime.{ticker}.{dtype}')
24
+
25
+ @classmethod
26
+ def push(cls, market_data):
27
+ return cls.realtime(ticker=market_data.ticker, dtype=market_data.__class__.__name__)
28
+
29
+ @classmethod
30
+ def parse(cls, topic: Topic) -> SimpleNamespace:
31
+ try:
32
+ _ = topic.value.split('.')
33
+
34
+ action = _.pop(0)
35
+ if action in ['open', 'close']:
36
+ dtype = None
37
+ else:
38
+ dtype = _.pop(-1)
39
+ ticker = '.'.join(_)
40
+
41
+ p = SimpleNamespace(
42
+ action=action,
43
+ dtype=dtype,
44
+ ticker=ticker
45
+ )
46
+ return p
47
+ except Exception as _:
48
+ raise ValueError(f'Invalid topic {topic}')
49
+
50
+
51
+ EVENT_ENGINE = EventEngine()
52
+ TOPIC = TopicSet
53
+ # EVENT_ENGINE.start()
@@ -0,0 +1,370 @@
1
+ import abc
2
+ import datetime
3
+ import pickle
4
+ import uuid
5
+ from collections import defaultdict
6
+ from multiprocessing import shared_memory
7
+ from typing import Self
8
+
9
+ from . import LOGGER
10
+ from ..base import TickData, TradeData, OrderBook, MarketData, TransactionSide
11
+ from ..profile import PROFILE, Profile
12
+
13
+ LOGGER = LOGGER.getChild('MarketEngine')
14
+
15
+ __all__ = ['MDS', 'MarketDataService', 'MarketDataMonitor', 'MonitorManager', 'Singleton']
16
+
17
+
18
+ class Singleton(type):
19
+ _instances = {}
20
+
21
+ def __call__(cls, *args, **kwargs):
22
+ if cls not in cls._instances:
23
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
24
+ return cls._instances[cls]
25
+
26
+
27
+ class MarketDataMonitor(object, metaclass=abc.ABCMeta):
28
+ """
29
+ this is a template for market data monitor
30
+
31
+ A data monitor is a module that process market data and generate custom index
32
+
33
+ When MDS receive an update of market data, the __call__ function of this monitor is triggered.
34
+
35
+ Note: all the market_data, of all subscribed ticker will be fed into monitor. It should be assumed that a storage for multiple ticker is required.
36
+ To access the monitor, use `monitor = MDS[monitor_id]`
37
+ To access the index generated by the monitor, use `monitor.value`
38
+ To indicate that the monitor is ready to use set `monitor.is_ready = True`
39
+
40
+ The implemented monitor should be initialized and use `MDS.add_monitor(monitor)` to attach onto the engine
41
+ """
42
+
43
+ def __init__(self, name: str, monitor_id: str = None):
44
+ self.name: str = name
45
+ self.monitor_id: str = uuid.uuid4().hex if monitor_id is None else monitor_id
46
+ self.enabled: bool = True
47
+
48
+ @abc.abstractmethod
49
+ def __call__(self, market_data: MarketData, **kwargs):
50
+ ...
51
+
52
+ def __reduce__(self):
53
+ return self.__class__.from_json, (self.to_json(),)
54
+
55
+ @abc.abstractmethod
56
+ def to_json(self, fmt='str') -> dict | str:
57
+ ...
58
+
59
+ @classmethod
60
+ @abc.abstractmethod
61
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
62
+ ...
63
+
64
+ def to_shm(self, name: str = None) -> str:
65
+ """
66
+ Put the data of the monitor into python shared memory.
67
+ This function is designed to facilitate multiprocessing.
68
+ Some monitor is not advised to be handled concurrently,
69
+ In which case, raise a NotImplementedError.
70
+
71
+ The function is expected to put all data into a sharable list,
72
+ and return the name of the list, which can be set by the given name.
73
+ Default name = self.monitor_id
74
+
75
+ Note that this method HAVE NO LOCK, use with caution.
76
+ """
77
+ if name is None:
78
+ name = f'{self.monitor_id}.json'
79
+
80
+ data = pickle.dumps(self.to_json(fmt='dict'))
81
+ size = len(data)
82
+
83
+ try:
84
+ shm = shared_memory.SharedMemory(name=name)
85
+
86
+ if shm.size != size:
87
+ shm.close()
88
+ shm.unlink()
89
+ shm = shared_memory.SharedMemory(create=True, size=size, name=name)
90
+ except FileNotFoundError as _:
91
+ shm = shared_memory.SharedMemory(create=True, size=size, name=name)
92
+
93
+ shm.buf[:size] = data
94
+ shm.close()
95
+ return name
96
+
97
+ @classmethod
98
+ def from_shm(cls, monitor_id: str):
99
+ """
100
+ retrieve the data and update the monitor from shared memory.
101
+ This function is designed to facilitate multiprocessing.
102
+ """
103
+ return
104
+
105
+ @abc.abstractmethod
106
+ def clear(self) -> None:
107
+ ...
108
+
109
+ @property
110
+ @abc.abstractmethod
111
+ def value(self) -> dict[str, float] | float:
112
+ ...
113
+
114
+ @property
115
+ def is_ready(self) -> bool:
116
+ return True
117
+
118
+
119
+ class MonitorManager(object, metaclass=Singleton):
120
+ """
121
+ manage market data monitor
122
+
123
+ state codes for the manager
124
+ 0: idle
125
+ 1: working
126
+ -1: terminating
127
+ """
128
+
129
+ def __init__(self):
130
+ self.monitor: dict[str, MarketDataMonitor] = {}
131
+
132
+ def __call__(self, market_data: MarketData):
133
+ for monitor_id in self.monitor:
134
+ self._work(monitor_id=monitor_id, market_data=market_data)
135
+
136
+ def add_monitor(self, monitor: MarketDataMonitor):
137
+ self.monitor[monitor.monitor_id] = monitor
138
+
139
+ def pop_monitor(self, monitor_id: str) -> MarketDataMonitor:
140
+ return self.monitor.pop(monitor_id)
141
+
142
+ def _work(self, monitor_id: str, market_data: MarketData):
143
+ monitor = self.monitor.get(monitor_id)
144
+ if monitor is not None and monitor.enabled:
145
+ monitor.__call__(market_data)
146
+
147
+ def start(self):
148
+ pass
149
+
150
+ def stop(self):
151
+ pass
152
+
153
+ def clear(self):
154
+ self.monitor.clear()
155
+
156
+ @property
157
+ def values(self) -> dict[str, float]:
158
+ values = {}
159
+
160
+ for monitor in self.monitor.values():
161
+ values.update(monitor.value)
162
+
163
+ return values
164
+
165
+
166
+ class MarketDataService(object, metaclass=Singleton):
167
+ def __init__(self, profile: Profile = None, **kwargs):
168
+ self.profile = PROFILE if profile is None else profile
169
+ self.cache_history = kwargs.pop('cache_history', False)
170
+
171
+ self._market_price = {}
172
+ self._market_history = defaultdict(dict)
173
+ self._market_time: datetime.datetime | None = None
174
+ self._timestamp: float | None = None
175
+
176
+ self._order_book: dict[str, OrderBook] = {}
177
+ self._tick_data: dict[str, TickData] = {}
178
+ self._trade_data: dict[str, TradeData] = {}
179
+ self._monitor: dict[str, MarketDataMonitor] = {}
180
+ self._monitor_manager = MonitorManager()
181
+
182
+ def __call__(self, **kwargs):
183
+ if 'market_data' in kwargs:
184
+ self.on_market_data(market_data=kwargs['market_data'])
185
+
186
+ def __getitem__(self, monitor_id: str) -> MarketDataMonitor:
187
+ return self.monitor[monitor_id]
188
+
189
+ def add_monitor(self, monitor: MarketDataMonitor):
190
+ self.monitor[monitor.monitor_id] = monitor
191
+ self.monitor_manager.add_monitor(monitor)
192
+
193
+ def pop_monitor(self, monitor: MarketDataMonitor = None, monitor_id: str = None, monitor_name: str = None):
194
+ if monitor_id is not None:
195
+ pass
196
+ elif monitor_name is not None:
197
+ for _ in list(self.monitor.values()):
198
+ if _.name == monitor_name:
199
+ monitor_id = _.monitor_id
200
+ if monitor is None:
201
+ LOGGER.error(f'monitor_name {monitor_name} not registered.')
202
+ elif monitor is not None:
203
+ monitor_id = monitor.monitor_id
204
+ else:
205
+ LOGGER.error('must assign a monitor, or monitor_id, or monitor_name to pop.')
206
+ return None
207
+
208
+ self.monitor.pop(monitor_id)
209
+ self.monitor_manager.pop_monitor(monitor_id)
210
+
211
+ def _on_trade_data(self, trade_data: TradeData):
212
+ ticker = trade_data.ticker
213
+
214
+ if ticker not in self._trade_data:
215
+ LOGGER.info(f'MDS confirmed {ticker} TradeData subscribed!')
216
+
217
+ self._trade_data[ticker] = trade_data
218
+
219
+ def _on_tick_data(self, tick_data: TickData):
220
+ ticker = tick_data.ticker
221
+
222
+ if ticker not in self._tick_data:
223
+ LOGGER.info(f'MDS confirmed {ticker} TickData subscribed!')
224
+
225
+ self._tick_data[ticker] = tick_data
226
+ # self._order_book[ticker] = tick_data.order_book
227
+
228
+ def _on_order_book(self, order_book):
229
+ ticker = order_book.ticker
230
+
231
+ if ticker not in self._order_book:
232
+ LOGGER.info(f'MDS confirmed {ticker} OrderBook subscribed!')
233
+
234
+ self._order_book[ticker] = order_book
235
+
236
+ def on_market_data(self, market_data: MarketData):
237
+ ticker = market_data.ticker
238
+ market_time = market_data.market_time
239
+ timestamp = market_data.timestamp
240
+ market_price = market_data.market_price
241
+
242
+ self._market_price[ticker] = market_price
243
+ self._market_time = market_time
244
+ self._timestamp = timestamp
245
+
246
+ if self.cache_history:
247
+ self._market_history[ticker][market_time] = market_price
248
+
249
+ if isinstance(market_data, TradeData):
250
+ self._on_trade_data(trade_data=market_data)
251
+ elif isinstance(market_data, TickData):
252
+ self._on_tick_data(tick_data=market_data)
253
+ elif isinstance(market_data, OrderBook):
254
+ self._on_order_book(order_book=market_data)
255
+
256
+ self.monitor_manager.__call__(market_data=market_data)
257
+
258
+ def get_order_book(self, ticker: str) -> OrderBook | None:
259
+ return self._order_book.get(ticker, None)
260
+
261
+ def get_queued_volume(self, ticker: str, side: TransactionSide | str | int, prior: float, posterior: float = None) -> float:
262
+ """
263
+ get queued volume prior / posterior to given price, NOT COUNTING GIVEN PRICE!
264
+ :param ticker: the given ticker
265
+ :param side: the given trade side
266
+ :param prior: the given price
267
+ :param posterior: optional the given posterior price
268
+ :return: the summed queued volume, in float.
269
+ """
270
+ order_book = self.get_order_book(ticker=ticker)
271
+
272
+ if order_book is None:
273
+ queued_volume = float('nan')
274
+ else:
275
+ trade_side = TransactionSide(side)
276
+
277
+ if trade_side.sign > 0:
278
+ book = order_book.bid
279
+ elif trade_side < 0:
280
+ book = order_book.ask
281
+ else:
282
+ raise ValueError(f'Invalid side {side}')
283
+
284
+ queued_volume = book.loc_volume(p0=prior, p1=posterior)
285
+ return queued_volume
286
+
287
+ def trade_time_between(self, start_time: datetime.datetime | float, end_time: datetime.datetime | float, **kwargs) -> datetime.timedelta:
288
+ return self.profile.trade_time_between(start_time=start_time, end_time=end_time, **kwargs)
289
+
290
+ def is_market_session(self, market_time: datetime.datetime | float | int) -> bool:
291
+ return self.profile.is_market_session(timestamp=market_time)
292
+
293
+ def clear(self):
294
+ # self._market_price.clear()
295
+ # self._market_time = None
296
+ # self._timestamp = None
297
+
298
+ self._market_history.clear()
299
+ self._order_book.clear()
300
+ self.monitor.clear()
301
+ self.monitor_manager.clear()
302
+
303
+ @property
304
+ def market_price(self) -> dict[str, float]:
305
+ result = self._market_price
306
+ return result
307
+
308
+ @property
309
+ def market_history(self) -> dict[str, dict[datetime.datetime, float]]:
310
+ result = self._market_history
311
+ return result
312
+
313
+ @property
314
+ def market_time(self) -> datetime.datetime | None:
315
+ if self._market_time is None:
316
+ if self._timestamp is None:
317
+ return None
318
+ else:
319
+ return datetime.datetime.fromtimestamp(self._timestamp, tz=self.profile.time_zone)
320
+ else:
321
+ return self._market_time
322
+
323
+ @property
324
+ def market_date(self) -> datetime.date | None:
325
+ if self.market_time is None:
326
+ return None
327
+
328
+ return self._market_time.date()
329
+
330
+ @property
331
+ def timestamp(self) -> float | None:
332
+ if self._timestamp is None:
333
+ if self._market_time is None:
334
+ return None
335
+ else:
336
+ return self._market_time.timestamp()
337
+ else:
338
+ return self._timestamp
339
+
340
+ @property
341
+ def session_start(self) -> datetime.time | None:
342
+ return self.profile.session_start
343
+
344
+ @property
345
+ def session_end(self) -> datetime.time | None:
346
+ return self.profile.session_end
347
+
348
+ @property
349
+ def session_break(self) -> tuple[datetime.time, datetime.time] | None:
350
+ return self.profile.session_break
351
+
352
+ @property
353
+ def monitor(self) -> dict[str, MarketDataMonitor]:
354
+ return self._monitor
355
+
356
+ @property
357
+ def monitor_manager(self) -> MonitorManager:
358
+ return self._monitor_manager
359
+
360
+ @monitor_manager.setter
361
+ def monitor_manager(self, manager: MonitorManager):
362
+ self._monitor_manager.clear()
363
+
364
+ self._monitor_manager = manager
365
+
366
+ for monitor in self.monitor.values():
367
+ self._monitor_manager.add_monitor(monitor=monitor)
368
+
369
+
370
+ MDS = MarketDataService()