PyAlgoEngine 0.3.10__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.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/MarketEngine.py +293 -132
  2. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/TradeEngine.py +19 -4
  3. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/__init__.py +2 -2
  4. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/_StrategyEngine.py +11 -0
  5. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/__init__.py +1 -1
  6. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PKG-INFO +1 -6
  7. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/PKG-INFO +1 -6
  8. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/AlgoEngine.py +0 -0
  9. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/EventEngine.py +0 -0
  10. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/BackTest.py +0 -0
  11. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/__init__.py +0 -0
  12. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/LICENSE +0 -0
  13. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
  14. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  15. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/requires.txt +0 -0
  16. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  17. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/README.md +0 -0
  18. {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/setup.cfg +0 -0
  19. {PyAlgoEngine-0.3.10 → 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
+ ...
46
51
 
47
- @property
52
+ @classmethod
48
53
  @abc.abstractmethod
49
- def value(self) -> dict[str, float] | float: ...
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
97
+
98
+ @abc.abstractmethod
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]:
@@ -299,7 +448,7 @@ class CN_Profile(Profile):
299
448
  def trade_calendar(self, start_date: datetime.date, end_date: datetime.date, market='XSHG', tz='UTC') -> list[datetime.date]:
300
449
  import pandas as pd
301
450
 
302
- if market in self.trade_calendar:
451
+ if market in self._trade_calendar:
303
452
  trade_calendar = self._trade_calendar[market]
304
453
  else:
305
454
  import exchange_calendars
@@ -317,15 +466,13 @@ class CN_Profile(Profile):
317
466
 
318
467
  @functools.lru_cache
319
468
  def is_trade_day(self, market_date: datetime.date, market='XSHG', tz='UTC') -> bool:
320
- import pandas as pd
321
-
322
- if market in self.trade_calendar:
469
+ if market in self._trade_calendar:
323
470
  trade_calendar = self._trade_calendar[market]
324
471
  else:
325
472
  import exchange_calendars
326
473
  trade_calendar = self._trade_calendar[market] = exchange_calendars.get_calendar(market)
327
474
 
328
- return trade_calendar.is_session(pd.Timestamp(market_date, tz=tz))
475
+ return trade_calendar.is_session(market_date)
329
476
 
330
477
  def trade_days_between(self, start_date: datetime.date, end_date: datetime.date = datetime.date.today(), **kwargs) -> int:
331
478
  """
@@ -471,8 +618,7 @@ class MarketDataService(object):
471
618
  self._tick_data: dict[str, TickData] = {}
472
619
  self._trade_data: dict[str, TradeData] = {}
473
620
  self._monitor: dict[str, MarketDataMonitor] = {}
474
-
475
- self.lock = threading.Lock()
621
+ self._monitor_manager = MonitorManager()
476
622
 
477
623
  if self.synthetic_orderbook:
478
624
  # init synthetic orderbook monitor
@@ -486,24 +632,30 @@ class MarketDataService(object):
486
632
  self.on_market_data(market_data=kwargs['market_data'])
487
633
 
488
634
  def __getitem__(self, monitor_id: str) -> MarketDataMonitor:
489
- return self._monitor[monitor_id]
635
+ return self.monitor[monitor_id]
490
636
 
491
637
  def add_monitor(self, monitor: MarketDataMonitor):
492
- self._monitor[monitor.monitor_id] = monitor
638
+ self.monitor[monitor.monitor_id] = monitor
639
+ self.monitor_manager.add_monitor(monitor)
493
640
 
494
641
  def pop_monitor(self, monitor: MarketDataMonitor = None, monitor_id: str = None, monitor_name: str = None):
495
642
  if monitor_id is not None:
496
- return self._monitor.pop(monitor_id)
643
+ pass
497
644
  elif monitor_name is not None:
498
- for _ in list(self._monitor.values()):
645
+ for _ in list(self.monitor.values()):
499
646
  if _.name == monitor_name:
500
- 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.')
501
650
  elif monitor is not None:
502
- return self._monitor.pop(monitor.monitor_id)
651
+ monitor_id = monitor.monitor_id
503
652
  else:
504
653
  LOGGER.error('must assign a monitor, or monitor_id, or monitor_name to pop.')
505
654
  return None
506
655
 
656
+ self.monitor.pop(monitor_id)
657
+ self.monitor_manager.pop_monitor(monitor_id)
658
+
507
659
  def init_cn_override(self):
508
660
  self.profile = CN_Profile()
509
661
 
@@ -522,7 +674,7 @@ class MarketDataService(object):
522
674
  LOGGER.info(f'MDS confirmed {ticker} TickData subscribed!')
523
675
 
524
676
  self._tick_data[ticker] = tick_data
525
- self._order_book[ticker] = tick_data.order_book
677
+ # self._order_book[ticker] = tick_data.order_book
526
678
 
527
679
  def _on_order_book(self, order_book):
528
680
  ticker = order_book.ticker
@@ -533,7 +685,6 @@ class MarketDataService(object):
533
685
  self._order_book[ticker] = order_book
534
686
 
535
687
  def on_market_data(self, market_data: MarketData):
536
- self.lock.acquire()
537
688
  ticker = market_data.ticker
538
689
  market_time = market_data.market_time
539
690
  timestamp = market_data.timestamp
@@ -553,18 +704,7 @@ class MarketDataService(object):
553
704
  elif isinstance(market_data, OrderBook):
554
705
  self._on_order_book(order_book=market_data)
555
706
 
556
- for monitor_id in self._monitor:
557
- monitor = self._monitor.get(monitor_id)
558
-
559
- if monitor is None:
560
- continue
561
-
562
- if not monitor.enabled:
563
- continue
564
-
565
- monitor.__call__(market_data)
566
-
567
- self.lock.release()
707
+ self.monitor_manager.__call__(market_data=market_data)
568
708
 
569
709
  def get_order_book(self, ticker: str) -> OrderBook | None:
570
710
  return self._order_book.get(ticker, None)
@@ -592,7 +732,7 @@ class MarketDataService(object):
592
732
  else:
593
733
  raise ValueError(f'Invalid side {side}')
594
734
 
595
- queued_volume = book.loc(prior=prior, posterior=posterior)
735
+ queued_volume = book.loc_volume(p0=prior, p1=posterior)
596
736
  return queued_volume
597
737
 
598
738
  def trade_time_between(self, start_time: datetime.datetime | float, end_time: datetime.datetime | float, **kwargs) -> datetime.timedelta:
@@ -602,28 +742,23 @@ class MarketDataService(object):
602
742
  return self.profile.in_trade_session(market_time=market_time)
603
743
 
604
744
  def clear(self):
605
- self.lock.acquire()
606
745
  # self._market_price.clear()
607
746
  # self._market_time = None
608
747
  # self._timestamp = None
609
748
 
610
749
  self._market_history.clear()
611
750
  self._order_book.clear()
612
- self._monitor.clear()
613
- self.lock.release()
751
+ self.monitor.clear()
752
+ self.monitor_manager.clear()
614
753
 
615
754
  @property
616
755
  def market_price(self) -> dict[str, float]:
617
- self.lock.acquire()
618
756
  result = self._market_price
619
- self.lock.release()
620
757
  return result
621
758
 
622
759
  @property
623
760
  def market_history(self) -> dict[str, dict[datetime.datetime, float]]:
624
- self.lock.acquire()
625
761
  result = self._market_history
626
- self.lock.release()
627
762
  return result
628
763
 
629
764
  @property
@@ -665,6 +800,23 @@ class MarketDataService(object):
665
800
  def session_break(self) -> tuple[datetime.time, datetime.time] | None:
666
801
  return self.profile.session_break
667
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
+
668
820
 
669
821
  class Replay(object, metaclass=abc.ABCMeta):
670
822
  @abc.abstractmethod
@@ -781,15 +933,22 @@ class ProgressiveReplay(Replay):
781
933
  tickers: list[str] = kwargs.pop('ticker', kwargs.pop('tickers', []))
782
934
  dtypes: list[str | type] = kwargs.pop('dtype', kwargs.pop('dtypes', [TradeData, OrderBook, TickData]))
783
935
 
784
- 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):
785
942
  tickers = list(tickers)
786
943
  else:
787
- tickers = [tickers]
944
+ raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
788
945
 
789
- if isinstance(dtypes, Iterable):
946
+ if isinstance(dtypes, str) or inspect.isclass(dtypes):
947
+ dtypes = [dtypes]
948
+ elif isinstance(dtypes, Iterable):
790
949
  dtypes = list(dtypes)
791
950
  else:
792
- dtypes = [dtypes]
951
+ raise TypeError(f'Invalid dtype {dtypes}, expect str or list[str]')
793
952
 
794
953
  for ticker in tickers:
795
954
  for dtype in dtypes:
@@ -797,7 +956,7 @@ class ProgressiveReplay(Replay):
797
956
 
798
957
  subscription = kwargs.pop('subscription', kwargs.pop('subscribe', []))
799
958
 
800
- if not isinstance(subscription, list):
959
+ if isinstance(subscription, dict):
801
960
  subscription = [subscription]
802
961
 
803
962
  for sub in subscription:
@@ -808,8 +967,10 @@ class ProgressiveReplay(Replay):
808
967
  def add_subscription(self, ticker: str, dtype: type | str):
809
968
  if isinstance(dtype, str):
810
969
  pass
811
- else:
970
+ elif inspect.isclass(dtype):
812
971
  dtype = dtype.__name__
972
+ else:
973
+ raise ValueError(f'Invalid dtype {dtype}, expect str or class.')
813
974
 
814
975
  topic = f'{ticker}.{dtype}'
815
976
  self.replay_subscription[topic] = (ticker, dtype)
@@ -841,9 +1002,6 @@ class ProgressiveReplay(Replay):
841
1002
  self.progress.reset()
842
1003
 
843
1004
  def next_trade_day(self):
844
- self.replay_task.clear()
845
- self.task_progress = 0
846
-
847
1005
  if self.date_progress < len(self.replay_calendar):
848
1006
  market_date = self.replay_calendar[self.date_progress]
849
1007
  self.progress.prompt = f'Replay {market_date:%Y-%m-%d} ({self.date_progress + 1} / {len(self.replay_calendar)}):'
@@ -871,6 +1029,9 @@ class ProgressiveReplay(Replay):
871
1029
  if self.eod is not None and self.date_progress:
872
1030
  self.eod(market_date=self.replay_calendar[self.date_progress - 1], replay=self)
873
1031
 
1032
+ self.replay_task.clear()
1033
+ self.task_progress = 0
1034
+
874
1035
  if self.bod is not None and self.date_progress < len(self.replay_calendar):
875
1036
  self.bod(market_date=self.replay_calendar[self.date_progress], replay=self)
876
1037
 
@@ -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
@@ -2185,7 +2185,22 @@ class SimMatch(object):
2185
2185
  # raise ValueError(f'Invalid working order state {order}')
2186
2186
 
2187
2187
  def _check_tick_data(self, market_data: TickData):
2188
- return self._check_order_book(market_data=market_data.order_book)
2188
+ for order_id in list(self.working):
2189
+ order = self.working.get(order_id)
2190
+
2191
+ if order is None:
2192
+ pass
2193
+ elif order.order_state in [OrderState.Placed, OrderState.PartFilled]:
2194
+ if order.limit_price is None:
2195
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
2196
+ elif order.side.sign > 0 and market_data.market_price <= order.limit_price:
2197
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
2198
+ elif order.side.sign < 0 and market_data.market_price >= order.limit_price:
2199
+ self._match(order=order, match_volume=order.working_volume, match_price=market_data.market_price)
2200
+ else:
2201
+ continue
2202
+ else:
2203
+ continue
2189
2204
 
2190
2205
  def _match(self, order: TradeInstruction, match_volume: float = None, match_price: float = None):
2191
2206
  if match_volume is None:
@@ -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']
@@ -189,6 +189,17 @@ class StrategyEngine(StrategyEngineTemplate):
189
189
  event_engine.register_handler(topic=topic_set.on_order, handler=self.on_order)
190
190
  event_engine.register_handler(topic=topic_set.on_report, handler=self.on_report)
191
191
 
192
+ def unregister(self, event_engine=None, topic_set=None):
193
+ if event_engine is None:
194
+ event_engine = self.event_engine
195
+
196
+ if topic_set is None:
197
+ topic_set = self.topic_set
198
+
199
+ event_engine.unregister_handler(topic=topic_set.realtime, handler=self.__call__)
200
+ event_engine.unregister_handler(topic=topic_set.on_order, handler=self.on_order)
201
+ event_engine.unregister_handler(topic=topic_set.on_report, handler=self.on_report)
202
+
192
203
  def cancel(self, ticker: str, side: TransactionSide = None, algo_id: str = None, order_id: str = None, **kwargs):
193
204
  position_tracker = self.position_tracker
194
205
 
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.10"
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.10
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
@@ -12,11 +12,6 @@ Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.8
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: numpy
16
- Requires-Dist: pandas
17
- Requires-Dist: exchange_calendars
18
- Requires-Dist: PyQuantKit
19
- Requires-Dist: PyEventEngine
20
15
 
21
16
  # PyAlgoEngine
22
17
  python algo trading engine
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.3.10
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
@@ -12,11 +12,6 @@ Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.8
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: numpy
16
- Requires-Dist: pandas
17
- Requires-Dist: exchange_calendars
18
- Requires-Dist: PyQuantKit
19
- Requires-Dist: PyEventEngine
20
15
 
21
16
  # PyAlgoEngine
22
17
  python algo trading engine