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,254 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
import time
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from algo_engine.backtest.metrics import TradeMetrics
|
|
9
|
+
from . import LOGGER
|
|
10
|
+
from .web_app import WebApp
|
|
11
|
+
from ...backtest import SimMatch, ProgressiveReplay
|
|
12
|
+
from ...base import MarketData, TradeReport, TradeInstruction
|
|
13
|
+
from ...profile import Profile, PROFILE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Tester(object, metaclass=abc.ABCMeta):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
start_date: datetime.date,
|
|
20
|
+
end_date: datetime.date,
|
|
21
|
+
dtype: list[str] = None,
|
|
22
|
+
profile: Profile = None,
|
|
23
|
+
**kwargs
|
|
24
|
+
):
|
|
25
|
+
self.start_date = start_date
|
|
26
|
+
self.end_date = end_date
|
|
27
|
+
self.dtype = ['TickData', 'TradeData'] if dtype is None else dtype
|
|
28
|
+
self.profile = PROFILE if profile is None else profile
|
|
29
|
+
|
|
30
|
+
self.timestamp = 0.
|
|
31
|
+
self.last_price = {}
|
|
32
|
+
self.subscription = set()
|
|
33
|
+
self.web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
|
|
34
|
+
self.metrics: dict[str, TradeMetrics] = {}
|
|
35
|
+
self.sim_match: dict[str, SimMatch] = {}
|
|
36
|
+
|
|
37
|
+
def register_ticker(self, ticker: str, **kwargs):
|
|
38
|
+
self.subscription.add(ticker)
|
|
39
|
+
|
|
40
|
+
self.metrics[ticker] = TradeMetrics()
|
|
41
|
+
|
|
42
|
+
self.web_app.register(ticker=ticker, **kwargs)
|
|
43
|
+
|
|
44
|
+
sim_match = self.sim_match[ticker] = SimMatch(
|
|
45
|
+
ticker=ticker,
|
|
46
|
+
instant_fill=kwargs.get('instant_fill', True)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# to add callback function to sim_match, use following codes.
|
|
50
|
+
# sim_match.on_order = self.on_order
|
|
51
|
+
# sim_match.on_report = self.on_report
|
|
52
|
+
|
|
53
|
+
def unregister_ticker(self, ticker: str, **kwargs):
|
|
54
|
+
self.subscription.remove(ticker)
|
|
55
|
+
|
|
56
|
+
self.metrics.pop(ticker)
|
|
57
|
+
|
|
58
|
+
# the web app does not provide an unregister method, however, this is not a requirement
|
|
59
|
+
|
|
60
|
+
sim_match = self.sim_match.pop(ticker)
|
|
61
|
+
sim_match.unregister()
|
|
62
|
+
|
|
63
|
+
def _launch_order(self, ticker: str, volume: float, limit_price: float):
|
|
64
|
+
order = TradeInstruction(ticker=ticker, side=np.sign(volume), volume=abs(float), timestamp=self.timestamp)
|
|
65
|
+
self.sim_match[ticker].launch_order(order=order)
|
|
66
|
+
|
|
67
|
+
def buy(self, ticker: str, volume: float = None, limit_price: float = None):
|
|
68
|
+
if volume is None:
|
|
69
|
+
trade_metrics = self.metrics[ticker]
|
|
70
|
+
exposure = trade_metrics.exposure
|
|
71
|
+
volume = -exposure if exposure < 0 else 1
|
|
72
|
+
|
|
73
|
+
if limit_price is None:
|
|
74
|
+
limit_price = self.last_price[ticker]
|
|
75
|
+
|
|
76
|
+
self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
|
|
77
|
+
|
|
78
|
+
def sell(self, ticker: str, volume: float = None, limit_price: float = None):
|
|
79
|
+
if volume is None:
|
|
80
|
+
trade_metrics = self.metrics[ticker]
|
|
81
|
+
exposure = trade_metrics.exposure
|
|
82
|
+
volume = -exposure if exposure > 0 else -1
|
|
83
|
+
|
|
84
|
+
if limit_price is None:
|
|
85
|
+
limit_price = self.last_price[ticker]
|
|
86
|
+
|
|
87
|
+
self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
|
|
88
|
+
|
|
89
|
+
@abc.abstractmethod
|
|
90
|
+
def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
@abc.abstractmethod
|
|
94
|
+
def on_market_data(self, market_data: MarketData, **kwargs):
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def on_report(self, report: TradeReport, **kwargs):
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@abc.abstractmethod
|
|
102
|
+
def on_order(self, order: TradeInstruction, **kwargs):
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def bod(self, market_date: datetime.date, **kwargs):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def eod(self, market_date: datetime.date, **kwargs):
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def run(self, **kwargs):
|
|
112
|
+
replay = ProgressiveReplay(
|
|
113
|
+
loader=self.load_data,
|
|
114
|
+
tickers=list(self.subscription),
|
|
115
|
+
dtype=['TickData', 'TradeData'],
|
|
116
|
+
start_date=self.start_date,
|
|
117
|
+
end_date=self.end_date,
|
|
118
|
+
bod=self.bod,
|
|
119
|
+
eod=self.eod,
|
|
120
|
+
tick_size=kwargs.get('progress_tick_size', 0.001),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_start_ts = time.time()
|
|
124
|
+
|
|
125
|
+
for market_data in replay:
|
|
126
|
+
self.on_market_data(market_data=market_data)
|
|
127
|
+
self.sim_match[market_data.ticker](market_data=market_data)
|
|
128
|
+
self.web_app.update(market_data=market_data)
|
|
129
|
+
|
|
130
|
+
self.timestamp = market_data.timestamp
|
|
131
|
+
self.last_price[market_data.ticker] = market_data.market_price
|
|
132
|
+
|
|
133
|
+
LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class StrategyTester(Tester):
|
|
137
|
+
from ...strategy.strategy_engine import StrategyEngine
|
|
138
|
+
|
|
139
|
+
def __init__(self, start_date: datetime.date, end_date: datetime.date, data_loader, strategy: StrategyEngine, **kwargs):
|
|
140
|
+
self.data_loader = data_loader
|
|
141
|
+
self.strategy = strategy
|
|
142
|
+
self.event_engine = self.strategy.event_engine
|
|
143
|
+
self.topic_set = self.strategy.topic_set
|
|
144
|
+
self.multi_threading = kwargs.get('multi_threading', False)
|
|
145
|
+
self.lock = self.strategy.lock
|
|
146
|
+
|
|
147
|
+
super().__init__(
|
|
148
|
+
start_date=start_date,
|
|
149
|
+
end_date=end_date,
|
|
150
|
+
dtype=kwargs.pop('dtype', ['TickData', 'TradeData']),
|
|
151
|
+
profile=kwargs.pop('profile', PROFILE),
|
|
152
|
+
event_engine=strategy.event_engine,
|
|
153
|
+
topic_set=strategy.topic_set,
|
|
154
|
+
multi_threading=kwargs.pop('multi_threading', False),
|
|
155
|
+
**kwargs
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def register_ticker(self, ticker: str, **kwargs):
|
|
159
|
+
super().register_ticker(ticker=ticker, **kwargs)
|
|
160
|
+
|
|
161
|
+
for ticker, sim_match in self.sim_match.items():
|
|
162
|
+
sim_match.register(event_engine=self.event_engine, topic_set=self.topic_set)
|
|
163
|
+
|
|
164
|
+
def register(self):
|
|
165
|
+
self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.mds.on_market_data)
|
|
166
|
+
self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.position_tracker.on_market_data)
|
|
167
|
+
self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.on_market_data)
|
|
168
|
+
|
|
169
|
+
self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.strategy.balance.on_order)
|
|
170
|
+
self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.on_order)
|
|
171
|
+
self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.strategy.balance.on_report)
|
|
172
|
+
self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.on_report)
|
|
173
|
+
|
|
174
|
+
def initialize_position_management(self):
|
|
175
|
+
for ticker in self.subscription:
|
|
176
|
+
risk_profile = self.strategy.position_tracker.dma.risk_profile
|
|
177
|
+
|
|
178
|
+
risk_profile.set_rule(ticker=ticker, key='max_trade_long', value=np.inf)
|
|
179
|
+
risk_profile.set_rule(ticker=ticker, key='max_trade_short', value=np.inf)
|
|
180
|
+
risk_profile.set_rule(ticker=ticker, key='max_exposure_long', value=np.inf)
|
|
181
|
+
risk_profile.set_rule(ticker=ticker, key='max_exposure_short', value=np.inf)
|
|
182
|
+
|
|
183
|
+
def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
|
|
184
|
+
return self.data_loader(ticker=ticker, market_date=market_date, dtype=dtype)
|
|
185
|
+
|
|
186
|
+
def bod(self, market_date: datetime.date, **kwargs):
|
|
187
|
+
super().bod(market_date=market_date, **kwargs)
|
|
188
|
+
self.bod(market_date=market_date, **kwargs)
|
|
189
|
+
|
|
190
|
+
def eod(self, market_date: datetime.date, **kwargs):
|
|
191
|
+
super().bod(market_date=market_date, **kwargs)
|
|
192
|
+
self.bod(market_date=market_date, **kwargs)
|
|
193
|
+
|
|
194
|
+
def on_market_data(self, market_data: MarketData, **kwargs):
|
|
195
|
+
self.strategy.__call__(market_data=market_data, **kwargs)
|
|
196
|
+
|
|
197
|
+
if self.lock.locked():
|
|
198
|
+
self.lock.release()
|
|
199
|
+
|
|
200
|
+
def on_report(self, report: TradeReport, **kwargs):
|
|
201
|
+
self.strategy.on_report(report=report, **kwargs)
|
|
202
|
+
|
|
203
|
+
def on_order(self, order: TradeInstruction, **kwargs):
|
|
204
|
+
self.strategy.on_order(order=order, **kwargs)
|
|
205
|
+
|
|
206
|
+
def _launch_order(self, ticker: str, volume: float, limit_price: float):
|
|
207
|
+
self.strategy.open_pos(ticker=ticker, volume=abs(volume), trade_side=np.sign(volume))
|
|
208
|
+
|
|
209
|
+
def buy(self, ticker: str, volume: float = None, limit_price: float = None):
|
|
210
|
+
if ticker not in self.subscription:
|
|
211
|
+
raise ValueError(f'{ticker} not subscribed for trading!')
|
|
212
|
+
|
|
213
|
+
super().buy(ticker=ticker, volume=volume, limit_price=limit_price)
|
|
214
|
+
|
|
215
|
+
def sell(self, ticker: str, volume: float = None, limit_price: float = None):
|
|
216
|
+
if ticker not in self.subscription:
|
|
217
|
+
raise ValueError(f'{ticker} not subscribed for trading!')
|
|
218
|
+
|
|
219
|
+
super().sell(ticker=ticker, volume=volume, limit_price=limit_price)
|
|
220
|
+
|
|
221
|
+
def run(self, **kwargs):
|
|
222
|
+
if not self.event_engine.active:
|
|
223
|
+
self.event_engine.start()
|
|
224
|
+
|
|
225
|
+
replay = ProgressiveReplay(
|
|
226
|
+
loader=self.load_data,
|
|
227
|
+
tickers=list(self.subscription),
|
|
228
|
+
dtype=['TickData', 'TradeData'],
|
|
229
|
+
start_date=self.start_date,
|
|
230
|
+
end_date=self.end_date,
|
|
231
|
+
bod=self.bod,
|
|
232
|
+
eod=self.eod,
|
|
233
|
+
tick_size=kwargs.get('progress_tick_size', 0.001),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
_start_ts = time.time()
|
|
237
|
+
|
|
238
|
+
for market_data in replay:
|
|
239
|
+
if self.multi_threading:
|
|
240
|
+
self.lock.acquire()
|
|
241
|
+
self.event_engine.put(topic=self.topic_set.push(market_data=market_data), market_data=market_data)
|
|
242
|
+
else:
|
|
243
|
+
self.strategy.mds.on_market_data(market_data=market_data)
|
|
244
|
+
self.strategy.position_tracker.on_market_data(market_data=market_data)
|
|
245
|
+
self.strategy.on_market_data(market_data=market_data)
|
|
246
|
+
|
|
247
|
+
if market_data.ticker in self.subscription:
|
|
248
|
+
self.sim_match[market_data.ticker](market_data=market_data)
|
|
249
|
+
self.web_app.update(market_data=market_data)
|
|
250
|
+
|
|
251
|
+
self.timestamp = market_data.timestamp
|
|
252
|
+
self.last_price[market_data.ticker] = market_data.market_price
|
|
253
|
+
|
|
254
|
+
LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import pathlib
|
|
4
|
+
from threading import Thread
|
|
5
|
+
|
|
6
|
+
from .doc_server import CandleStick
|
|
7
|
+
from .. import LOGGER
|
|
8
|
+
from ..bokeh_server import DocManager, DocServer
|
|
9
|
+
from ...profile import Profile, PROFILE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WebApp(object):
|
|
13
|
+
def __init__(self, start_date: datetime.date, end_date: datetime.date, name: str = 'WebApp.Backtest', address: str = '0.0.0.0', port: int = 8080, profile: Profile = None, **kwargs):
|
|
14
|
+
from flask import Flask
|
|
15
|
+
self.start_date = start_date
|
|
16
|
+
self.end_date = end_date
|
|
17
|
+
self.name = name
|
|
18
|
+
self.root_dir = pathlib.Path(__file__).parent
|
|
19
|
+
self.profile = PROFILE if profile is None else profile
|
|
20
|
+
self.host = address
|
|
21
|
+
self.port = port
|
|
22
|
+
|
|
23
|
+
self.flask = Flask(
|
|
24
|
+
import_name=self.name,
|
|
25
|
+
template_folder=self.root_dir.joinpath('templates'),
|
|
26
|
+
static_folder=self.root_dir.joinpath('static')
|
|
27
|
+
)
|
|
28
|
+
self.doc_manager = DocManager(host='localhost', port=port)
|
|
29
|
+
self.dashboard: dict[str, dict[str, DocServer]] = {}
|
|
30
|
+
|
|
31
|
+
def update(self, **kwargs):
|
|
32
|
+
for doc_server in self.doc_manager.doc_server.values():
|
|
33
|
+
doc_server.update(**kwargs)
|
|
34
|
+
|
|
35
|
+
def register(self, ticker: str, **kwargs):
|
|
36
|
+
if ticker in self.dashboard:
|
|
37
|
+
raise ValueError(f'Ticker {ticker} already registered.')
|
|
38
|
+
|
|
39
|
+
dashboard = self.dashboard[ticker] = {}
|
|
40
|
+
candlestick = dashboard[f'candlesticks'] = CandleStick(ticker=ticker, start_date=self.start_date, end_date=self.end_date, **kwargs)
|
|
41
|
+
|
|
42
|
+
self.doc_manager.register(url=f'/candlesticks/{ticker}', doc_server=candlestick)
|
|
43
|
+
|
|
44
|
+
def render_index(self):
|
|
45
|
+
from flask import render_template
|
|
46
|
+
|
|
47
|
+
dashboard_url = {ticker: f'{self.url}/{ticker}' for ticker in self.dashboard}
|
|
48
|
+
|
|
49
|
+
html = render_template(
|
|
50
|
+
'index.html',
|
|
51
|
+
title=f'PyAlgoEngine.Backtest.App',
|
|
52
|
+
data=dashboard_url
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return html
|
|
56
|
+
|
|
57
|
+
def render_dashboard(self, ticker: str):
|
|
58
|
+
from flask import render_template
|
|
59
|
+
from bokeh.embed import server_document
|
|
60
|
+
|
|
61
|
+
dashboard = self.dashboard[ticker]
|
|
62
|
+
bokeh_scripts = {}
|
|
63
|
+
|
|
64
|
+
for name, doc_server in dashboard.items():
|
|
65
|
+
url = self.doc_manager.doc_url[doc_server]
|
|
66
|
+
doc_script = server_document(url=f'http://{self.doc_manager.bokeh_host}:{self.doc_manager.bokeh_port}{url}')
|
|
67
|
+
bokeh_scripts[name] = doc_script
|
|
68
|
+
|
|
69
|
+
html = render_template(
|
|
70
|
+
'dash.html',
|
|
71
|
+
ticker=ticker,
|
|
72
|
+
framework="flask",
|
|
73
|
+
**bokeh_scripts
|
|
74
|
+
)
|
|
75
|
+
return html
|
|
76
|
+
|
|
77
|
+
def serve(self, blocking: bool = True):
|
|
78
|
+
from waitress import serve
|
|
79
|
+
|
|
80
|
+
LOGGER.info(f'starting {self} service...')
|
|
81
|
+
|
|
82
|
+
self.doc_manager.start()
|
|
83
|
+
self.flask.route(rule='/', methods=["GET"])(self.render_index)
|
|
84
|
+
|
|
85
|
+
for ticker in self.dashboard:
|
|
86
|
+
def renderer():
|
|
87
|
+
return self.render_dashboard(ticker=ticker)
|
|
88
|
+
|
|
89
|
+
self.flask.route(rule=f'/{ticker}', methods=["GET"])(renderer)
|
|
90
|
+
|
|
91
|
+
if blocking:
|
|
92
|
+
return serve(app=self.flask, host=self.host, port=self.port)
|
|
93
|
+
|
|
94
|
+
t = Thread(target=serve, kwargs=dict(app=self.flask, host=self.host, port=self.port))
|
|
95
|
+
t.start()
|
|
96
|
+
|
|
97
|
+
# a monkey patch to resolve flask double logging issues
|
|
98
|
+
for hdl in (logger := self.flask.logger).handlers:
|
|
99
|
+
logger.removeHandler(hdl)
|
|
100
|
+
|
|
101
|
+
for hdl in (logger := LOGGER.root).handlers:
|
|
102
|
+
logger.removeHandler(hdl)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def url(self) -> str:
|
|
106
|
+
if self.host == '0.0.0.0':
|
|
107
|
+
return f'http://localhost:{self.port}/'
|
|
108
|
+
else:
|
|
109
|
+
return f'http://{self.host}:{self.port}/'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def start_app(start_date: datetime.date, end_date: datetime.date, blocking: bool = True, **kwargs):
|
|
113
|
+
web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
|
|
114
|
+
web_app.serve(blocking=blocking)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == '__main__':
|
|
118
|
+
parser = argparse.ArgumentParser(description='Start Backtest.App')
|
|
119
|
+
parser.add_argument('--start_date', type=str, required=True, help='Start date in YYYY-MM-DD format')
|
|
120
|
+
parser.add_argument('--end_date', type=str, required=True, help='End date in YYYY-MM-DD format')
|
|
121
|
+
|
|
122
|
+
args = parser.parse_args()
|
|
123
|
+
|
|
124
|
+
start_app(
|
|
125
|
+
start_date=datetime.datetime.strptime(args.start_date, '%Y-%m-%d').date(),
|
|
126
|
+
end_date=datetime.datetime.strptime(args.end_date, '%Y-%m-%d').date(),
|
|
127
|
+
)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import uuid
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from functools import partial
|
|
5
|
+
from threading import Thread, Lock
|
|
6
|
+
from typing import overload
|
|
7
|
+
|
|
8
|
+
from . import LOGGER
|
|
9
|
+
from ..base import MarketData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DocTheme(object, metaclass=abc.ABCMeta):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DocServer(object, metaclass=abc.ABCMeta):
|
|
17
|
+
def __init__(self, theme: DocTheme = None, max_size: int = None, update_interval: float = 0., lock: Lock = None, **kwargs):
|
|
18
|
+
from bokeh.document import Document
|
|
19
|
+
from bokeh.models import ColumnDataSource
|
|
20
|
+
|
|
21
|
+
self.theme: DocTheme = theme
|
|
22
|
+
self.max_size: int = max_size
|
|
23
|
+
self.update_interval: float = update_interval
|
|
24
|
+
self.lock = Lock() if lock is None else lock
|
|
25
|
+
|
|
26
|
+
self.bokeh_documents: dict[int, Document] = {}
|
|
27
|
+
# self.bokeh_source: dict[int, ColumnDataSource] = {}
|
|
28
|
+
self.bokeh_data_pipe: dict[int, dict[str, list[...]]] = {}
|
|
29
|
+
self.bokeh_data_patch: dict[int, dict[str, list[tuple[int, ...]]]] = {}
|
|
30
|
+
self.bokeh_data_source: dict[int, ColumnDataSource] = {}
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
return f'<{self.__class__.__name__}>(id={id(self.__class__)})'
|
|
34
|
+
|
|
35
|
+
def __call__(self, doc):
|
|
36
|
+
self.register_document(doc=doc)
|
|
37
|
+
|
|
38
|
+
def __hash__(self):
|
|
39
|
+
return id(self)
|
|
40
|
+
|
|
41
|
+
@overload
|
|
42
|
+
def update(self, timestamp: float, market_price: float, **kwargs):
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def update(self, timestamp: float, open_price: float, close_price: float, high_price: float, low_price: float, **kwargs):
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def update(self, market_data: MarketData, **kwargs):
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def update(self, **kwargs):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
def layout(self, doc_id: int):
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def stream(self, doc_id: int = None):
|
|
62
|
+
if doc_id is None:
|
|
63
|
+
for doc_id in list(self.bokeh_documents):
|
|
64
|
+
self.stream(doc_id=doc_id)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# doc = self.bokeh_documents[doc_id]
|
|
68
|
+
data_pipe = self.bokeh_data_pipe[doc_id]
|
|
69
|
+
source = self.bokeh_data_source[doc_id]
|
|
70
|
+
|
|
71
|
+
source.stream(new_data=deepcopy(data_pipe), rollover=self.max_size)
|
|
72
|
+
for key, seq in data_pipe.items():
|
|
73
|
+
seq.clear()
|
|
74
|
+
|
|
75
|
+
LOGGER.debug(f'{self.__class__} <stream> updated!')
|
|
76
|
+
|
|
77
|
+
def patch(self, doc_id: int = None):
|
|
78
|
+
if doc_id is None:
|
|
79
|
+
for doc_id in list(self.bokeh_documents):
|
|
80
|
+
self.patch(doc_id=doc_id)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# doc = self.bokeh_documents[doc_id]
|
|
84
|
+
data_patch = self.bokeh_data_patch[doc_id]
|
|
85
|
+
source = self.bokeh_data_source[doc_id]
|
|
86
|
+
|
|
87
|
+
source.patch(patches=deepcopy(data_patch))
|
|
88
|
+
for key, seq in data_patch.items():
|
|
89
|
+
seq.clear()
|
|
90
|
+
|
|
91
|
+
LOGGER.debug(f'{self.__class__} <patch> updated!')
|
|
92
|
+
|
|
93
|
+
def register_document(self, doc):
|
|
94
|
+
from bokeh.models import ColumnDataSource
|
|
95
|
+
|
|
96
|
+
self.lock.acquire()
|
|
97
|
+
|
|
98
|
+
doc_id = uuid.uuid4().int
|
|
99
|
+
|
|
100
|
+
data = deepcopy(self.data)
|
|
101
|
+
self.bokeh_documents[doc_id] = doc
|
|
102
|
+
self.bokeh_data_pipe[doc_id] = {key: [] for key in data}
|
|
103
|
+
self.bokeh_data_patch[doc_id] = {key: [] for key in data}
|
|
104
|
+
self.bokeh_data_source[doc_id] = ColumnDataSource(data=data)
|
|
105
|
+
|
|
106
|
+
self.layout(doc_id=doc_id)
|
|
107
|
+
|
|
108
|
+
if self.update_interval:
|
|
109
|
+
doc.add_periodic_callback(callback=partial(self.stream, doc_id=doc_id), period_milliseconds=int(self.update_interval * 1000))
|
|
110
|
+
doc.add_periodic_callback(callback=partial(self.patch, doc_id=doc_id), period_milliseconds=int(self.update_interval * 1000))
|
|
111
|
+
|
|
112
|
+
doc.on_session_destroyed(partial(self._unregister_document, doc_id=doc_id))
|
|
113
|
+
|
|
114
|
+
LOGGER.info(f'{self} registered Bokeh document id = {doc_id}!')
|
|
115
|
+
self.lock.release()
|
|
116
|
+
|
|
117
|
+
def _unregister_document(self, session_context, doc_id: int):
|
|
118
|
+
self.lock.acquire()
|
|
119
|
+
LOGGER.info(f'Session {doc_id} disconnected!')
|
|
120
|
+
|
|
121
|
+
self.bokeh_documents.pop(doc_id)
|
|
122
|
+
self.bokeh_data_pipe.pop(doc_id)
|
|
123
|
+
self.bokeh_data_patch.pop(doc_id)
|
|
124
|
+
self.bokeh_data_source.pop(doc_id)
|
|
125
|
+
self.lock.release()
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
@abc.abstractmethod
|
|
129
|
+
def data(self) -> dict[str, list]:
|
|
130
|
+
"""
|
|
131
|
+
the data used to provide initial values for new bokeh.ColumnDataSource.
|
|
132
|
+
"""
|
|
133
|
+
...
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DocManager(object):
|
|
137
|
+
def __init__(self, host: str = 'localhost', port: int = 21543, **kwargs):
|
|
138
|
+
self.host = host
|
|
139
|
+
self.port = port
|
|
140
|
+
|
|
141
|
+
self.bokeh_host = kwargs.get('bokeh_host', 'localhost')
|
|
142
|
+
self.bokeh_port = kwargs.get('bokeh_port', 5006)
|
|
143
|
+
self.bokeh_check_unused_sessions = kwargs.get('bokeh_check_unused_sessions', 1)
|
|
144
|
+
|
|
145
|
+
self.doc_server: dict[str, DocServer] = {}
|
|
146
|
+
self.doc_url: dict[DocServer, str] = {}
|
|
147
|
+
self.bokeh_thread = Thread(target=self.serve_bokeh, daemon=True)
|
|
148
|
+
|
|
149
|
+
def __getitem__(self, url: str):
|
|
150
|
+
return self.doc_server.__getitem__(url)
|
|
151
|
+
|
|
152
|
+
def __setitem__(self, url: str, doc_server: DocServer):
|
|
153
|
+
return self.register(url=url, doc_server=doc_server)
|
|
154
|
+
|
|
155
|
+
def __contains__(self, url: str):
|
|
156
|
+
return self.doc_server.__contains__(url)
|
|
157
|
+
|
|
158
|
+
def register(self, url: str, doc_server: DocServer):
|
|
159
|
+
if url in self.doc_server:
|
|
160
|
+
LOGGER.warning(f'{url} already registered! Existed doc_server {self.doc_server[url]} overridden!')
|
|
161
|
+
|
|
162
|
+
self.doc_server[url] = doc_server
|
|
163
|
+
self.doc_url[doc_server] = url
|
|
164
|
+
return doc_server
|
|
165
|
+
|
|
166
|
+
def serve_bokeh(self):
|
|
167
|
+
from bokeh.server.server import Server
|
|
168
|
+
import psutil
|
|
169
|
+
import socket
|
|
170
|
+
|
|
171
|
+
# Get all network interfaces and their IP addresses
|
|
172
|
+
addrs = psutil.net_if_addrs()
|
|
173
|
+
websocket_origin = [
|
|
174
|
+
f"{self.host}:{self.port}"
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
for interface, addr_info in addrs.items():
|
|
178
|
+
for addr in addr_info:
|
|
179
|
+
if addr.family == socket.AF_INET: # Filter only IPv4 addresses
|
|
180
|
+
LOGGER.info(f"Binding network interface: {interface}, IP Address: {addr.address}")
|
|
181
|
+
websocket_origin.append(f"{addr.address}:{self.port}")
|
|
182
|
+
|
|
183
|
+
server = Server(
|
|
184
|
+
applications=self.doc_server,
|
|
185
|
+
address=self.bokeh_host,
|
|
186
|
+
port=self.bokeh_port,
|
|
187
|
+
check_unused_sessions_milliseconds=(self.bokeh_check_unused_sessions * 1000),
|
|
188
|
+
allow_websocket_origin=[f"{self.bokeh_host}:{self.bokeh_port}"] + websocket_origin,
|
|
189
|
+
# num_procs=1
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
LOGGER.info(
|
|
193
|
+
f'bokeh service started at {self.bokeh_host}:{self.bokeh_port}!\n' +
|
|
194
|
+
'\n'.join([f'http://{self.bokeh_host}:{self.bokeh_port}{url} => {app}' for url, app in self.doc_server.items()])
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
server.start()
|
|
198
|
+
|
|
199
|
+
# for url in applications:
|
|
200
|
+
# server.io_loop.add_callback(server.show, url)
|
|
201
|
+
|
|
202
|
+
server.io_loop.start()
|
|
203
|
+
|
|
204
|
+
def start(self):
|
|
205
|
+
self.bokeh_thread.start()
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from algo_engine.apps.backtest import WebApp, LOGGER
|
|
5
|
+
from algo_engine.base import Progress
|
|
6
|
+
from algo_engine.profile import PROFILE_CN
|
|
7
|
+
from algo_engine.utils import fake_data
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
PROFILE_CN.override_profile()
|
|
12
|
+
ticker = '000016.SH'
|
|
13
|
+
market_date = datetime.date.today()
|
|
14
|
+
|
|
15
|
+
data_set = fake_data(market_date=market_date)
|
|
16
|
+
web_app = WebApp(start_date=market_date, end_date=market_date)
|
|
17
|
+
LOGGER.info(f'{len(data_set)} fake data generated for {ticker} {market_date}.')
|
|
18
|
+
|
|
19
|
+
web_app.register(ticker=ticker)
|
|
20
|
+
web_app.serve(blocking=False)
|
|
21
|
+
|
|
22
|
+
LOGGER.info(f'web app started at {web_app.url}')
|
|
23
|
+
|
|
24
|
+
for ts, row in Progress(list(data_set.iterrows())):
|
|
25
|
+
web_app.update(
|
|
26
|
+
timestamp=ts,
|
|
27
|
+
ticker=ticker,
|
|
28
|
+
open_price=row['open_price'],
|
|
29
|
+
close_price=row['close_price'],
|
|
30
|
+
high_price=row['high_price'],
|
|
31
|
+
low_price=row['low_price'],
|
|
32
|
+
)
|
|
33
|
+
time.sleep(0.05)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
38
|
+
|
|
39
|
+
# sys.exit(-1)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .. import LOGGER
|
|
4
|
+
|
|
5
|
+
LOGGER = LOGGER.getChild('BackTest')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def set_logger(logger: logging.Logger):
|
|
9
|
+
global LOGGER
|
|
10
|
+
LOGGER = logger
|
|
11
|
+
|
|
12
|
+
replay.LOGGER = LOGGER.getChild('Replay')
|
|
13
|
+
sim_match.LOGGER = LOGGER.getChild('SimMatch')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from .replay import Replay, SimpleReplay, ProgressiveReplay
|
|
17
|
+
from .sim_match import SimMatch
|
|
18
|
+
|
|
19
|
+
__all__ = ['Replay', 'SimpleReplay', 'ProgressiveReplay', 'SimMatch']
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
__package__ = 'algo_engine.backtest'
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
import event_engine
|
|
7
|
+
|
|
8
|
+
from ..engine import TOPIC, MarketDataService, Balance, RiskProfile, PositionManagementService
|
|
9
|
+
from ..engine.algo_engine import AlgoRegistry, AlgoEngine
|
|
10
|
+
from ..strategy import EventDMA
|
|
11
|
+
from ..strategy.strategy_engine import StrategyEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_stop(code=0):
|
|
15
|
+
EVENT_ENGINE.stop()
|
|
16
|
+
# noinspection PyUnresolvedReferences, PyProtectedMember
|
|
17
|
+
# `import os`
|
|
18
|
+
# `os._exit(code)`
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_start(start_date: datetime.date, end_date: datetime.date, data_loader: Callable, **kwargs):
|
|
22
|
+
EVENT_ENGINE.start()
|
|
23
|
+
STRATEGY_ENGINE.back_test(
|
|
24
|
+
start_date=start_date,
|
|
25
|
+
end_date=end_date,
|
|
26
|
+
data_loader=data_loader,
|
|
27
|
+
**kwargs
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# in backtest, the global objects is newly inited to separate from production
|
|
32
|
+
EVENT_ENGINE = event_engine.EventEngine()
|
|
33
|
+
MDS = MarketDataService()
|
|
34
|
+
ALGO_REGISTRY = AlgoRegistry()
|
|
35
|
+
ALGO_ENGINE = AlgoEngine(mds=MDS, registry=ALGO_REGISTRY)
|
|
36
|
+
|
|
37
|
+
BALANCE = Balance()
|
|
38
|
+
RISK_PROFILE = RiskProfile(mds=MDS, balance=BALANCE)
|
|
39
|
+
DMA = EventDMA(event_engine=EVENT_ENGINE, mds=MDS, risk_profile=RISK_PROFILE)
|
|
40
|
+
POSITION_TRACKER = PositionManagementService(dma=DMA, algo_engine=ALGO_ENGINE)
|
|
41
|
+
STRATEGY_ENGINE = StrategyEngine(event_engine=EVENT_ENGINE, position_tracker=POSITION_TRACKER)
|
|
42
|
+
BALANCE.add(strategy=STRATEGY_ENGINE, position_tracker=POSITION_TRACKER)
|
|
43
|
+
|
|
44
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.realtime, handler=MDS.on_market_data)
|
|
45
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.on_report, handler=BALANCE.on_report)
|
|
46
|
+
EVENT_ENGINE.register_handler(topic=TOPIC.on_order, handler=BALANCE.on_order)
|
|
47
|
+
STRATEGY_ENGINE.register()
|
|
48
|
+
|
|
49
|
+
MDS.synthetic_orderbook = True
|
|
50
|
+
|
|
51
|
+
__all__ = ['BALANCE', 'RISK_PROFILE', 'DMA', 'POSITION_TRACKER', 'STRATEGY_ENGINE', 'BALANCE', 'EVENT_ENGINE', 'MDS']
|