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