PyAlgoEngine 0.5.0a0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/LICENSE +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PKG-INFO +27 -20
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/PKG-INFO +27 -20
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/SOURCES.txt +2 -1
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/requires.txt +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/top_level.txt +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/README.md +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/__init__.py +1 -1
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/__init__.py +2 -1
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/backtest/__init__.py +2 -1
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/backtest/doc_server.py +35 -27
- pyalgoengine-0.5.2/algo_engine/apps/backtest/tester.py +254 -0
- pyalgoengine-0.5.2/algo_engine/apps/backtest/web_app.py +127 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/bokeh_server.py +37 -6
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/demo/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/apps/demo/test.py +1 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/back_test/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/back_test/__main__.py +0 -0
- {PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest → pyalgoengine-0.5.2/algo_engine/back_test}/metrics.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/back_test/replay.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/back_test/sim_match.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/console_utils.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/finance_decimal.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/market_utils.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/technical_analysis.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/telemetrics.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/base/trade_utils.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/engine/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/engine/algo_engine.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/engine/event_engine.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/engine/market_engine.py +12 -3
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/engine/trade_engine.py +4 -4
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/monitor/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/monitor/advanced_data_interface.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/profile/__init__.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/profile/cn.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/strategy/__init__.py +2 -2
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/strategy/strategy_engine.py +40 -8
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/utils/__init__.py +0 -0
- pyalgoengine-0.5.2/algo_engine/utils/commit_regularizer.py +49 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/algo_engine/utils/data_utils.py +0 -0
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/setup.cfg +4 -4
- {PyAlgoEngine-0.5.0a0 → pyalgoengine-0.5.2}/setup.py +0 -0
- PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/tester.py +0 -162
- PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/web_app.py +0 -84
|
File without changes
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: PyAlgoEngine
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: Basic algo engine
|
|
5
|
-
Home-page: https://github.com/BolunHan/PyAlgoEngine
|
|
6
|
-
Author: Bolun.Han
|
|
7
|
-
Author-email: Bolun.Han@outlook.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Requires-Python: >=3.1
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PyAlgoEngine
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Basic algo engine
|
|
5
|
+
Home-page: https://github.com/BolunHan/PyAlgoEngine
|
|
6
|
+
Author: Bolun.Han
|
|
7
|
+
Author-email: Bolun.Han@outlook.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.1
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: exchange_calendars
|
|
20
|
+
Requires-Dist: PyEventEngine
|
|
21
|
+
Provides-Extra: webapps
|
|
22
|
+
Requires-Dist: flask; extra == "webapps"
|
|
23
|
+
Requires-Dist: waitress; extra == "webapps"
|
|
24
|
+
Requires-Dist: bokeh; extra == "webapps"
|
|
25
|
+
|
|
26
|
+
# PyAlgoEngine
|
|
27
|
+
python algo trading engine
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: PyAlgoEngine
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: Basic algo engine
|
|
5
|
-
Home-page: https://github.com/BolunHan/PyAlgoEngine
|
|
6
|
-
Author: Bolun.Han
|
|
7
|
-
Author-email: Bolun.Han@outlook.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Requires-Python: >=3.1
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: PyAlgoEngine
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: Basic algo engine
|
|
5
|
+
Home-page: https://github.com/BolunHan/PyAlgoEngine
|
|
6
|
+
Author: Bolun.Han
|
|
7
|
+
Author-email: Bolun.Han@outlook.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.1
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: exchange_calendars
|
|
20
|
+
Requires-Dist: PyEventEngine
|
|
21
|
+
Provides-Extra: webapps
|
|
22
|
+
Requires-Dist: flask; extra == "webapps"
|
|
23
|
+
Requires-Dist: waitress; extra == "webapps"
|
|
24
|
+
Requires-Dist: bokeh; extra == "webapps"
|
|
25
|
+
|
|
26
|
+
# PyAlgoEngine
|
|
27
|
+
python algo trading engine
|
|
@@ -11,13 +11,13 @@ algo_engine/apps/__init__.py
|
|
|
11
11
|
algo_engine/apps/bokeh_server.py
|
|
12
12
|
algo_engine/apps/backtest/__init__.py
|
|
13
13
|
algo_engine/apps/backtest/doc_server.py
|
|
14
|
-
algo_engine/apps/backtest/metrics.py
|
|
15
14
|
algo_engine/apps/backtest/tester.py
|
|
16
15
|
algo_engine/apps/backtest/web_app.py
|
|
17
16
|
algo_engine/apps/demo/__init__.py
|
|
18
17
|
algo_engine/apps/demo/test.py
|
|
19
18
|
algo_engine/back_test/__init__.py
|
|
20
19
|
algo_engine/back_test/__main__.py
|
|
20
|
+
algo_engine/back_test/metrics.py
|
|
21
21
|
algo_engine/back_test/replay.py
|
|
22
22
|
algo_engine/back_test/sim_match.py
|
|
23
23
|
algo_engine/base/__init__.py
|
|
@@ -39,4 +39,5 @@ algo_engine/profile/cn.py
|
|
|
39
39
|
algo_engine/strategy/__init__.py
|
|
40
40
|
algo_engine/strategy/strategy_engine.py
|
|
41
41
|
algo_engine/utils/__init__.py
|
|
42
|
+
algo_engine/utils/commit_regularizer.py
|
|
42
43
|
algo_engine/utils/data_utils.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -9,8 +9,9 @@ def set_logger(logger: logging.Logger):
|
|
|
9
9
|
global LOGGER
|
|
10
10
|
LOGGER = logger
|
|
11
11
|
|
|
12
|
+
from . import backtest
|
|
12
13
|
backtest.set_logger(LOGGER.getChild('Backtester'))
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
from .bokeh_server import DocServer, DocTheme
|
|
16
|
-
from . import
|
|
17
|
+
from .backtest.tester import Tester, StrategyTester
|
|
@@ -6,6 +6,7 @@ LOGGER = LOGGER.getChild('Backtester')
|
|
|
6
6
|
|
|
7
7
|
from .doc_server import CandleStick, StickTheme
|
|
8
8
|
from .web_app import WebApp, start_app
|
|
9
|
+
from .tester import Tester
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def set_logger(logger: logging.Logger):
|
|
@@ -16,4 +17,4 @@ def set_logger(logger: logging.Logger):
|
|
|
16
17
|
web_app.LOGGER = LOGGER
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
__all__ = ['CandleStick', 'StickTheme', 'WebApp', 'start_app']
|
|
20
|
+
__all__ = ['CandleStick', 'StickTheme', 'WebApp', 'start_app', 'Tester']
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import pathlib
|
|
3
3
|
from functools import partial
|
|
4
|
-
from
|
|
5
|
-
from typing import overload, TypedDict, NotRequired
|
|
4
|
+
from typing import TypedDict, NotRequired
|
|
6
5
|
|
|
7
6
|
import pandas as pd
|
|
8
|
-
from bokeh.models import PanTool, WheelPanTool, WheelZoomTool, BoxZoomTool, ResetTool, ExamineTool, SaveTool, CrosshairTool, HoverTool, Toolbar
|
|
9
|
-
from bokeh.models import RangeTool, Range1d
|
|
10
|
-
from bokeh.plotting import figure, gridplot
|
|
11
7
|
|
|
12
8
|
from .. import DocServer, DocTheme
|
|
13
9
|
from ...base import MarketData, TradeData, TransactionData
|
|
@@ -124,24 +120,15 @@ class CandleStick(DocServer):
|
|
|
124
120
|
|
|
125
121
|
return last_idx, self.indices[last_idx]
|
|
126
122
|
|
|
127
|
-
@overload
|
|
128
|
-
def update(self, timestamp: float, market_price: float, **kwargs):
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
@overload
|
|
132
|
-
def update(self, timestamp: float, open_price: float, close_price: float, high_price: float, low_price: float, **kwargs):
|
|
133
|
-
...
|
|
134
|
-
|
|
135
|
-
@overload
|
|
136
|
-
def update(self, market_data: MarketData, **kwargs):
|
|
137
|
-
...
|
|
138
|
-
|
|
139
123
|
def update(self, **kwargs):
|
|
140
124
|
self.lock.acquire()
|
|
141
125
|
|
|
142
126
|
if 'market_data' in kwargs:
|
|
143
127
|
market_data: MarketData = kwargs['market_data']
|
|
144
128
|
|
|
129
|
+
if market_data.ticker != self.ticker:
|
|
130
|
+
return
|
|
131
|
+
|
|
145
132
|
if isinstance(market_data, (TradeData, TransactionData)):
|
|
146
133
|
self._on_obs(timestamp=market_data.timestamp, price=market_data.price, volume=market_data.volume)
|
|
147
134
|
else:
|
|
@@ -150,11 +137,16 @@ class CandleStick(DocServer):
|
|
|
150
137
|
else:
|
|
151
138
|
kwargs = kwargs.copy()
|
|
152
139
|
timestamp = kwargs.pop('timestamp', self.timestamp)
|
|
140
|
+
ticker = kwargs.pop('ticker')
|
|
153
141
|
price = kwargs.pop('market_price', kwargs.pop('close_price'))
|
|
154
142
|
volume = kwargs.pop('volume', 0)
|
|
155
143
|
|
|
144
|
+
assert ticker is not None, 'Must assign a ticker for update function!'
|
|
156
145
|
assert price is not None, f'Must assign a market_price or close_price for {self.__class__} update function!'
|
|
157
146
|
|
|
147
|
+
if ticker != self.ticker:
|
|
148
|
+
return
|
|
149
|
+
|
|
158
150
|
self._on_obs(timestamp=timestamp, price=price, volume=volume, **kwargs)
|
|
159
151
|
self.timestamp = timestamp
|
|
160
152
|
|
|
@@ -228,20 +220,24 @@ class CandleStick(DocServer):
|
|
|
228
220
|
self._register_candlestick(doc_id=doc_id)
|
|
229
221
|
|
|
230
222
|
def _register_candlestick(self, doc_id: int):
|
|
223
|
+
from bokeh.models import PanTool, WheelPanTool, WheelZoomTool, BoxZoomTool, ResetTool, ExamineTool, SaveTool, CrosshairTool, HoverTool, RangeTool, Range1d
|
|
224
|
+
from bokeh.plotting import figure, gridplot
|
|
225
|
+
|
|
231
226
|
doc = self.bokeh_documents[doc_id]
|
|
232
227
|
source = self.bokeh_source[doc_id]
|
|
233
228
|
|
|
234
229
|
tools = [
|
|
235
230
|
PanTool(dimensions="width", syncable=False),
|
|
236
231
|
WheelPanTool(dimension="width", syncable=False),
|
|
237
|
-
BoxZoomTool(dimensions="
|
|
232
|
+
BoxZoomTool(dimensions="auto", syncable=False),
|
|
238
233
|
WheelZoomTool(dimensions="width", syncable=False),
|
|
239
234
|
CrosshairTool(dimensions="both", syncable=False),
|
|
240
235
|
HoverTool(mode='vline', syncable=False, formatters={'@market_time': 'datetime'}),
|
|
241
|
-
ExamineTool(),
|
|
242
|
-
ResetTool(),
|
|
243
|
-
SaveTool()
|
|
236
|
+
ExamineTool(syncable=False),
|
|
237
|
+
ResetTool(syncable=False),
|
|
238
|
+
SaveTool(syncable=False)
|
|
244
239
|
]
|
|
240
|
+
|
|
245
241
|
tooltips = [
|
|
246
242
|
("market_time", "@market_time{%H:%M:%S}"),
|
|
247
243
|
("close_price", "@close_price"),
|
|
@@ -254,7 +250,7 @@ class CandleStick(DocServer):
|
|
|
254
250
|
title=f"{self.ticker} Candlestick",
|
|
255
251
|
x_range=Range1d(start=0, end=len(self.indices), bounds='auto'),
|
|
256
252
|
x_axis_type="linear",
|
|
257
|
-
sizing_mode="stretch_both",
|
|
253
|
+
# sizing_mode="stretch_both",
|
|
258
254
|
min_height=80,
|
|
259
255
|
tools=tools,
|
|
260
256
|
tooltips=tooltips,
|
|
@@ -287,16 +283,13 @@ class CandleStick(DocServer):
|
|
|
287
283
|
plot.xaxis.major_label_overrides = {i: datetime.datetime.fromtimestamp(ts, tz=self.profile.time_zone).strftime('%Y-%m-%d %H:%M:%S') for i, ts in enumerate(self.indices)}
|
|
288
284
|
plot.xaxis.ticker.min_interval = 1.
|
|
289
285
|
tools[5].renderers = [_candlestick]
|
|
290
|
-
plot.toolbar.autohide = True
|
|
291
|
-
plot.toolbar.active_drag = tools[0]
|
|
292
|
-
plot.toolbar.active_scroll = tools[3]
|
|
293
286
|
|
|
294
287
|
range_selector = figure(
|
|
295
288
|
y_range=plot.y_range,
|
|
296
289
|
min_height=20,
|
|
297
290
|
tools=[],
|
|
298
291
|
toolbar_location=None,
|
|
299
|
-
sizing_mode="stretch_both"
|
|
292
|
+
# sizing_mode="stretch_both"
|
|
300
293
|
)
|
|
301
294
|
|
|
302
295
|
range_tool = RangeTool(x_range=plot.x_range)
|
|
@@ -309,8 +302,23 @@ class CandleStick(DocServer):
|
|
|
309
302
|
range_selector.xgrid.visible = False
|
|
310
303
|
range_selector.ygrid.visible = False
|
|
311
304
|
|
|
312
|
-
root = gridplot(
|
|
305
|
+
root = gridplot(
|
|
306
|
+
children=[
|
|
307
|
+
[plot],
|
|
308
|
+
[range_selector]
|
|
309
|
+
],
|
|
310
|
+
sizing_mode="stretch_both",
|
|
311
|
+
merge_tools=True,
|
|
312
|
+
toolbar_options={
|
|
313
|
+
'autohide': True,
|
|
314
|
+
'active_drag': tools[0],
|
|
315
|
+
'active_scroll': tools[3]
|
|
316
|
+
},
|
|
317
|
+
)
|
|
313
318
|
root.rows = ['80%', '20%']
|
|
319
|
+
root.width_policy = 'max'
|
|
320
|
+
root.height_policy = 'max'
|
|
321
|
+
|
|
314
322
|
doc.add_root(root)
|
|
315
323
|
|
|
316
324
|
def to_csv(self, filename: str | pathlib.Path):
|
|
@@ -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.back_test.metrics import TradeMetrics
|
|
9
|
+
from . import LOGGER
|
|
10
|
+
from .web_app import WebApp
|
|
11
|
+
from ...back_test 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
|
+
)
|