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.
Files changed (43) hide show
  1. PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
  2. PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
  3. PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
  4. PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
  5. PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
  6. algo_engine/__init__.py +41 -0
  7. algo_engine/apps/__init__.py +17 -0
  8. algo_engine/apps/backtest/__init__.py +20 -0
  9. algo_engine/apps/backtest/doc_server.py +331 -0
  10. algo_engine/apps/backtest/tester.py +254 -0
  11. algo_engine/apps/backtest/web_app.py +127 -0
  12. algo_engine/apps/bokeh_server.py +205 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +39 -0
  15. algo_engine/backtest/__init__.py +19 -0
  16. algo_engine/backtest/__main__.py +51 -0
  17. algo_engine/backtest/metrics.py +179 -0
  18. algo_engine/backtest/replay.py +261 -0
  19. algo_engine/backtest/sim_match.py +295 -0
  20. algo_engine/base/__init__.py +40 -0
  21. algo_engine/base/console_utils.py +1070 -0
  22. algo_engine/base/finance_decimal.py +258 -0
  23. algo_engine/base/market_buffer.py +571 -0
  24. algo_engine/base/market_utils.py +3092 -0
  25. algo_engine/base/market_utils_nt.py +188 -0
  26. algo_engine/base/market_utils_posix.py +3004 -0
  27. algo_engine/base/technical_analysis.py +406 -0
  28. algo_engine/base/telemetrics.py +78 -0
  29. algo_engine/base/trade_utils.py +709 -0
  30. algo_engine/engine/__init__.py +28 -0
  31. algo_engine/engine/algo_engine.py +901 -0
  32. algo_engine/engine/event_engine.py +53 -0
  33. algo_engine/engine/market_engine.py +370 -0
  34. algo_engine/engine/trade_engine.py +2037 -0
  35. algo_engine/monitor/__init__.py +15 -0
  36. algo_engine/monitor/advanced_data_interface.py +239 -0
  37. algo_engine/profile/__init__.py +121 -0
  38. algo_engine/profile/cn.py +175 -0
  39. algo_engine/strategy/__init__.py +44 -0
  40. algo_engine/strategy/strategy_engine.py +440 -0
  41. algo_engine/utils/__init__.py +3 -0
  42. algo_engine/utils/commit_regularizer.py +49 -0
  43. 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']