PyAlgoEngine 0.12.0.post3__cp313-cp313-win_amd64.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 (156) hide show
  1. algo_engine/__infra__.pxd +3 -0
  2. algo_engine/__init__.pxd +3 -0
  3. algo_engine/__init__.py +91 -0
  4. algo_engine/apps/__init__.py +17 -0
  5. algo_engine/apps/backtest/__init__.py +20 -0
  6. algo_engine/apps/backtest/doc_server.py +328 -0
  7. algo_engine/apps/backtest/static/styles/dash.css +48 -0
  8. algo_engine/apps/backtest/templates/dash.html +20 -0
  9. algo_engine/apps/backtest/templates/index.html +40 -0
  10. algo_engine/apps/backtest/tester.py +252 -0
  11. algo_engine/apps/backtest/web_app.py +125 -0
  12. algo_engine/apps/bokeh_server.py +245 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +40 -0
  15. algo_engine/apps/sim_input/__init__.py +23 -0
  16. algo_engine/apps/sim_input/client.py +412 -0
  17. algo_engine/apps/sim_input/sim_keyboard.py +88 -0
  18. algo_engine/apps/sim_input/sim_mouse.py +137 -0
  19. algo_engine/apps/sim_input/window.py +162 -0
  20. algo_engine/backtest/__init__.py +19 -0
  21. algo_engine/backtest/__main__.py +51 -0
  22. algo_engine/backtest/metrics.py +179 -0
  23. algo_engine/backtest/replay.py +635 -0
  24. algo_engine/backtest/sim_match.py +506 -0
  25. algo_engine/base/__infra__.pxd +3 -0
  26. algo_engine/base/__init__.pxd +3 -0
  27. algo_engine/base/__init__.py +60 -0
  28. algo_engine/base/c_allocator_protocol.c +10756 -0
  29. algo_engine/base/c_allocator_protocol.cp313-win_amd64.pyd +0 -0
  30. algo_engine/base/c_allocator_protocol.pxd +24 -0
  31. algo_engine/base/c_allocator_protocol.pyi +68 -0
  32. algo_engine/base/c_allocator_protocol.pyx +88 -0
  33. algo_engine/base/c_intern_string.c +5845 -0
  34. algo_engine/base/c_intern_string.cp313-win_amd64.pyd +0 -0
  35. algo_engine/base/c_intern_string.pxd +14 -0
  36. algo_engine/base/c_intern_string.pyi +22 -0
  37. algo_engine/base/c_intern_string.pyx +17 -0
  38. algo_engine/base/c_market_data/__infra__.pxd +196 -0
  39. algo_engine/base/c_market_data/__init__.pxd +196 -0
  40. algo_engine/base/c_market_data/__init__.py +24 -0
  41. algo_engine/base/c_market_data/c_candlestick.c +18960 -0
  42. algo_engine/base/c_market_data/c_candlestick.cp313-win_amd64.pyd +0 -0
  43. algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
  44. algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
  45. algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
  46. algo_engine/base/c_market_data/c_internal.c +14023 -0
  47. algo_engine/base/c_market_data/c_internal.cp313-win_amd64.pyd +0 -0
  48. algo_engine/base/c_market_data/c_internal.pxd +14 -0
  49. algo_engine/base/c_market_data/c_internal.pyi +47 -0
  50. algo_engine/base/c_market_data/c_internal.pyx +41 -0
  51. algo_engine/base/c_market_data/c_market_data.c +30739 -0
  52. algo_engine/base/c_market_data/c_market_data.cp313-win_amd64.pyd +0 -0
  53. algo_engine/base/c_market_data/c_market_data.h +1464 -0
  54. algo_engine/base/c_market_data/c_market_data.pxd +418 -0
  55. algo_engine/base/c_market_data/c_market_data.pyi +550 -0
  56. algo_engine/base/c_market_data/c_market_data.pyx +688 -0
  57. algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
  58. algo_engine/base/c_market_data/c_market_data_buffer.cp313-win_amd64.pyd +0 -0
  59. algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  60. algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
  61. algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
  62. algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
  63. algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  64. algo_engine/base/c_market_data/c_tick.c +44230 -0
  65. algo_engine/base/c_market_data/c_tick.cp313-win_amd64.pyd +0 -0
  66. algo_engine/base/c_market_data/c_tick.pxd +53 -0
  67. algo_engine/base/c_market_data/c_tick.pyi +466 -0
  68. algo_engine/base/c_market_data/c_tick.pyx +673 -0
  69. algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
  70. algo_engine/base/c_market_data/c_trade_utils.cp313-win_amd64.pyd +0 -0
  71. algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
  72. algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
  73. algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
  74. algo_engine/base/c_market_data/c_transaction.c +23521 -0
  75. algo_engine/base/c_market_data/c_transaction.cp313-win_amd64.pyd +0 -0
  76. algo_engine/base/c_market_data/c_transaction.pxd +27 -0
  77. algo_engine/base/c_market_data/c_transaction.pyi +433 -0
  78. algo_engine/base/c_market_data/c_transaction.pyx +460 -0
  79. algo_engine/base/console_utils.py +1070 -0
  80. algo_engine/base/finance_decimal.py +258 -0
  81. algo_engine/base/telemetrics.py +104 -0
  82. algo_engine/engine/__infra__.pxd +10 -0
  83. algo_engine/engine/__init__.pxd +10 -0
  84. algo_engine/engine/__init__.py +40 -0
  85. algo_engine/engine/algo_engine.py +904 -0
  86. algo_engine/engine/c_event_engine.c +16289 -0
  87. algo_engine/engine/c_event_engine.cp313-win_amd64.pyd +0 -0
  88. algo_engine/engine/c_event_engine.pxd +25 -0
  89. algo_engine/engine/c_event_engine.pyi +68 -0
  90. algo_engine/engine/c_market_engine.c +24222 -0
  91. algo_engine/engine/c_market_engine.cp313-win_amd64.pyd +0 -0
  92. algo_engine/engine/c_market_engine.pxd +87 -0
  93. algo_engine/engine/c_market_engine.pyi +357 -0
  94. algo_engine/engine/event_engine.py +53 -0
  95. algo_engine/engine/trade_engine.py +2037 -0
  96. algo_engine/exchange_profile/__infra__.pxd +99 -0
  97. algo_engine/exchange_profile/__init__.pxd +99 -0
  98. algo_engine/exchange_profile/__init__.py +36 -0
  99. algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  100. algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
  101. algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  102. algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  103. algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
  104. algo_engine/exchange_profile/c_exchange_profile.cp313-win_amd64.pyd +0 -0
  105. algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
  106. algo_engine/exchange_profile/c_exchange_profile.pyi +867 -0
  107. algo_engine/exchange_profile/c_exchange_profile.pyx +1482 -0
  108. algo_engine/exchange_profile/c_profile_cn.c +7779 -0
  109. algo_engine/exchange_profile/c_profile_cn.cp313-win_amd64.pyd +0 -0
  110. algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
  111. algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
  112. algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
  113. algo_engine/exchange_profile/c_profile_default.c +7779 -0
  114. algo_engine/exchange_profile/c_profile_default.cp313-win_amd64.pyd +0 -0
  115. algo_engine/exchange_profile/c_profile_default.pxd +1 -0
  116. algo_engine/exchange_profile/c_profile_default.pyi +3 -0
  117. algo_engine/exchange_profile/c_profile_default.pyx +1 -0
  118. algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
  119. algo_engine/exchange_profile/c_profile_dispatcher.cp313-win_amd64.pyd +0 -0
  120. algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
  121. algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
  122. algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
  123. algo_engine/includes/algo_engine/base/c_allocator_protocol.c +10756 -0
  124. algo_engine/includes/algo_engine/base/c_intern_string.c +5845 -0
  125. algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18960 -0
  126. algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14023 -0
  127. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30739 -0
  128. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
  129. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
  130. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  131. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  132. algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44230 -0
  133. algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
  134. algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23521 -0
  135. algo_engine/includes/algo_engine/engine/c_event_engine.c +16289 -0
  136. algo_engine/includes/algo_engine/engine/c_market_engine.c +24222 -0
  137. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  138. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
  139. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  140. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  141. algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
  142. algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7779 -0
  143. algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7779 -0
  144. algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
  145. algo_engine/monitor/__init__.py +15 -0
  146. algo_engine/monitor/advanced_data_interface.py +334 -0
  147. algo_engine/strategy/__init__.py +44 -0
  148. algo_engine/strategy/strategy_engine.py +441 -0
  149. algo_engine/utils/__init__.py +3 -0
  150. algo_engine/utils/commit_regularizer.py +49 -0
  151. algo_engine/utils/data_utils.py +296 -0
  152. pyalgoengine-0.12.0.post3.dist-info/METADATA +142 -0
  153. pyalgoengine-0.12.0.post3.dist-info/RECORD +156 -0
  154. pyalgoengine-0.12.0.post3.dist-info/WHEEL +5 -0
  155. pyalgoengine-0.12.0.post3.dist-info/licenses/LICENSE +21 -0
  156. pyalgoengine-0.12.0.post3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,252 @@
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.apps import LOGGER
9
+ from algo_engine.apps.backtest.web_app import WebApp
10
+ from algo_engine.backtest import ProgressReplay, SimMatch
11
+ from algo_engine.backtest.metrics import TradeMetrics
12
+ from algo_engine.base import MarketData, TradeInstruction, TradeReport
13
+
14
+
15
+ class Tester(object, metaclass=abc.ABCMeta):
16
+ def __init__(
17
+ self,
18
+ start_date: datetime.date,
19
+ end_date: datetime.date,
20
+ dtype: list[str] = None,
21
+ **kwargs
22
+ ):
23
+ self.start_date = start_date
24
+ self.end_date = end_date
25
+ self.dtype = ['TickData', 'TradeData'] if dtype is None else dtype
26
+
27
+ self.timestamp = 0.
28
+ self.last_price = {}
29
+ self.subscription = set()
30
+ self.web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
31
+ self.metrics: dict[str, TradeMetrics] = {}
32
+ self.sim_match: dict[str, SimMatch] = {}
33
+
34
+ def register_ticker(self, ticker: str, **kwargs):
35
+ self.subscription.add(ticker)
36
+
37
+ self.metrics[ticker] = TradeMetrics()
38
+
39
+ self.web_app.register(ticker=ticker, **kwargs)
40
+
41
+ sim_match = self.sim_match[ticker] = SimMatch(
42
+ ticker=ticker,
43
+ instant_fill=kwargs.get('instant_fill', True)
44
+ )
45
+
46
+ # to add callback function to sim_match, use following codes.
47
+ # sim_match.on_order = self.on_order
48
+ # sim_match.on_report = self.on_report
49
+
50
+ def unregister_ticker(self, ticker: str, **kwargs):
51
+ self.subscription.remove(ticker)
52
+
53
+ self.metrics.pop(ticker)
54
+
55
+ # the web app does not provide an unregister method, however, this is not a requirement
56
+
57
+ sim_match = self.sim_match.pop(ticker)
58
+ sim_match.unregister()
59
+
60
+ def _launch_order(self, ticker: str, volume: float, limit_price: float):
61
+ order = TradeInstruction(ticker=ticker, side=np.sign(volume), volume=abs(float), timestamp=self.timestamp)
62
+ self.sim_match[ticker].launch_order(order=order)
63
+
64
+ def buy(self, ticker: str, volume: float = None, limit_price: float = None):
65
+ if volume is None:
66
+ trade_metrics = self.metrics[ticker]
67
+ exposure = trade_metrics.exposure
68
+ volume = -exposure if exposure < 0 else 1
69
+
70
+ if limit_price is None:
71
+ limit_price = self.last_price[ticker]
72
+
73
+ self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
74
+
75
+ def sell(self, ticker: str, volume: float = None, limit_price: float = None):
76
+ if volume is None:
77
+ trade_metrics = self.metrics[ticker]
78
+ exposure = trade_metrics.exposure
79
+ volume = -exposure if exposure > 0 else -1
80
+
81
+ if limit_price is None:
82
+ limit_price = self.last_price[ticker]
83
+
84
+ self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
85
+
86
+ @abc.abstractmethod
87
+ def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
88
+ ...
89
+
90
+ @abc.abstractmethod
91
+ def on_market_data(self, market_data: MarketData, **kwargs):
92
+ ...
93
+
94
+ @abc.abstractmethod
95
+ def on_report(self, report: TradeReport, **kwargs):
96
+ ...
97
+
98
+ @abc.abstractmethod
99
+ def on_order(self, order: TradeInstruction, **kwargs):
100
+ ...
101
+
102
+ def bod(self, market_date: datetime.date, **kwargs):
103
+ pass
104
+
105
+ def eod(self, market_date: datetime.date, **kwargs):
106
+ pass
107
+
108
+ def run(self, **kwargs):
109
+ replay = ProgressReplay(
110
+ loader=self.load_data,
111
+ start_date=self.start_date,
112
+ end_date=self.end_date,
113
+ bod=self.bod,
114
+ eod=self.eod,
115
+ )
116
+
117
+ for ticker in self.subscription:
118
+ replay.add_subscription(ticker, dtype='TickData')
119
+ replay.add_subscription(ticker, dtype='TradeData')
120
+
121
+ _start_ts = time.time()
122
+
123
+ for market_data in replay:
124
+ self.on_market_data(market_data=market_data)
125
+ self.sim_match[market_data.ticker](market_data=market_data)
126
+ self.web_app.update(market_data=market_data)
127
+
128
+ self.timestamp = market_data.timestamp
129
+ self.last_price[market_data.ticker] = market_data.market_price
130
+
131
+ LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
132
+
133
+
134
+ class StrategyTester(Tester):
135
+ from ...strategy.strategy_engine import StrategyEngine
136
+
137
+ def __init__(self, start_date: datetime.date, end_date: datetime.date, data_loader, strategy: StrategyEngine, **kwargs):
138
+ self.data_loader = data_loader
139
+ self.strategy = strategy
140
+ self.event_engine = self.strategy.event_engine
141
+ self.topic_set = self.strategy.topic_set
142
+ self.multi_threading = kwargs.get('multi_threading', False)
143
+ self.lock = self.strategy.lock
144
+
145
+ super().__init__(
146
+ start_date=start_date,
147
+ end_date=end_date,
148
+ dtype=kwargs.pop('dtype', ['TickData', 'TradeData']),
149
+ event_engine=strategy.event_engine,
150
+ topic_set=strategy.topic_set,
151
+ multi_threading=kwargs.pop('multi_threading', False),
152
+ **kwargs
153
+ )
154
+
155
+ def register_ticker(self, ticker: str, **kwargs):
156
+ super().register_ticker(ticker=ticker, **kwargs)
157
+
158
+ for ticker, sim_match in self.sim_match.items():
159
+ sim_match.register(event_engine=self.event_engine, topic_set=self.topic_set)
160
+
161
+ def register(self):
162
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.mds.on_market_data)
163
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.position_tracker.on_market_data)
164
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.on_market_data)
165
+
166
+ self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.strategy.balance.on_order)
167
+ self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.on_order)
168
+ self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.strategy.balance.on_report)
169
+ self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.on_report)
170
+
171
+ def initialize_position_management(self):
172
+ for ticker in self.subscription:
173
+ risk_profile = self.strategy.position_tracker.dma.risk_profile
174
+
175
+ risk_profile.set_rule(ticker=ticker, key='max_trade_long', value=np.inf)
176
+ risk_profile.set_rule(ticker=ticker, key='max_trade_short', value=np.inf)
177
+ risk_profile.set_rule(ticker=ticker, key='max_exposure_long', value=np.inf)
178
+ risk_profile.set_rule(ticker=ticker, key='max_exposure_short', value=np.inf)
179
+
180
+ def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
181
+ return self.data_loader(ticker=ticker, market_date=market_date, dtype=dtype)
182
+
183
+ def bod(self, market_date: datetime.date, **kwargs):
184
+ super().bod(market_date=market_date, **kwargs)
185
+ self.bod(market_date=market_date, **kwargs)
186
+
187
+ def eod(self, market_date: datetime.date, **kwargs):
188
+ super().bod(market_date=market_date, **kwargs)
189
+ self.bod(market_date=market_date, **kwargs)
190
+
191
+ def on_market_data(self, market_data: MarketData, **kwargs):
192
+ self.strategy.__call__(market_data=market_data, **kwargs)
193
+
194
+ if self.lock.locked():
195
+ self.lock.release()
196
+
197
+ def on_report(self, report: TradeReport, **kwargs):
198
+ self.strategy.on_report(report=report, **kwargs)
199
+
200
+ def on_order(self, order: TradeInstruction, **kwargs):
201
+ self.strategy.on_order(order=order, **kwargs)
202
+
203
+ def _launch_order(self, ticker: str, volume: float, limit_price: float):
204
+ self.strategy.open_pos(ticker=ticker, volume=abs(volume), trade_side=np.sign(volume))
205
+
206
+ def buy(self, ticker: str, volume: float = None, limit_price: float = None):
207
+ if ticker not in self.subscription:
208
+ raise ValueError(f'{ticker} not subscribed for trading!')
209
+
210
+ super().buy(ticker=ticker, volume=volume, limit_price=limit_price)
211
+
212
+ def sell(self, ticker: str, volume: float = None, limit_price: float = None):
213
+ if ticker not in self.subscription:
214
+ raise ValueError(f'{ticker} not subscribed for trading!')
215
+
216
+ super().sell(ticker=ticker, volume=volume, limit_price=limit_price)
217
+
218
+ def run(self, **kwargs):
219
+ if not self.event_engine.active:
220
+ self.event_engine.start()
221
+
222
+ replay = ProgressReplay(
223
+ loader=self.load_data,
224
+ start_date=self.start_date,
225
+ end_date=self.end_date,
226
+ bod=self.bod,
227
+ eod=self.eod,
228
+ )
229
+
230
+ for ticker in self.subscription:
231
+ replay.add_subscription(ticker, dtype='TickData')
232
+ replay.add_subscription(ticker, dtype='TradeData')
233
+
234
+ _start_ts = time.time()
235
+
236
+ for market_data in replay:
237
+ if self.multi_threading:
238
+ self.lock.acquire()
239
+ self.event_engine.put(topic=self.topic_set.push(market_data=market_data), market_data=market_data)
240
+ else:
241
+ self.strategy.mds.on_market_data(market_data=market_data)
242
+ self.strategy.position_tracker.on_market_data(market_data=market_data)
243
+ self.strategy.on_market_data(market_data=market_data)
244
+
245
+ if market_data.ticker in self.subscription:
246
+ self.sim_match[market_data.ticker](market_data=market_data)
247
+ self.web_app.update(market_data=market_data)
248
+
249
+ self.timestamp = market_data.timestamp
250
+ self.last_price[market_data.ticker] = market_data.market_price
251
+
252
+ LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
@@ -0,0 +1,125 @@
1
+ import argparse
2
+ import datetime
3
+ import pathlib
4
+ from threading import Thread
5
+
6
+ from algo_engine.apps import LOGGER
7
+ from algo_engine.apps.backtest.doc_server import CandleStick
8
+ from algo_engine.apps.bokeh_server import DocManager, DocServer
9
+
10
+
11
+ class WebApp(object):
12
+ def __init__(self, start_date: datetime.date, end_date: datetime.date, name: str = 'WebApp.Backtest', address: str = '0.0.0.0', port: int = 8080, **kwargs):
13
+ from flask import Flask
14
+ self.start_date = start_date
15
+ self.end_date = end_date
16
+ self.name = name
17
+ self.root_dir = pathlib.Path(__file__).parent
18
+ self.host = address
19
+ self.port = port
20
+
21
+ self.flask = Flask(
22
+ import_name=self.name,
23
+ template_folder=self.root_dir.joinpath('templates'),
24
+ static_folder=self.root_dir.joinpath('static')
25
+ )
26
+ self.doc_manager = DocManager(host='localhost', port=port)
27
+ self.dashboard: dict[str, dict[str, DocServer]] = {}
28
+
29
+ def update(self, **kwargs):
30
+ for doc_server in self.doc_manager.doc_server.values():
31
+ doc_server.update(**kwargs)
32
+
33
+ def register(self, ticker: str, **kwargs):
34
+ if ticker in self.dashboard:
35
+ raise ValueError(f'Ticker {ticker} already registered.')
36
+
37
+ dashboard = self.dashboard[ticker] = {}
38
+ candlestick = dashboard[f'candlesticks'] = CandleStick(ticker=ticker, start_date=self.start_date, end_date=self.end_date, **kwargs)
39
+
40
+ self.doc_manager.register(url=f'/candlesticks/{ticker}', doc_server=candlestick)
41
+
42
+ def render_index(self):
43
+ from flask import render_template
44
+
45
+ dashboard_url = {ticker: f'{self.url}/{ticker}' for ticker in self.dashboard}
46
+
47
+ html = render_template(
48
+ 'index.html',
49
+ title=f'PyAlgoEngine.Backtest.App',
50
+ data=dashboard_url
51
+ )
52
+
53
+ return html
54
+
55
+ def render_dashboard(self, ticker: str):
56
+ from flask import render_template
57
+ from bokeh.embed import server_document
58
+
59
+ dashboard = self.dashboard[ticker]
60
+ bokeh_scripts = {}
61
+
62
+ for name, doc_server in dashboard.items():
63
+ url = self.doc_manager.doc_url[doc_server]
64
+ doc_script = server_document(url=f'http://{self.doc_manager.bokeh_host}:{self.doc_manager.bokeh_port}{url}')
65
+ bokeh_scripts[name] = doc_script
66
+
67
+ html = render_template(
68
+ 'dash.html',
69
+ ticker=ticker,
70
+ framework="flask",
71
+ **bokeh_scripts
72
+ )
73
+ return html
74
+
75
+ def serve(self, blocking: bool = True):
76
+ from waitress import serve
77
+
78
+ LOGGER.info(f'starting {self} service...')
79
+
80
+ self.doc_manager.start()
81
+ self.flask.route(rule='/', methods=["GET"])(self.render_index)
82
+
83
+ for ticker in self.dashboard:
84
+ def renderer():
85
+ return self.render_dashboard(ticker=ticker)
86
+
87
+ self.flask.route(rule=f'/{ticker}', methods=["GET"])(renderer)
88
+
89
+ if blocking:
90
+ return serve(app=self.flask, host=self.host, port=self.port)
91
+
92
+ t = Thread(target=serve, kwargs=dict(app=self.flask, host=self.host, port=self.port))
93
+ t.start()
94
+
95
+ # a monkey patch to resolve flask double logging issues
96
+ for hdl in (logger := self.flask.logger).handlers:
97
+ logger.removeHandler(hdl)
98
+
99
+ for hdl in (logger := LOGGER.root).handlers:
100
+ logger.removeHandler(hdl)
101
+
102
+ @property
103
+ def url(self) -> str:
104
+ if self.host == '0.0.0.0':
105
+ return f'http://localhost:{self.port}/'
106
+ else:
107
+ return f'http://{self.host}:{self.port}/'
108
+
109
+
110
+ def start_app(start_date: datetime.date, end_date: datetime.date, blocking: bool = True, **kwargs):
111
+ web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
112
+ web_app.serve(blocking=blocking)
113
+
114
+
115
+ if __name__ == '__main__':
116
+ parser = argparse.ArgumentParser(description='Start Backtest.App')
117
+ parser.add_argument('--start_date', type=str, required=True, help='Start date in YYYY-MM-DD format')
118
+ parser.add_argument('--end_date', type=str, required=True, help='End date in YYYY-MM-DD format')
119
+
120
+ args = parser.parse_args()
121
+
122
+ start_app(
123
+ start_date=datetime.datetime.strptime(args.start_date, '%Y-%m-%d').date(),
124
+ end_date=datetime.datetime.strptime(args.end_date, '%Y-%m-%d').date(),
125
+ )
@@ -0,0 +1,245 @@
1
+ import abc
2
+ import uuid
3
+ from copy import deepcopy
4
+ from functools import partial
5
+ from threading import Event, Lock, Thread
6
+ from typing import overload
7
+
8
+ from algo_engine.base import MarketData
9
+ from . import LOGGER
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)
148
+ self.stop_event = Event()
149
+ self.bokeh_server = None
150
+
151
+ def __getitem__(self, url: str):
152
+ return self.doc_server.__getitem__(url)
153
+
154
+ def __setitem__(self, url: str, doc_server: DocServer):
155
+ return self.register(url=url, doc_server=doc_server)
156
+
157
+ def __contains__(self, url: str):
158
+ return self.doc_server.__contains__(url)
159
+
160
+ def register(self, url: str, doc_server: DocServer):
161
+ if url in self.doc_server:
162
+ LOGGER.warning(f'{url} already registered! Existed doc_server {self.doc_server[url]} overridden!')
163
+
164
+ self.doc_server[url] = doc_server
165
+ self.doc_url[doc_server] = url
166
+ return doc_server
167
+
168
+ def serve_bokeh(self):
169
+ from bokeh.server.server import Server
170
+ import psutil
171
+ import socket
172
+
173
+ # Get all network interfaces and their IP addresses
174
+ addrs = psutil.net_if_addrs()
175
+ websocket_origin = [
176
+ f"{self.host}:{self.port}"
177
+ ]
178
+
179
+ for interface, addr_info in addrs.items():
180
+ for addr in addr_info:
181
+ if addr.family == socket.AF_INET: # Filter only IPv4 addresses
182
+ LOGGER.info(f"Binding network interface: {interface}, IP Address: {addr.address}")
183
+ websocket_origin.append(f"{addr.address}:{self.port}")
184
+ websocket_origin.append(f"{addr.address}:{self.bokeh_port}")
185
+
186
+ if (_ := f'localhost:{self.port}') not in websocket_origin:
187
+ websocket_origin.append(_)
188
+ if (_ := f'127.0.0.1:{self.port}') not in websocket_origin:
189
+ websocket_origin.append(_)
190
+ if (_ := f'localhost:{self.bokeh_port}') not in websocket_origin:
191
+ websocket_origin.append(_)
192
+ if (_ := f'127.0.0.1:{self.bokeh_port}') not in websocket_origin:
193
+ websocket_origin.append(_)
194
+ if (_ := f'{self.bokeh_host}:{self.port}') not in websocket_origin:
195
+ websocket_origin.append(_)
196
+ if (_ := f'{self.bokeh_host}:{self.bokeh_port}') not in websocket_origin:
197
+ websocket_origin.append(_)
198
+
199
+ self.bokeh_server = Server(
200
+ applications=self.doc_server,
201
+ address='0.0.0.0',
202
+ port=self.bokeh_port,
203
+ check_unused_sessions_milliseconds=(self.bokeh_check_unused_sessions * 1000),
204
+ allow_websocket_origin=websocket_origin,
205
+ use_xheaders=True,
206
+ # num_procs=1
207
+ )
208
+
209
+ LOGGER.info(
210
+ f'bokeh service started at {self.bokeh_host}:{self.bokeh_port}!\n' +
211
+ '\n'.join([f'http://{self.bokeh_host}:{self.bokeh_port}{url} => {app}' for url, app in self.doc_server.items()])
212
+ )
213
+
214
+ self.bokeh_server.start()
215
+
216
+ # Start the Bokeh server IOLoop unless stop_event is triggered
217
+ while not self.stop_event.is_set():
218
+ try:
219
+ self.bokeh_server.io_loop.start()
220
+ except Exception as e:
221
+ LOGGER.error(f"Error in Bokeh server: {e}")
222
+ finally:
223
+ LOGGER.info("Bokeh server has been stopped.")
224
+ break
225
+
226
+ def start(self):
227
+ LOGGER.info(f'Starting Bokeh service...')
228
+ self.bokeh_thread.start()
229
+
230
+ def stop(self):
231
+ LOGGER.info(f'Stopping Bokeh service...')
232
+
233
+ # Signal the event to stop
234
+ self.stop_event.set()
235
+
236
+ # Stop the Bokeh server gracefully
237
+ if self.bokeh_server is not None:
238
+ self.bokeh_server.io_loop.stop() # Stop the IOLoop
239
+ self.bokeh_server.stop() # Stop the Bokeh server itself
240
+
241
+ # Wait for the Bokeh thread to finish if it's still running
242
+ if self.bokeh_thread.is_alive():
243
+ self.bokeh_thread.join()
244
+
245
+ LOGGER.info('Bokeh service has been stopped.')
File without changes
@@ -0,0 +1,40 @@
1
+ import datetime
2
+ import time
3
+
4
+ from algo_engine.apps.backtest import LOGGER, WebApp
5
+ from algo_engine.base import Progress
6
+ from algo_engine.exchange_profile import PROFILE_CN
7
+ from algo_engine.utils import fake_data
8
+
9
+
10
+ def main():
11
+ PROFILE_CN.activate()
12
+
13
+ ticker = '000016.SH'
14
+ market_date = datetime.date.today()
15
+
16
+ data_set = fake_data(market_date=market_date)
17
+ web_app = WebApp(start_date=market_date, end_date=market_date)
18
+ LOGGER.info(f'{len(data_set)} fake data generated for {ticker} {market_date}.')
19
+
20
+ web_app.register(ticker=ticker)
21
+ web_app.serve(blocking=False)
22
+
23
+ LOGGER.info(f'web app started at {web_app.url}')
24
+
25
+ for ts, row in Progress(list(data_set.iterrows())):
26
+ web_app.update(
27
+ timestamp=ts,
28
+ ticker=ticker,
29
+ open_price=row['open_price'],
30
+ close_price=row['close_price'],
31
+ high_price=row['high_price'],
32
+ low_price=row['low_price'],
33
+ )
34
+ time.sleep(0.05)
35
+
36
+
37
+ if __name__ == '__main__':
38
+ main()
39
+
40
+ # sys.exit(-1)
@@ -0,0 +1,23 @@
1
+ import platform
2
+
3
+ from .. import LOGGER
4
+
5
+
6
+ def check_windows_version(min_version=(6, 1)):
7
+ """
8
+ Ensure the script is running on a compatible Windows version.
9
+ :param min_version: Minimum required version as a tuple (major, minor).
10
+ Default is (6, 1) for Windows 7 (major=6, minor=1).
11
+ """
12
+ # Get the Windows version
13
+ if not platform.system() == "Windows":
14
+ raise EnvironmentError("This script only works on Windows.")
15
+
16
+ version_str = platform.version()
17
+ version_tuple = tuple(map(int, platform.win32_ver()[1].split('.')))
18
+
19
+ if version_tuple < min_version:
20
+ raise EnvironmentError(f"Unsupported Windows version: {version_str}. Minimum required is {min_version[0]}.{min_version[1]}.")
21
+
22
+
23
+ LOGGER = LOGGER.getChild('SimInput')