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.
- algo_engine/__infra__.pxd +3 -0
- algo_engine/__init__.pxd +3 -0
- algo_engine/__init__.py +91 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +328 -0
- algo_engine/apps/backtest/static/styles/dash.css +48 -0
- algo_engine/apps/backtest/templates/dash.html +20 -0
- algo_engine/apps/backtest/templates/index.html +40 -0
- algo_engine/apps/backtest/tester.py +252 -0
- algo_engine/apps/backtest/web_app.py +125 -0
- algo_engine/apps/bokeh_server.py +245 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +40 -0
- algo_engine/apps/sim_input/__init__.py +23 -0
- algo_engine/apps/sim_input/client.py +412 -0
- algo_engine/apps/sim_input/sim_keyboard.py +88 -0
- algo_engine/apps/sim_input/sim_mouse.py +137 -0
- algo_engine/apps/sim_input/window.py +162 -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 +635 -0
- algo_engine/backtest/sim_match.py +506 -0
- algo_engine/base/__infra__.pxd +3 -0
- algo_engine/base/__init__.pxd +3 -0
- algo_engine/base/__init__.py +60 -0
- algo_engine/base/c_allocator_protocol.c +10756 -0
- algo_engine/base/c_allocator_protocol.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_allocator_protocol.pxd +24 -0
- algo_engine/base/c_allocator_protocol.pyi +68 -0
- algo_engine/base/c_allocator_protocol.pyx +88 -0
- algo_engine/base/c_intern_string.c +5845 -0
- algo_engine/base/c_intern_string.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_intern_string.pxd +14 -0
- algo_engine/base/c_intern_string.pyi +22 -0
- algo_engine/base/c_intern_string.pyx +17 -0
- algo_engine/base/c_market_data/__infra__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.pxd +196 -0
- algo_engine/base/c_market_data/__init__.py +24 -0
- algo_engine/base/c_market_data/c_candlestick.c +18960 -0
- algo_engine/base/c_market_data/c_candlestick.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
- algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
- algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
- algo_engine/base/c_market_data/c_internal.c +14023 -0
- algo_engine/base/c_market_data/c_internal.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_internal.pxd +14 -0
- algo_engine/base/c_market_data/c_internal.pyi +47 -0
- algo_engine/base/c_market_data/c_internal.pyx +41 -0
- algo_engine/base/c_market_data/c_market_data.c +30739 -0
- algo_engine/base/c_market_data/c_market_data.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/base/c_market_data/c_market_data.pxd +418 -0
- algo_engine/base/c_market_data/c_market_data.pyi +550 -0
- algo_engine/base/c_market_data/c_market_data.pyx +688 -0
- algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
- algo_engine/base/c_market_data/c_market_data_buffer.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
- algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
- algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/base/c_market_data/c_tick.c +44230 -0
- algo_engine/base/c_market_data/c_tick.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_tick.pxd +53 -0
- algo_engine/base/c_market_data/c_tick.pyi +466 -0
- algo_engine/base/c_market_data/c_tick.pyx +673 -0
- algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
- algo_engine/base/c_market_data/c_trade_utils.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
- algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
- algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
- algo_engine/base/c_market_data/c_transaction.c +23521 -0
- algo_engine/base/c_market_data/c_transaction.cp313-win_amd64.pyd +0 -0
- algo_engine/base/c_market_data/c_transaction.pxd +27 -0
- algo_engine/base/c_market_data/c_transaction.pyi +433 -0
- algo_engine/base/c_market_data/c_transaction.pyx +460 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/telemetrics.py +104 -0
- algo_engine/engine/__infra__.pxd +10 -0
- algo_engine/engine/__init__.pxd +10 -0
- algo_engine/engine/__init__.py +40 -0
- algo_engine/engine/algo_engine.py +904 -0
- algo_engine/engine/c_event_engine.c +16289 -0
- algo_engine/engine/c_event_engine.cp313-win_amd64.pyd +0 -0
- algo_engine/engine/c_event_engine.pxd +25 -0
- algo_engine/engine/c_event_engine.pyi +68 -0
- algo_engine/engine/c_market_engine.c +24222 -0
- algo_engine/engine/c_market_engine.cp313-win_amd64.pyd +0 -0
- algo_engine/engine/c_market_engine.pxd +87 -0
- algo_engine/engine/c_market_engine.pyi +357 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/exchange_profile/__infra__.pxd +99 -0
- algo_engine/exchange_profile/__init__.pxd +99 -0
- algo_engine/exchange_profile/__init__.py +36 -0
- algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
- algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
- algo_engine/exchange_profile/c_exchange_profile.cp313-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
- algo_engine/exchange_profile/c_exchange_profile.pyi +867 -0
- algo_engine/exchange_profile/c_exchange_profile.pyx +1482 -0
- algo_engine/exchange_profile/c_profile_cn.c +7779 -0
- algo_engine/exchange_profile/c_profile_cn.cp313-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
- algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
- algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
- algo_engine/exchange_profile/c_profile_default.c +7779 -0
- algo_engine/exchange_profile/c_profile_default.cp313-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_default.pxd +1 -0
- algo_engine/exchange_profile/c_profile_default.pyi +3 -0
- algo_engine/exchange_profile/c_profile_default.pyx +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
- algo_engine/exchange_profile/c_profile_dispatcher.cp313-win_amd64.pyd +0 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
- algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
- algo_engine/includes/algo_engine/base/c_allocator_protocol.c +10756 -0
- algo_engine/includes/algo_engine/base/c_intern_string.c +5845 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18960 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14023 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30739 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44230 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
- algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23521 -0
- algo_engine/includes/algo_engine/engine/c_event_engine.c +16289 -0
- algo_engine/includes/algo_engine/engine/c_market_engine.c +24222 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
- algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
- algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7779 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7779 -0
- algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +334 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +441 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- algo_engine/utils/data_utils.py +296 -0
- pyalgoengine-0.12.0.post3.dist-info/METADATA +142 -0
- pyalgoengine-0.12.0.post3.dist-info/RECORD +156 -0
- pyalgoengine-0.12.0.post3.dist-info/WHEEL +5 -0
- pyalgoengine-0.12.0.post3.dist-info/licenses/LICENSE +21 -0
- 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')
|