PyAlgoEngine 0.3.11.post1__tar.gz → 0.3.12.post3__tar.gz

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 (19) hide show
  1. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/MarketEngine.py +287 -124
  2. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/TradeEngine.py +3 -3
  3. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/__init__.py +2 -2
  4. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/__init__.py +1 -1
  5. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PKG-INFO +1 -1
  6. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  7. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/AlgoEngine.py +0 -0
  8. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/EventEngine.py +0 -0
  9. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/BackTest.py +0 -0
  10. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/_StrategyEngine.py +0 -0
  11. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/__init__.py +0 -0
  12. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/LICENSE +0 -0
  13. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
  14. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  15. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/requires.txt +0 -0
  16. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  17. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/README.md +0 -0
  18. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/setup.cfg +0 -0
  19. {PyAlgoEngine-0.3.11.post1 → PyAlgoEngine-0.3.12.post3}/setup.py +0 -0
@@ -1,18 +1,19 @@
1
- from __future__ import annotations
2
-
3
1
  import abc
4
2
  import datetime
5
3
  import functools
6
- import threading
4
+ import inspect
5
+ import json
6
+ import pickle
7
7
  import uuid
8
8
  from collections import defaultdict
9
- from typing import Iterable
9
+ from multiprocessing import shared_memory
10
+ from typing import Iterable, Self
10
11
 
11
12
  from PyQuantKit import TickData, TradeData, OrderBook, MarketData, Progress, TransactionSide, BarData, TransactionData
12
13
 
13
14
  from . import LOGGER
14
15
 
15
- __all__ = ['MDS', 'MarketDataService', 'MarketDataMonitor', 'SyntheticOrderBookMonitor', 'MinuteBarMonitor', 'Profile', 'ProgressiveReplay', 'SimpleReplay', 'Replay']
16
+ __all__ = ['MDS', 'MarketDataService', 'MarketDataMonitor', 'MonitorManager', 'SyntheticOrderBookMonitor', 'MinuteBarMonitor', 'Profile', 'ProgressiveReplay', 'SimpleReplay', 'Replay']
16
17
  LOGGER = LOGGER.getChild('MarketEngine')
17
18
 
18
19
 
@@ -32,41 +33,139 @@ class MarketDataMonitor(object, metaclass=abc.ABCMeta):
32
33
  The implemented monitor should be initialized and use `MDS.add_monitor(monitor)` to attach onto the engine
