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,15 @@
|
|
|
1
|
+
from ..engine import MDS
|
|
2
|
+
from ..engine.market_engine import MarketDataMonitor as Monitor
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def add_synthetic_orderbook():
|
|
6
|
+
# init synthetic orderbook monitor
|
|
7
|
+
monitor = SyntheticOrderBookMonitor(mds=MDS)
|
|
8
|
+
MDS.add_monitor(monitor=monitor)
|
|
9
|
+
# override current orderbook
|
|
10
|
+
MDS._order_book = monitor.order_book
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from .advanced_data_interface import *
|
|
14
|
+
|
|
15
|
+
__all__ = ['Monitor', 'SyntheticOrderBookMonitor', 'MinuteBarMonitor']
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import pickle
|
|
4
|
+
from multiprocessing import shared_memory
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
from . import Monitor
|
|
8
|
+
from ..base import TradeData, OrderBook, MarketData, BarData, TransactionData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyntheticOrderBookMonitor(Monitor):
|
|
12
|
+
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
|
|
15
|
+
super().__init__(
|
|
16
|
+
name=kwargs.pop('name', 'Monitor.SyntheticOrderBook'),
|
|
17
|
+
monitor_id=kwargs.pop('monitor_id', None)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.order_book: dict[str, OrderBook] = {}
|
|
21
|
+
|
|
22
|
+
def __call__(self, market_data: MarketData, **kwargs):
|
|
23
|
+
if isinstance(market_data, TradeData):
|
|
24
|
+
self.on_trade_data(trade_data=market_data)
|
|
25
|
+
|
|
26
|
+
def on_trade_data(self, trade_data: TradeData):
|
|
27
|
+
ticker = trade_data.ticker
|
|
28
|
+
|
|
29
|
+
if order_book := self.order_book.get(ticker):
|
|
30
|
+
if order_book.market_time <= trade_data.market_time:
|
|
31
|
+
side = trade_data.side
|
|
32
|
+
price = trade_data.price
|
|
33
|
+
book = order_book.ask if side.sign > 0 else order_book.bid
|
|
34
|
+
listed_volume = book.at_price(price).volume if price in book else 0.
|
|
35
|
+
traded_volume = trade_data.volume
|
|
36
|
+
book.update(price=price, volume=max(0, listed_volume - traded_volume))
|
|
37
|
+
|
|
38
|
+
def on_transaction_data(self, transaction_data: TransactionData):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def to_json(self, fmt='str', **kwargs) -> str | dict:
|
|
42
|
+
data_dict = dict(
|
|
43
|
+
name=self.name,
|
|
44
|
+
monitor_id=self.monitor_id,
|
|
45
|
+
order_book={k: v.to_json(fmt='dict') for k, v in self.order_book.items()},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if fmt == 'dict':
|
|
49
|
+
return data_dict
|
|
50
|
+
elif fmt == 'str':
|
|
51
|
+
return json.dumps(data_dict, **kwargs)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f'Invalid format {fmt}, except "dict" or "str".')
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
|
|
57
|
+
if isinstance(json_message, dict):
|
|
58
|
+
json_dict = json_message
|
|
59
|
+
else:
|
|
60
|
+
json_dict = json.loads(json_message)
|
|
61
|
+
|
|
62
|
+
self = cls(
|
|
63
|
+
name=json_dict['name'],
|
|
64
|
+
monitor_id=json_dict['monitor_id'],
|
|
65
|
+
keep_order_log=json_dict['keep_order_log']
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.order_book = {k: MarketData.from_json(v) for k, v in json_dict['order_book'].items()}
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def from_shm(self, name: str = None) -> None:
|
|
72
|
+
if name is None:
|
|
73
|
+
name = f'{self.monitor_id}.json'
|
|
74
|
+
|
|
75
|
+
shm = shared_memory.SharedMemory(name=name)
|
|
76
|
+
json_dict = pickle.loads(bytes(shm.buf))
|
|
77
|
+
|
|
78
|
+
self.clear()
|
|
79
|
+
|
|
80
|
+
self.order_book.update({k: MarketData.from_json(v) for k, v in json_dict['order_book'].items()})
|
|
81
|
+
|
|
82
|
+
def clear(self) -> None:
|
|
83
|
+
self.order_book.clear()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def value(self) -> dict[str, OrderBook]:
|
|
87
|
+
return self.order_book
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MinuteBarMonitor(Monitor):
|
|
91
|
+
|
|
92
|
+
def __init__(self, interval: float = 60., **kwargs):
|
|
93
|
+
self.interval = interval
|
|
94
|
+
|
|
95
|
+
super().__init__(
|
|
96
|
+
name=kwargs.pop('name', 'Monitor.MinuteBarMonitor'),
|
|
97
|
+
monitor_id=kwargs.pop('monitor_id', None)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self._minute_bar_data: dict[str, BarData] = {}
|
|
101
|
+
self._last_bar_data: dict[str, BarData] = {}
|
|
102
|
+
|
|
103
|
+
def __call__(self, market_data: MarketData, **kwargs):
|
|
104
|
+
self._update_last_bar(market_data=market_data, interval=self.interval)
|
|
105
|
+
# self._update_active_bar(market_data=market_data, interval=self.interval)
|
|
106
|
+
|
|
107
|
+
def _update_last_bar(self, market_data: MarketData, interval: float):
|
|
108
|
+
ticker = market_data.ticker
|
|
109
|
+
market_price = market_data.market_price
|
|
110
|
+
market_time = market_data.market_time
|
|
111
|
+
timestamp = market_data.timestamp
|
|
112
|
+
|
|
113
|
+
if ticker not in self._minute_bar_data or market_time >= self._minute_bar_data[ticker].bar_end_time:
|
|
114
|
+
# update bar_data
|
|
115
|
+
if ticker in self._minute_bar_data:
|
|
116
|
+
self._last_bar_data[ticker] = self._minute_bar_data[ticker]
|
|
117
|
+
|
|
118
|
+
bar_data = self._minute_bar_data[ticker] = BarData(
|
|
119
|
+
ticker=ticker,
|
|
120
|
+
timestamp=int(timestamp // interval + 1) * interval,
|
|
121
|
+
start_timestamp=int(timestamp // interval) * interval,
|
|
122
|
+
bar_span=datetime.timedelta(seconds=interval),
|
|
123
|
+
high_price=market_price,
|
|
124
|
+
low_price=market_price,
|
|
125
|
+
open_price=market_price,
|
|
126
|
+
close_price=market_price,
|
|
127
|
+
volume=0.,
|
|
128
|
+
notional=0.,
|
|
129
|
+
trade_count=0
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
bar_data = self._minute_bar_data[ticker]
|
|
133
|
+
|
|
134
|
+
if isinstance(market_data, TradeData):
|
|
135
|
+
bar_data['volume'] += market_data.volume
|
|
136
|
+
bar_data['notional'] += market_data.notional
|
|
137
|
+
bar_data['trade_count'] += 1
|
|
138
|
+
|
|
139
|
+
bar_data['close_price'] = market_price
|
|
140
|
+
bar_data['high_price'] = max(bar_data.high_price, market_price)
|
|
141
|
+
bar_data['low_price'] = min(bar_data.low_price, market_price)
|
|
142
|
+
|
|
143
|
+
def _update_active_bar(self, market_data: MarketData, interval: float):
|
|
144
|
+
ticker = market_data.ticker
|
|
145
|
+
market_price = market_data.market_price
|
|
146
|
+
market_time = market_data.market_time
|
|
147
|
+
timestamp = market_data.timestamp
|
|
148
|
+
|
|
149
|
+
if ticker not in self._minute_bar_data or market_time >= self._minute_bar_data[ticker].bar_end_time:
|
|
150
|
+
bar_data = self._minute_bar_data[ticker] = BarData(
|
|
151
|
+
ticker=ticker,
|
|
152
|
+
start_timestamp=timestamp - interval,
|
|
153
|
+
timestamp=timestamp,
|
|
154
|
+
bar_span=datetime.timedelta(seconds=interval),
|
|
155
|
+
high_price=market_price,
|
|
156
|
+
low_price=market_price,
|
|
157
|
+
open_price=market_price,
|
|
158
|
+
close_price=market_price,
|
|
159
|
+
volume=0.,
|
|
160
|
+
notional=0.,
|
|
161
|
+
trade_count=0
|
|
162
|
+
)
|
|
163
|
+
bar_data.history = []
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
bar_data = self._minute_bar_data[ticker]
|
|
167
|
+
|
|
168
|
+
history: list[TradeData] = getattr(bar_data, 'history')
|
|
169
|
+
bar_data['start_timestamp'] = timestamp - interval
|
|
170
|
+
|
|
171
|
+
if isinstance(market_data, TradeData):
|
|
172
|
+
history.append(market_data)
|
|
173
|
+
|
|
174
|
+
while True:
|
|
175
|
+
if history[0].market_time >= bar_data.bar_start_time:
|
|
176
|
+
break
|
|
177
|
+
else:
|
|
178
|
+
history.pop(0)
|
|
179
|
+
|
|
180
|
+
bar_data['volume'] = sum([_.volume for _ in history])
|
|
181
|
+
bar_data['notional'] = sum([_.notional for _ in history])
|
|
182
|
+
bar_data['trade_count'] = len([_.notional for _ in history])
|
|
183
|
+
bar_data['close_price'] = market_price
|
|
184
|
+
bar_data['open_price'] = history[0].market_price
|
|
185
|
+
bar_data['high_price'] = max([_.market_price for _ in history])
|
|
186
|
+
bar_data['low_price'] = min([_.market_price for _ in history])
|
|
187
|
+
|
|
188
|
+
def to_json(self, fmt='str', **kwargs) -> str | dict:
|
|
189
|
+
data_dict = dict(
|
|
190
|
+
name=self.name,
|
|
191
|
+
monitor_id=self.monitor_id,
|
|
192
|
+
interval=self.interval,
|
|
193
|
+
minute_bar_data={k: v.to_json(fmt='dict') for k, v in self._minute_bar_data.items()},
|
|
194
|
+
last_bar_data={k: v.to_json(fmt='dict') for k, v in self._last_bar_data.items()},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if fmt == 'dict':
|
|
198
|
+
return data_dict
|
|
199
|
+
elif fmt == 'str':
|
|
200
|
+
return json.dumps(data_dict, **kwargs)
|
|
201
|
+
else:
|
|
202
|
+
raise ValueError(f'Invalid format {fmt}, except "dict" or "str".')
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
|
|
206
|
+
if isinstance(json_message, dict):
|
|
207
|
+
json_dict = json_message
|
|
208
|
+
else:
|
|
209
|
+
json_dict = json.loads(json_message)
|
|
210
|
+
|
|
211
|
+
self = cls(
|
|
212
|
+
name=json_dict['name'],
|
|
213
|
+
monitor_id=json_dict['monitor_id'],
|
|
214
|
+
interval=json_dict['interval'],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
self._minute_bar_data = {k: MarketData.from_json(v) for k, v in json_dict['minute_bar_data'].items()}
|
|
218
|
+
self._last_bar_data = {k: MarketData.from_json(v) for k, v in json_dict['last_bar_data'].items()}
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def from_shm(self, name: str = None) -> None:
|
|
222
|
+
if name is None:
|
|
223
|
+
name = f'{self.monitor_id}.json'
|
|
224
|
+
|
|
225
|
+
shm = shared_memory.SharedMemory(name=name)
|
|
226
|
+
json_dict = pickle.loads(bytes(shm.buf))
|
|
227
|
+
|
|
228
|
+
self.clear()
|
|
229
|
+
|
|
230
|
+
self._minute_bar_data.update({k: MarketData.from_json(v) for k, v in json_dict['minute_bar_data'].items()})
|
|
231
|
+
self._last_bar_data.update({k: MarketData.from_json(v) for k, v in json_dict['last_bar_data'].items()})
|
|
232
|
+
|
|
233
|
+
def clear(self) -> None:
|
|
234
|
+
self._minute_bar_data.clear()
|
|
235
|
+
self._last_bar_data.clear()
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def value(self) -> dict[str, BarData]:
|
|
239
|
+
return self._last_bar_data
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Profile(object, metaclass=abc.ABCMeta):
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
profile_id: str,
|
|
10
|
+
session_start: datetime.time | None = None,
|
|
11
|
+
session_end: datetime.time | None = None,
|
|
12
|
+
session_break: list[tuple[datetime.time, datetime.time]] = None
|
|
13
|
+
):
|
|
14
|
+
self.profile_id = profile_id
|
|
15
|
+
self.session_start = session_start
|
|
16
|
+
self.session_end = session_end
|
|
17
|
+
self.session_break = [] if session_break is None else session_break
|
|
18
|
+
|
|
19
|
+
self.time_zone = None
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
return f'<Profile {self.profile_id}>({id(self)})'
|
|
23
|
+
|
|
24
|
+
def override_profile(self, profile: Self = None) -> Self:
|
|
25
|
+
if profile is None:
|
|
26
|
+
profile = PROFILE
|
|
27
|
+
|
|
28
|
+
profile.profile_id = self.profile_id
|
|
29
|
+
profile.session_start = self.session_start
|
|
30
|
+
profile.session_end = self.session_end
|
|
31
|
+
|
|
32
|
+
if profile.session_break is None or self.session_break is None:
|
|
33
|
+
profile.session_break = self.session_break
|
|
34
|
+
else:
|
|
35
|
+
profile.session_break.clear()
|
|
36
|
+
profile.session_break.extend(self.session_break)
|
|
37
|
+
|
|
38
|
+
profile.trade_time_between = self.trade_time_between
|
|
39
|
+
profile.is_market_session = self.is_market_session
|
|
40
|
+
profile.trade_calendar = self.trade_calendar
|
|
41
|
+
|
|
42
|
+
return profile
|
|
43
|
+
|
|
44
|
+
@abc.abstractmethod
|
|
45
|
+
def trade_time_between(self, start_time: datetime.datetime | float, end_time: datetime.datetime | float, **kwargs) -> datetime.timedelta:
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
def is_market_session(self, timestamp: float | int | datetime.datetime, **kwargs) -> bool:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def trade_calendar(self, start_date: datetime.date, end_date: datetime.date, **kwargs) -> list[datetime.date]:
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def range_break(self) -> list[dict]:
|
|
58
|
+
"""
|
|
59
|
+
an range break designed for plotly.
|
|
60
|
+
"""
|
|
61
|
+
range_break = []
|
|
62
|
+
|
|
63
|
+
if not self.session_break:
|
|
64
|
+
return range_break
|
|
65
|
+
|
|
66
|
+
# Convert session_break to range_break format
|
|
67
|
+
for start, end in self.session_break:
|
|
68
|
+
start_hour = start.hour + start.minute / 60
|
|
69
|
+
end_hour = end.hour + end.minute / 60
|
|
70
|
+
range_break.append(dict(bounds=[start_hour, end_hour], pattern="hour"))
|
|
71
|
+
|
|
72
|
+
# Add the additional fixed non-trading periods
|
|
73
|
+
if self.session_start is not None and self.session_start != datetime.time.min:
|
|
74
|
+
range_break.append(
|
|
75
|
+
dict(bounds=[0, self.session_start.hour + self.session_start.minute / 60], pattern="hour"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if self.session_end is not None and self.session_end != datetime.time.max:
|
|
79
|
+
range_break.append(
|
|
80
|
+
dict(bounds=[self.session_end.hour + self.session_end.minute / 60, 24], pattern="hour"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return range_break
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DefaultProfile(Profile):
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__(
|
|
89
|
+
profile_id='non-stop',
|
|
90
|
+
session_start=datetime.time.min,
|
|
91
|
+
session_end=None,
|
|
92
|
+
session_break=None
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def trade_time_between(self, start_time: datetime.datetime | float, end_time: datetime.datetime | float, **kwargs) -> datetime.timedelta:
|
|
96
|
+
if start_time is not None and isinstance(start_time, (float, int)):
|
|
97
|
+
start_time = datetime.datetime.fromtimestamp(start_time, tz=self.time_zone)
|
|
98
|
+
|
|
99
|
+
if end_time is not None and isinstance(end_time, (float, int)):
|
|
100
|
+
end_time = datetime.datetime.fromtimestamp(end_time, tz=self.time_zone)
|
|
101
|
+
|
|
102
|
+
if start_time is None or end_time is None:
|
|
103
|
+
return datetime.timedelta(seconds=0)
|
|
104
|
+
|
|
105
|
+
if start_time > end_time:
|
|
106
|
+
return datetime.timedelta(seconds=0)
|
|
107
|
+
|
|
108
|
+
return end_time - start_time
|
|
109
|
+
|
|
110
|
+
def is_market_session(self, timestamp: float | int | datetime.datetime, **kwargs) -> bool:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
def trade_calendar(self, start_date: datetime.date, end_date: datetime.date, **kwargs) -> list[datetime.date]:
|
|
114
|
+
return [start_date + datetime.timedelta(days=i) for i in range((end_date - start_date).days + 1)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
from .cn import PROFILE_CN
|
|
118
|
+
|
|
119
|
+
PROFILE = DefaultProfile()
|
|
120
|
+
|
|
121
|
+
__all__ = ['Profile', 'PROFILE', 'PROFILE_CN']
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
from . import Profile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProfileCN(Profile):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__(
|
|
10
|
+
profile_id='cn',
|
|
11
|
+
session_start=datetime.time(9, 30),
|
|
12
|
+
session_end=datetime.time(15, 0),
|
|
13
|
+
session_break=[(datetime.time(11, 30), datetime.time(13, 0))]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
self.cn_trade_calendar_cache = {}
|
|
17
|
+
|
|
18
|
+
def override_profile(self, profile: Profile = None):
|
|
19
|
+
profile = super().override_profile(profile=profile)
|
|
20
|
+
|
|
21
|
+
setattr(profile, 'cn_trade_calendar_cache', self.cn_trade_calendar_cache)
|
|
22
|
+
|
|
23
|
+
@functools.lru_cache
|
|
24
|
+
def trade_calendar(self, start_date: datetime.date, end_date: datetime.date, **kwargs) -> list[datetime.date]:
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import exchange_calendars
|
|
27
|
+
|
|
28
|
+
market = kwargs.get('market', 'XSHG')
|
|
29
|
+
tz = kwargs.get('tz', 'UTC')
|
|
30
|
+
|
|
31
|
+
if market in self.cn_trade_calendar_cache:
|
|
32
|
+
trade_calendar = self.cn_trade_calendar_cache[market]
|
|
33
|
+
else:
|
|
34
|
+
trade_calendar = self.cn_trade_calendar_cache[market] = exchange_calendars.get_calendar(market)
|
|
35
|
+
|
|
36
|
+
calendar = trade_calendar.sessions_in_range(start_date, end_date)
|
|
37
|
+
|
|
38
|
+
# noinspection PyTypeChecker
|
|
39
|
+
result = list(pd.to_datetime(calendar).date)
|
|
40
|
+
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
@functools.lru_cache
|
|
44
|
+
def is_trade_day(self, market_date: datetime.date, market='XSHG', tz='UTC') -> bool:
|
|
45
|
+
if market in self.cn_trade_calendar_cache:
|
|
46
|
+
trade_calendar = self.cn_trade_calendar_cache[market]
|
|
47
|
+
else:
|
|
48
|
+
import exchange_calendars
|
|
49
|
+
trade_calendar = self.cn_trade_calendar_cache[market] = exchange_calendars.get_calendar(market)
|
|
50
|
+
|
|
51
|
+
return trade_calendar.is_session(market_date)
|
|
52
|
+
|
|
53
|
+
def trade_days_between(self, start_date: datetime.date, end_date: datetime.date = datetime.date.today(), **kwargs) -> int:
|
|
54
|
+
"""
|
|
55
|
+
Returns the number of trade days between the given date, which is the pre-open of the start_date to the pre-open of the end_date.
|
|
56
|
+
:param start_date: the given trade date
|
|
57
|
+
:param end_date: the given trade date
|
|
58
|
+
:return: integer number of days
|
|
59
|
+
"""
|
|
60
|
+
assert start_date <= end_date, "The end date must not before the start date"
|
|
61
|
+
|
|
62
|
+
if start_date == end_date:
|
|
63
|
+
offset = 0
|
|
64
|
+
else:
|
|
65
|
+
market_date_list = self.trade_calendar(start_date=start_date, end_date=end_date, **kwargs)
|
|
66
|
+
if not market_date_list:
|
|
67
|
+
offset = 0
|
|
68
|
+
else:
|
|
69
|
+
last_trade_date = market_date_list[-1]
|
|
70
|
+
offset = len(market_date_list)
|
|
71
|
+
|
|
72
|
+
if last_trade_date == end_date:
|
|
73
|
+
offset -= 1
|
|
74
|
+
|
|
75
|
+
return offset
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def time_to_seconds(cls, t: datetime.time):
|
|
79
|
+
return (t.hour * 60 + t.minute) * 60 + t.second + t.microsecond / 1000
|
|
80
|
+
|
|
81
|
+
def trade_time_between(self, start_time: datetime.datetime | datetime.time | float | int, end_time: datetime.datetime | datetime.time | float | int, fmt='timedelta', **kwargs):
|
|
82
|
+
if start_time is None or end_time is None:
|
|
83
|
+
if fmt == 'timestamp':
|
|
84
|
+
return 0.
|
|
85
|
+
elif fmt == 'timedelta':
|
|
86
|
+
return datetime.timedelta(0)
|
|
87
|
+
else:
|
|
88
|
+
raise NotImplementedError(f'Invalid fmt {fmt}, should be "timestamp" or "timedelta"')
|
|
89
|
+
|
|
90
|
+
session_start = kwargs.pop('session_start', self.session_start)
|
|
91
|
+
session_break = kwargs.pop('session_break', self.session_break)
|
|
92
|
+
session_end = kwargs.pop('session_end', self.session_end)
|
|
93
|
+
session_length_0 = datetime.timedelta(seconds=self.time_to_seconds(session_break[0]) - self.time_to_seconds(session_start))
|
|
94
|
+
session_length_1 = datetime.timedelta(seconds=self.time_to_seconds(session_end) - self.time_to_seconds(session_break[1]))
|
|
95
|
+
session_length = session_length_0 + session_length_1
|
|
96
|
+
implied_date = datetime.date.today()
|
|
97
|
+
|
|
98
|
+
if isinstance(start_time, (float, int)):
|
|
99
|
+
start_time = datetime.datetime.fromtimestamp(start_time, tz=self.time_zone)
|
|
100
|
+
implied_date = start_time.date()
|
|
101
|
+
|
|
102
|
+
if isinstance(end_time, (float, int)):
|
|
103
|
+
end_time = datetime.datetime.fromtimestamp(end_time, tz=self.time_zone)
|
|
104
|
+
implied_date = end_time.date()
|
|
105
|
+
|
|
106
|
+
if isinstance(start_time, datetime.time):
|
|
107
|
+
start_time = datetime.datetime.combine(implied_date, start_time)
|
|
108
|
+
|
|
109
|
+
if isinstance(end_time, datetime.time):
|
|
110
|
+
end_time = datetime.datetime.combine(implied_date, end_time)
|
|
111
|
+
|
|
112
|
+
offset = datetime.timedelta()
|
|
113
|
+
|
|
114
|
+
market_time = start_time.time()
|
|
115
|
+
|
|
116
|
+
# calculate the timespan from start_time to session_end
|
|
117
|
+
if market_time <= session_start:
|
|
118
|
+
offset += session_length
|
|
119
|
+
elif session_start < market_time <= session_break[0]:
|
|
120
|
+
offset += datetime.datetime.combine(start_time.date(), session_break[0]) - start_time
|
|
121
|
+
offset += session_length_1
|
|
122
|
+
elif session_break[0] < market_time <= session_break[1]:
|
|
123
|
+
offset += session_length_1
|
|
124
|
+
elif session_break[1] < market_time <= session_end:
|
|
125
|
+
offset += datetime.datetime.combine(start_time.date(), session_end) - start_time
|
|
126
|
+
else:
|
|
127
|
+
offset += datetime.timedelta(0)
|
|
128
|
+
|
|
129
|
+
offset -= session_length
|
|
130
|
+
|
|
131
|
+
market_time = end_time.time()
|
|
132
|
+
|
|
133
|
+
# calculate the timespan from session_start to end_time
|
|
134
|
+
if market_time <= session_start:
|
|
135
|
+
offset += datetime.timedelta(0)
|
|
136
|
+
elif session_start < market_time <= session_break[0]:
|
|
137
|
+
offset += end_time - datetime.datetime.combine(end_time.date(), session_start)
|
|
138
|
+
elif session_break[0] < market_time <= session_break[1]:
|
|
139
|
+
offset += session_length_0
|
|
140
|
+
elif session_break[1] < market_time <= session_end:
|
|
141
|
+
offset += end_time - datetime.datetime.combine(end_time.date(), session_break[1])
|
|
142
|
+
offset += session_length_0
|
|
143
|
+
else:
|
|
144
|
+
offset += session_length
|
|
145
|
+
|
|
146
|
+
# calculate market_date difference
|
|
147
|
+
if start_time.date() != end_time.date():
|
|
148
|
+
offset += session_length * self.trade_days_between(start_date=start_time.date(), end_date=end_time.date(), **kwargs)
|
|
149
|
+
|
|
150
|
+
if fmt == 'timestamp':
|
|
151
|
+
return offset.total_seconds()
|
|
152
|
+
elif fmt == 'timedelta':
|
|
153
|
+
return offset
|
|
154
|
+
else:
|
|
155
|
+
raise NotImplementedError(f'Invalid fmt {fmt}, should be "timestamp" or "timedelta"')
|
|
156
|
+
|
|
157
|
+
def is_market_session(self, timestamp: float | int | datetime.datetime, **kwargs) -> bool:
|
|
158
|
+
if isinstance(timestamp, (float, int)):
|
|
159
|
+
market_time = datetime.datetime.fromtimestamp(timestamp, tz=self.time_zone).time()
|
|
160
|
+
elif isinstance(timestamp, datetime.datetime):
|
|
161
|
+
market_time = timestamp.time()
|
|
162
|
+
elif isinstance(timestamp, datetime.time):
|
|
163
|
+
market_time = timestamp
|
|
164
|
+
else:
|
|
165
|
+
raise TypeError(f'Expect timestamp to be a float, int or datetime, got {type(timestamp)}!')
|
|
166
|
+
|
|
167
|
+
if (market_time < datetime.time(9, 30)
|
|
168
|
+
or datetime.time(11, 30) < market_time < datetime.time(13, 0)
|
|
169
|
+
or datetime.time(15, 0) < market_time):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
PROFILE_CN = ProfileCN()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from threading import Lock
|
|
3
|
+
|
|
4
|
+
from .. import LOGGER
|
|
5
|
+
from ..base import TradeInstruction
|
|
6
|
+
from ..engine import EVENT_ENGINE, TOPIC, MDS, MarketDataService, Balance, Inventory, DirectMarketAccess, RiskProfile, PositionManagementService
|
|
7
|
+
|
|
8
|
+
LOGGER = LOGGER.getChild('Strategy')
|
|
9
|
+
|
|
10
|
+
from .strategy_engine import StrategyEngine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventDMA(DirectMarketAccess):
|
|
14
|
+
def __init__(self, mds: MarketDataService, risk_profile: RiskProfile, event_engine=None, cool_down: float = None):
|
|
15
|
+
self.event_engine = EVENT_ENGINE if event_engine is None else event_engine
|
|
16
|
+
super().__init__(mds=mds, risk_profile=risk_profile, cool_down=cool_down)
|
|
17
|
+
|
|
18
|
+
def _launch_order_handler(self, order: TradeInstruction, **kwargs):
|
|
19
|
+
self.event_engine.put(topic=TOPIC.launch_order(ticker=order.ticker), order=order, **kwargs)
|
|
20
|
+
|
|
21
|
+
def _cancel_order_handler(self, order: TradeInstruction, **kwargs):
|
|
22
|
+
self.event_engine.put(topic=TOPIC.cancel_order(ticker=order.ticker), order_id=order.order_id, **kwargs)
|
|
23
|
+
|
|
24
|
+
def _reject_order_handler(self, order: TradeInstruction, **kwargs):
|
|
25
|
+
raise NotImplementedError()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_logger(logger: logging.Logger):
|
|
29
|
+
global LOGGER
|
|
30
|
+
LOGGER = logger
|
|
31
|
+
|
|
32
|
+
strategy_engine.LOGGER = logger.getChild('Strategy')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
REPLAY_LOCK = Lock()
|
|
36
|
+
INVENTORY = Inventory()
|
|
37
|
+
BALANCE = Balance(inventory=INVENTORY) # need to be registered
|
|
38
|
+
RISK_PROFILE = RiskProfile(mds=MDS, balance=BALANCE)
|
|
39
|
+
DMA = EventDMA(mds=MDS, risk_profile=RISK_PROFILE)
|
|
40
|
+
POSITION_TRACKER = PositionManagementService(dma=DMA)
|
|
41
|
+
STRATEGY_ENGINE = StrategyEngine(event_engine=EVENT_ENGINE, position_tracker=POSITION_TRACKER) # need to be registered, also register MDS
|
|
42
|
+
BALANCE.add(strategy=STRATEGY_ENGINE, position_tracker=POSITION_TRACKER)
|
|
43
|
+
|
|
44
|
+
__all__ = ['INVENTORY', 'BALANCE', 'RISK_PROFILE', 'DMA', 'POSITION_TRACKER', 'STRATEGY_ENGINE']
|