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.
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/MarketEngine.py +293 -132
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/TradeEngine.py +19 -4
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/__init__.py +2 -2
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/_StrategyEngine.py +11 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/__init__.py +1 -1
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PKG-INFO +1 -6
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/PKG-INFO +1 -6
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/AlgoEngine.py +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Engine/EventEngine.py +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/BackTest.py +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/AlgoEngine/Strategies/__init__.py +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/LICENSE +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/requires.txt +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/README.md +0 -0
- {PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/setup.cfg +0 -0
- {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
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import pickle
|
|
7
7
|
import uuid
|
|
8
8
|
from collections import defaultdict
|
|
9
|
-
from
|
|
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
|
|
36
|
-
self.name = name
|
|
37
|
-
self.monitor_id = uuid.uuid4().hex if monitor_id is None else monitor_id
|
|
38
|
-
self.
|
|
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
|
|
49
|
+
def to_json(self, fmt='str') -> dict | str:
|
|
50
|
+
...
|
|
46
51
|
|
|
47
|
-
@
|
|
52
|
+
@classmethod
|
|
48
53
|
@abc.abstractmethod
|
|
49
|
-
def
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
182
|
-
bar_data
|
|
183
|
-
bar_data
|
|
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
|
|
186
|
-
bar_data
|
|
187
|
-
bar_data
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
225
|
-
bar_data
|
|
226
|
-
bar_data
|
|
227
|
-
bar_data
|
|
228
|
-
bar_data
|
|
229
|
-
bar_data
|
|
230
|
-
bar_data
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
635
|
+
return self.monitor[monitor_id]
|
|
490
636
|
|
|
491
637
|
def add_monitor(self, monitor: MarketDataMonitor):
|
|
492
|
-
self.
|
|
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
|
-
|
|
643
|
+
pass
|
|
497
644
|
elif monitor_name is not None:
|
|
498
|
-
for _ in list(self.
|
|
645
|
+
for _ in list(self.monitor.values()):
|
|
499
646
|
if _.name == monitor_name:
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
613
|
-
self.
|
|
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
|
|
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
|
|
944
|
+
raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
|
|
788
945
|
|
|
789
|
-
if isinstance(dtypes,
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyAlgoEngine
|
|
3
|
-
Version: 0.3.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{PyAlgoEngine-0.3.10 → PyAlgoEngine-0.3.12.post3}/PyAlgoEngine.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|