33
34
  """
34
35
 
35
- def __init__(self, name: str, monitor_id: str = None, mds: MarketDataService = None):
36
- self.name = name
37
- self.monitor_id = uuid.uuid4().hex if monitor_id is None else monitor_id
38
- self.mds = MDS if mds is None else mds
39
- self.enabled = True
36
+ def __init__(self, name: str, monitor_id: str = None):
37
+ self.name: str = name
38
+ self.monitor_id: str = uuid.uuid4().hex if monitor_id is None else monitor_id
39
+ self.enabled: bool = True
40
40
 
41
41
  @abc.abstractmethod
42
- def __call__(self, market_data: MarketData, **kwargs): ...
42
+ def __call__(self, market_data: MarketData, **kwargs):
43
+ ...
44
+
45
+ def __reduce__(self):
46
+ return self.__class__.from_json, (self.to_json(),)
43
47
 
44
48
  @abc.abstractmethod
45
- def clear(self) -> None: ...
49
+ def to_json(self, fmt='str') -> dict | str:
50
+ ...
51
+
52
+ @classmethod
53
+ @abc.abstractmethod
54
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
55
+ ...
56
+
57
+ def to_shm(self, name: str = None) -> str:
58
+ """
59
+ Put the data of the monitor into python shared memory.
60
+ This function is designed to facilitate multiprocessing.
61
+ Some monitor is not advised to be handled concurrently,
62
+ In which case, raise a NotImplementedError.
63
+
64
+ The function is expected to put all data into a sharable list,
65
+ and return the name of the list, which can be set by the given name.
66
+ Default name = self.monitor_id
67
+
68
+ Note that this method HAVE NO LOCK, use with caution.
69
+ """
70
+ if name is None:
71
+ name = f'{self.monitor_id}.json'
72
+
73
+ data = pickle.dumps(self.to_json(fmt='dict'))
74
+ size = len(data)
75
+
76
+ try:
77
+ shm = shared_memory.SharedMemory(name=name)
78
+
79
+ if shm.size != size:
80
+ shm.close()
81
+ shm.unlink()
82
+ shm = shared_memory.SharedMemory(create=True, size=size, name=name)
83
+ except FileNotFoundError as _:
84
+ shm = shared_memory.SharedMemory(create=True, size=size, name=name)
85
+
86
+ shm.buf[:size] = data
87
+ shm.close()
88
+ return name
89
+
90
+ @classmethod
91
+ def from_shm(cls, monitor_id: str):
92
+ """
93
+ retrieve the data and update the monitor from shared memory.
94
+ This function is designed to facilitate multiprocessing.
95
+ """
96
+ return
46
97
 
47
- @property
48
98
  @abc.abstractmethod
49
- def value(self) -> dict[str, float] | float: ...
99
+ def clear(self) -> None:
100
+ ...
50
101
 
51
102
  @property
52
103
  @abc.abstractmethod
53
- def is_ready(self) -> bool: ...
104
+ def value(self) -> dict[str, float] | float:
105
+ ...
106
+
107
+ @property
108
+ def is_ready(self) -> bool:
109
+ return True
110
+
111
+
112
+ class MonitorManager(object):
113
+ """
114
+ manage market data monitor
115
+
116
+ state codes for the manager
117
+ 0: idle
118
+ 1: working
119
+ -1: terminating
120
+ """
121
+
122
+ def __init__(self):
123
+ self.monitor: dict[str, MarketDataMonitor] = {}
124
+
125
+ def __call__(self, market_data: MarketData):
126
+ for monitor_id in self.monitor:
127
+ self._work(monitor_id=monitor_id, market_data=market_data)
128
+
129
+ def add_monitor(self, monitor: MarketDataMonitor):
130
+ self.monitor[monitor.monitor_id] = monitor
131
+
132
+ def pop_monitor(self, monitor_id: str) -> MarketDataMonitor:
133
+ return self.monitor.pop(monitor_id)
134
+
135
+ def _work(self, monitor_id: str, market_data: MarketData):
136
+ monitor = self.monitor.get(monitor_id)
137
+ if monitor is not None and monitor.enabled:
138
+ monitor.__call__(market_data)
139
+
140
+ def start(self):
141
+ pass
142
+
143
+ def stop(self):
144
+ pass
145
+
146
+ def clear(self):
147
+ self.monitor.clear()
148
+
149
+ @property
150
+ def values(self) -> dict[str, float]:
151
+ values = {}
152
+
153
+ for monitor in self.monitor.values():
154
+ values.update(monitor.value)
155
+
156
+ return values
54
157
 
55
158
 
56
159
  class SyntheticOrderBookMonitor(MarketDataMonitor):
57
- def __init__(self, keep_order_log: bool = False, **kwargs):
58
- self.keep_order_log = keep_order_log
160
+
161
+ def __init__(self, **kwargs):
59
162
 
60
163
  super().__init__(
61
164
  name=kwargs.pop('name', 'Monitor.SyntheticOrderBook'),
62
- monitor_id=kwargs.pop('monitor_id', None),
63
- mds=kwargs.pop('mds', None),
165
+ monitor_id=kwargs.pop('monitor_id', None)
64
166
  )
65
167
 
66
- self._is_ready = True
67
- self._value = {}
68
- self.order_book = {}
69
- self.order_log = {}
168
+ self.order_book: dict[str, OrderBook] = {}
70
169
 
71
170
  def __call__(self, market_data: MarketData, **kwargs):
72
171
  if isinstance(market_data, TradeData):
@@ -77,55 +176,59 @@ class SyntheticOrderBookMonitor(MarketDataMonitor):
77
176
 
78
177
  if order_book := self.order_book.get(ticker):
79
178
  if order_book.market_time <= trade_data.market_time:
80
- side: TransactionSide = trade_data.side
179
+ side = trade_data.side
81
180
  price = trade_data.price
82
181
  book = order_book.ask if side.sign > 0 else order_book.bid
83
182
  listed_volume = book.at_price(price).volume if price in book else 0.
84
183
  traded_volume = trade_data.volume
85
- book.update_entry(price=price, volume=max(0, listed_volume - traded_volume))
86
-
87
- if self.keep_order_log:
88
- self._update_order_log(trade_data=trade_data)
184
+ book.update(price=price, volume=max(0, listed_volume - traded_volume))
89
185
 
90
186
  def on_transaction_data(self, transaction_data: TransactionData):
91
187
  pass
92
188
 
93
- def _update_order_log(self, trade_data: TradeData):
94
- side: TransactionSide = trade_data.side
95
- price = trade_data.price
96
- traded_volume = trade_data.volume
97
-
98
- for order_id in list(self.order_log):
99
- order_log = self.order_log.get(order_id)
100
-
101
- if order_log is None:
102
- continue
103
-
104
- if side.sign > 0:
105
- if order_log.side.sign < 0:
106
- if price == order_log.price:
107
- order_log.volume -= traded_volume
108
- elif price > order_log.price:
109
- order_log.volume = 0.
110
- else:
111
- if price <= order_log.price:
112
- order_log.volume = 0.
113
- elif side.sign < 0:
114
- if order_log.side.sign > 0:
115
- if price == order_log.price:
116
- order_log.volume -= traded_volume
117
- elif price < order_log.price:
118
- order_log.volume = 0.
119
- else:
120
- if price >= order_log.price:
121
- order_log.volume = 0.
122
-
123
- if order_log.volume <= 0:
124
- self.order_log.pop(order_id)
189
+ def to_json(self, fmt='str', **kwargs) -> str | dict:
190
+ data_dict = dict(
191
+ name=self.name,
192
+ monitor_id=self.monitor_id,
193
+ order_book={k: v.to_json(fmt='dict') for k, v in self.order_book.items()},
194
+ )
125
195
 
126
- @property
127
- def is_ready(self) -> bool:
128
- return self._is_ready
196
+ if fmt == 'dict':
197
+ return data_dict
198
+ elif fmt == 'str':
199
+ return json.dumps(data_dict, **kwargs)
200
+ else:
201
+ raise ValueError(f'Invalid format {fmt}, except "dict" or "str".')
202
+
203
+ @classmethod
204
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
205
+ if isinstance(json_message, dict):
206
+ json_dict = json_message
207
+ else:
208
+ json_dict = json.loads(json_message)
209
+
210
+ self = cls(
211
+ name=json_dict['name'],
212
+ monitor_id=json_dict['monitor_id'],
213
+ keep_order_log=json_dict['keep_order_log']
214
+ )
215
+
216
+ self.order_book = {k: MarketData.from_json(v) for k, v in json_dict['order_book'].items()}
217
+ return self
218
+
219
+ def from_shm(self, name: str = None) -> None:
220
+ if name is None:
221
+ name = f'{self.monitor_id}.json'
222
+
223
+ shm = shared_memory.SharedMemory(name=name)
224
+ json_dict = pickle.loads(bytes(shm.buf))
225
+
226
+ self.clear()
227
+
228
+ self.order_book.update({k: MarketData.from_json(v) for k, v in json_dict['order_book'].items()})
229
+
230
+ def clear(self) -> None:
231
+ self.order_book.clear()
129
232
 
130
233
  @property
131
234
  def value(self) -> dict[str, OrderBook]:
@@ -133,21 +236,18 @@ class SyntheticOrderBookMonitor(MarketDataMonitor):
133
236
 
134
237
 
135
238
  class MinuteBarMonitor(MarketDataMonitor):
239
+
136
240
  def __init__(self, interval: float = 60., **kwargs):
137
241
  self.interval = interval
138
242
 
139
243
  super().__init__(
140
244
  name=kwargs.pop('name', 'Monitor.MinuteBarMonitor'),
141
- monitor_id=kwargs.pop('monitor_id', None),
142
- mds=kwargs.pop('mds', None),
245
+ monitor_id=kwargs.pop('monitor_id', None)
143
246
  )
144
247
 
145
248
  self._minute_bar_data: dict[str, BarData] = {}
146
249
  self._last_bar_data: dict[str, BarData] = {}
147
250
 
148
- self._is_ready = True
149
- self._value = {}
150
-
151
251
  def __call__(self, market_data: MarketData, **kwargs):
152
252
  self._update_last_bar(market_data=market_data, interval=self.interval)
153
253
  # self._update_active_bar(market_data=market_data, interval=self.interval)
@@ -156,6 +256,7 @@ class MinuteBarMonitor(MarketDataMonitor):
156
256
  ticker = market_data.ticker
157
257
  market_price = market_data.market_price
158
258
  market_time = market_data.market_time
259
+ timestamp = market_data.timestamp
159
260
 
160
261
  if ticker not in self._minute_bar_data or market_time >= self._minute_bar_data[ticker].bar_end_time:
161
262
  # update bar_data
@@ -164,7 +265,8 @@ class MinuteBarMonitor(MarketDataMonitor):
164
265
 
165
266
  bar_data = self._minute_bar_data[ticker] = BarData(
166
267
  ticker=ticker,
167
- bar_start_time=datetime.datetime.fromtimestamp(self.mds.timestamp // interval * interval),
268
+ timestamp=int(timestamp // interval + 1) * interval,
269
+ start_timestamp=int(timestamp // interval) * interval,
168
270
  bar_span=datetime.timedelta(seconds=interval),
169
271
  high_price=market_price,
170
272
  low_price=market_price,
@@ -178,23 +280,25 @@ class MinuteBarMonitor(MarketDataMonitor):
178
280
  bar_data = self._minute_bar_data[ticker]
179
281
 
180
282
  if isinstance(market_data, TradeData):
181
- bar_data.volume += market_data.volume
182
- bar_data.notional += market_data.notional
183
- bar_data.trade_count += 1
283
+ bar_data['volume'] += market_data.volume
284
+ bar_data['notional'] += market_data.notional
285
+ bar_data['trade_count'] += 1
184
286
 
185
- bar_data.close_price = market_price
186
- bar_data.high_price = max(bar_data.high_price, market_price)
187
- bar_data.low_price = min(bar_data.low_price, market_price)
287
+ bar_data['close_price'] = market_price
288
+ bar_data['high_price'] = max(bar_data.high_price, market_price)
289
+ bar_data['low_price'] = min(bar_data.low_price, market_price)
188
290
 
189
291
  def _update_active_bar(self, market_data: MarketData, interval: float):
190
292
  ticker = market_data.ticker
191
293
  market_price = market_data.market_price
192
- market_time = MarketData.market_time
294
+ market_time = market_data.market_time
295
+ timestamp = market_data.timestamp
193
296
 
194
297
  if ticker not in self._minute_bar_data or market_time >= self._minute_bar_data[ticker].bar_end_time:
195
298
  bar_data = self._minute_bar_data[ticker] = BarData(
196
299
  ticker=ticker,
197
- bar_start_time=datetime.datetime.fromtimestamp(self.mds.timestamp - interval),
300
+ start_timestamp=timestamp - interval,
301
+ timestamp=timestamp,
198
302
  bar_span=datetime.timedelta(seconds=interval),
199
303
  high_price=market_price,
200
304
  low_price=market_price,
@@ -210,7 +314,7 @@ class MinuteBarMonitor(MarketDataMonitor):
210
314
  bar_data = self._minute_bar_data[ticker]
211
315
 
212
316
  history: list[TradeData] = getattr(bar_data, 'history')
213
- bar_data.bar_start_time = datetime.datetime.fromtimestamp(self.mds.timestamp - interval)
317
+ bar_data['start_timestamp'] = timestamp - interval
214
318
 
215
319
  if isinstance(market_data, TradeData):
216
320
  history.append(market_data)
@@ -221,17 +325,62 @@ class MinuteBarMonitor(MarketDataMonitor):
221
325
  else:
222
326
  history.pop(0)
223
327
 
224
- bar_data.volume = sum([_.volume for _ in history])
225
- bar_data.notional = sum([_.notional for _ in history])
226
- bar_data.trade_count = len([_.notional for _ in history])
227
- bar_data.close_price = market_price
228
- bar_data.open_price = history[0].market_price
229
- bar_data.high_price = max([_.market_price for _ in history])
230
- bar_data.low_price = min([_.market_price for _ in history])
328
+ bar_data['volume'] = sum([_.volume for _ in history])
329
+ bar_data['notional'] = sum([_.notional for _ in history])
330
+ bar_data['trade_count'] = len([_.notional for _ in history])
331
+ bar_data['close_price'] = market_price
332
+ bar_data['open_price'] = history[0].market_price
333
+ bar_data['high_price'] = max([_.market_price for _ in history])
334
+ bar_data['low_price'] = min([_.market_price for _ in history])
335
+
336
+ def to_json(self, fmt='str', **kwargs) -> str | dict:
337
+ data_dict = dict(
338
+ name=self.name,
339
+ monitor_id=self.monitor_id,
340
+ interval=self.interval,
341
+ minute_bar_data={k: v.to_json(fmt='dict') for k, v in self._minute_bar_data.items()},
342
+ last_bar_data={k: v.to_json(fmt='dict') for k, v in self._last_bar_data.items()},
343
+ )
231
344
 
232
- @property
233
- def is_ready(self) -> bool:
234
- return self._is_ready
345
+ if fmt == 'dict':
346
+ return data_dict
347
+ elif fmt == 'str':
348
+ return json.dumps(data_dict, **kwargs)
349
+ else:
350
+ raise ValueError(f'Invalid format {fmt}, except "dict" or "str".')
351
+
352
+ @classmethod
353
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
354
+ if isinstance(json_message, dict):
355
+ json_dict = json_message
356
+ else:
357
+ json_dict = json.loads(json_message)
358
+
359
+ self = cls(
360
+ name=json_dict['name'],
361
+ monitor_id=json_dict['monitor_id'],
362
+ interval=json_dict['interval'],
363
+ )
364
+
365
+ self._minute_bar_data = {k: MarketData.from_json(v) for k, v in json_dict['minute_bar_data'].items()}
366
+ self._last_bar_data = {k: MarketData.from_json(v) for k, v in json_dict['last_bar_data'].items()}
367
+ return self
368
+
369
+ def from_shm(self, name: str = None) -> None:
370
+ if name is None:
371
+ name = f'{self.monitor_id}.json'
372
+
373
+ shm = shared_memory.SharedMemory(name=name)
374
+ json_dict = pickle.loads(bytes(shm.buf))
375
+
376
+ self.clear()
377
+
378
+ self._minute_bar_data.update({k: MarketData.from_json(v) for k, v in json_dict['minute_bar_data'].items()})
379
+ self._last_bar_data.update({k: MarketData.from_json(v) for k, v in json_dict['last_bar_data'].items()})
380
+
381
+ def clear(self) -> None:
382
+ self._minute_bar_data.clear()
383
+ self._last_bar_data.clear()
235
384
 
236
385
  @property
237
386
  def value(self) -> dict[str, BarData]:
@@ -469,8 +618,7 @@ class MarketDataService(object):
469
618
  self._tick_data: dict[str, TickData] = {}
470
619
  self._trade_data: dict[str, TradeData] = {}
471
620
  self._monitor: dict[str, MarketDataMonitor] = {}
472
-
473
- self.lock = threading.Lock()
621
+ self._monitor_manager = MonitorManager()
474
622
 
475
623
  if self.synthetic_orderbook:
476
624
  # init synthetic orderbook monitor
@@ -484,24 +632,30 @@ class MarketDataService(object):
484
632
  self.on_market_data(market_data=kwargs['market_data'])
485
633
 
486
634
  def __getitem__(self, monitor_id: str) -> MarketDataMonitor:
487
- return self._monitor[monitor_id]
635
+ return self.monitor[monitor_id]
488
636
 
489
637
  def add_monitor(self, monitor: MarketDataMonitor):
490
- self._monitor[monitor.monitor_id] = monitor
638
+ self.monitor[monitor.monitor_id] = monitor
639
+ self.monitor_manager.add_monitor(monitor)
491
640
 
492
641
  def pop_monitor(self, monitor: MarketDataMonitor = None, monitor_id: str = None, monitor_name: str = None):
493
642
  if monitor_id is not None:
494
- return self._monitor.pop(monitor_id)
643
+ pass
495
644
  elif monitor_name is not None:
496
- for _ in list(self._monitor.values()):
645
+ for _ in list(self.monitor.values()):
497
646
  if _.name == monitor_name:
498
- return self._monitor.pop(_.monitor_id)
647
+ monitor_id = _.monitor_id
648
+ if monitor is None:
649
+ LOGGER.error(f'monitor_name {monitor_name} not registered.')
499
650
  elif monitor is not None:
500
- return self._monitor.pop(monitor.monitor_id)
651
+ monitor_id = monitor.monitor_id
501
652
  else:
502
653
  LOGGER.error('must assign a monitor, or monitor_id, or monitor_name to pop.')
503
654
  return None
504
655
 
656
+ self.monitor.pop(monitor_id)
657
+ self.monitor_manager.pop_monitor(monitor_id)
658
+
505
659
  def init_cn_override(self):
506
660
  self.profile = CN_Profile()
507
661
 
@@ -520,7 +674,7 @@ class MarketDataService(object):
520
674
  LOGGER.info(f'MDS confirmed {ticker} TickData subscribed!')
521
675
 
522
676
  self._tick_data[ticker] = tick_data
523
- self._order_book[ticker] = tick_data.order_book
677
+ # self._order_book[ticker] = tick_data.order_book
524
678
 
525
679
  def _on_order_book(self, order_book):
526
680
  ticker = order_book.ticker
@@ -531,7 +685,6 @@ class MarketDataService(object):
531
685
  self._order_book[ticker] = order_book
532
686
 
533
687
  def on_market_data(self, market_data: MarketData):
534
- self.lock.acquire()
535
688
  ticker = market_data.ticker
536
689
  market_time = market_data.market_time
537
690
  timestamp = market_data.timestamp
@@ -551,18 +704,7 @@ class MarketDataService(object):
551
704
  elif isinstance(market_data, OrderBook):
552
705
  self._on_order_book(order_book=market_data)
553
706
 
554
- for monitor_id in self._monitor:
555
- monitor = self._monitor.get(monitor_id)
556
-
557
- if monitor is None:
558
- continue
559
-
560
- if not monitor.enabled:
561
- continue
562
-
563
- monitor.__call__(market_data)
564
-
565
- self.lock.release()
707
+ self.monitor_manager.__call__(market_data=market_data)
566
708
 
567
709
  def get_order_book(self, ticker: str) -> OrderBook | None:
568
710
  return self._order_book.get(ticker, None)
@@ -590,7 +732,7 @@ class MarketDataService(object):
590
732
  else:
591
733
  raise ValueError(f'Invalid side {side}')
592
734
 
593
- queued_volume = book.loc(prior=prior, posterior=posterior)
735
+ queued_volume = book.loc_volume(p0=prior, p1=posterior)
594
736
  return queued_volume
595
737
 
596
738
  def trade_time_between(self, start_time: datetime.datetime | float, end_time: datetime.datetime | float, **kwargs) -> datetime.timedelta:
@@ -600,28 +742,23 @@ class MarketDataService(object):
600
742
  return self.profile.in_trade_session(market_time=market_time)
601
743
 
602
744
  def clear(self):
603
- self.lock.acquire()
604
745
  # self._market_price.clear()
605
746
  # self._market_time = None
606
747
  # self._timestamp = None
607
748
 
608
749
  self._market_history.clear()
609
750
  self._order_book.clear()
610
- self._monitor.clear()
611
- self.lock.release()
751
+ self.monitor.clear()
752
+ self.monitor_manager.clear()
612
753
 
613
754
  @property
614
755
  def market_price(self) -> dict[str, float]:
615
- self.lock.acquire()
616
756
  result = self._market_price
617
- self.lock.release()
618
757
  return result
619
758
 
620
759
  @property
621
760
  def market_history(self) -> dict[str, dict[datetime.datetime, float]]:
622
- self.lock.acquire()
623
761
  result = self._market_history
624
- self.lock.release()
625
762
  return result
626
763
 
627
764
  @property
@@ -663,6 +800,23 @@ class MarketDataService(object):
663
800
  def session_break(self) -> tuple[datetime.time, datetime.time] | None:
664
801
  return self.profile.session_break
665
802
 
803
+ @property
804
+ def monitor(self) -> dict[str, MarketDataMonitor]:
805
+ return self._monitor
806
+
807
+ @property
808
+ def monitor_manager(self) -> MonitorManager:
809
+ return self._monitor_manager
810
+
811
+ @monitor_manager.setter
812
+ def monitor_manager(self, manager: MonitorManager):
813
+ self._monitor_manager.clear()
814
+
815
+ self._monitor_manager = manager
816
+
817
+ for monitor in self.monitor.values():
818
+ self._monitor_manager.add_monitor(monitor=monitor)
819
+
666
820
 
667
821
  class Replay(object, metaclass=abc.ABCMeta):
668
822
  @abc.abstractmethod
@@ -779,15 +933,22 @@ class ProgressiveReplay(Replay):
779
933
  tickers: list[str] = kwargs.pop('ticker', kwargs.pop('tickers', []))
780
934
  dtypes: list[str | type] = kwargs.pop('dtype', kwargs.pop('dtypes', [TradeData, OrderBook, TickData]))
781
935
 
782
- if isinstance(tickers, Iterable):
936
+ if not all([arg_name in inspect.getfullargspec(loader).args for arg_name in ['market_date', 'ticker', 'dtype']]):
937
+ raise TypeError('loader function has 3 requires args, market_date, ticker and dtype.')
938
+
939
+ if isinstance(tickers, str):
940
+ tickers = [tickers]
941
+ elif isinstance(tickers, Iterable):
783
942
  tickers = list(tickers)
784
943
  else:
785
- tickers = [tickers]
944
+ raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
786
945
 
787
- if isinstance(dtypes, Iterable):
946
+ if isinstance(dtypes, str) or inspect.isclass(dtypes):
947
+ dtypes = [dtypes]
948
+ elif isinstance(dtypes, Iterable):
788
949
  dtypes = list(dtypes)
789
950
  else:
790
- dtypes = [dtypes]
951
+ raise TypeError(f'Invalid dtype {dtypes}, expect str or list[str]')
791
952
 
792
953
  for ticker in tickers:
793
954
  for dtype in dtypes:
@@ -795,7 +956,7 @@ class ProgressiveReplay(Replay):
795
956
 
796
957
  subscription = kwargs.pop('subscription', kwargs.pop('subscribe', []))
797
958
 
798
- if not isinstance(subscription, list):
959
+ if isinstance(subscription, dict):
799
960
  subscription = [subscription]
800
961
 
801
962
  for sub in subscription:
@@ -806,8 +967,10 @@ class ProgressiveReplay(Replay):
806
967
  def add_subscription(self, ticker: str, dtype: type | str):
807
968
  if isinstance(dtype, str):
808
969
  pass
809
- else:
970
+ elif inspect.isclass(dtype):
810
971
  dtype = dtype.__name__
972
+ else:
973
+ raise ValueError(f'Invalid dtype {dtype}, expect str or class.')
811
974
 
812
975
  topic = f'{ticker}.{dtype}'
813
976
  self.replay_subscription[topic] = (ticker, dtype)
@@ -2000,7 +2000,7 @@ class SimMatch(object):
2000
2000
  self.market_time = datetime.datetime.min
2001
2001
 
2002
2002
  def __call__(self, **kwargs):
2003
- order = kwargs.pop('order', None)
2003
+ order: TradeInstruction = kwargs.pop('order', None)
2004
2004
  market_data = kwargs.pop('market_data', None)
2005
2005
 
2006
2006
  if order is not None:
@@ -2084,7 +2084,7 @@ class SimMatch(object):
2084
2084
  if order.side.sign > 0:
2085
2085
  # match order based on worst offer
2086
2086
  if order.limit_price is None:
2087
- self._match(order=order, match_price=market_data.VWAP)
2087
+ self._match(order=order, match_price=market_data.vwap)
2088
2088
  elif market_data.high_price < order.limit_price:
2089
2089
  self._match(order=order, match_price=market_data.high_price)
2090
2090
  # match order based on limit price
@@ -2096,7 +2096,7 @@ class SimMatch(object):
2096
2096
  elif order.side.sign < 0:
2097
2097
  # match order based on worst offer
2098
2098
  if order.limit_price is None:
2099
- self._match(order=order, match_price=market_data.VWAP)
2099
+ self._match(order=order, match_price=market_data.vwap)
2100
2100
  elif market_data.low_price > order.limit_price:
2101
2101
  self._match(order=order, match_price=market_data.low_price)
2102
2102
  # match order based on limit price
@@ -92,10 +92,10 @@ _ = get_logger()
92
92
 
93
93
  from .EventEngine import EVENT_ENGINE, TOPIC
94
94
  from .AlgoEngine import AlgoTemplate, ALGO_ENGINE, ALGO_REGISTRY
95
- from .MarketEngine import MDS, MarketDataService, MarketDataMonitor, SyntheticOrderBookMonitor, MinuteBarMonitor, ProgressiveReplay, SimpleReplay, Replay
95
+ from .MarketEngine import MDS, MarketDataService, MarketDataMonitor, MonitorManager, SyntheticOrderBookMonitor, MinuteBarMonitor, ProgressiveReplay, SimpleReplay, Replay
96
96
  from .TradeEngine import DirectMarketAccess, Balance, PositionManagementService, Inventory, RiskProfile, SimMatch
97
97
 
98
98
  __all__ = ['set_logger', 'LOGGER', 'EVENT_ENGINE', 'TOPIC',
99
99
  'AlgoTemplate', 'ALGO_ENGINE', 'ALGO_REGISTRY',
100
- 'MDS', 'MarketDataService', 'MarketDataMonitor', 'SyntheticOrderBookMonitor', 'MinuteBarMonitor', 'ProgressiveReplay', 'SimpleReplay', 'Replay',
100
+ 'MDS', 'MarketDataService', 'MarketDataMonitor', 'MonitorManager', 'SyntheticOrderBookMonitor', 'MinuteBarMonitor', 'ProgressiveReplay', 'SimpleReplay', 'Replay',
101
101
  'DirectMarketAccess', 'Balance', 'PositionManagementService', 'Inventory', 'RiskProfile', 'SimMatch']
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.11.post1"
1
+ __version__ = "0.3.12.post3"
2
2
 
3
3
  import traceback
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.3.11.post1
3
+ Version: 0.3.12.post3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.3.11.post1
3
+ Version: 0.3.12.post3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han