PyAlgoEngine 0.5.0a0__tar.gz → 0.5.3__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.
Files changed (47) hide show
  1. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PKG-INFO +1 -1
  2. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PyAlgoEngine.egg-info/SOURCES.txt +6 -5
  4. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/__init__.py +3 -3
  5. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/__init__.py +2 -1
  6. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/backtest/__init__.py +2 -1
  7. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/backtest/doc_server.py +35 -27
  8. PyAlgoEngine-0.5.3/algo_engine/apps/backtest/tester.py +254 -0
  9. PyAlgoEngine-0.5.3/algo_engine/apps/backtest/web_app.py +127 -0
  10. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/bokeh_server.py +37 -6
  11. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/demo/test.py +1 -0
  12. {PyAlgoEngine-0.5.0a0/algo_engine/back_test → PyAlgoEngine-0.5.3/algo_engine/backtest}/__main__.py +2 -2
  13. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/engine/market_engine.py +12 -3
  14. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/engine/trade_engine.py +4 -4
  15. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/strategy/__init__.py +2 -2
  16. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/strategy/strategy_engine.py +42 -10
  17. PyAlgoEngine-0.5.3/algo_engine/utils/commit_regularizer.py +49 -0
  18. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/tester.py +0 -162
  19. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/web_app.py +0 -84
  20. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/LICENSE +0 -0
  21. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  22. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PyAlgoEngine.egg-info/requires.txt +0 -0
  23. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  24. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/README.md +0 -0
  25. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/apps/demo/__init__.py +0 -0
  26. {PyAlgoEngine-0.5.0a0/algo_engine/back_test → PyAlgoEngine-0.5.3/algo_engine/backtest}/__init__.py +0 -0
  27. {PyAlgoEngine-0.5.0a0/algo_engine/apps → PyAlgoEngine-0.5.3/algo_engine}/backtest/metrics.py +0 -0
  28. {PyAlgoEngine-0.5.0a0/algo_engine/back_test → PyAlgoEngine-0.5.3/algo_engine/backtest}/replay.py +0 -0
  29. {PyAlgoEngine-0.5.0a0/algo_engine/back_test → PyAlgoEngine-0.5.3/algo_engine/backtest}/sim_match.py +0 -0
  30. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/__init__.py +0 -0
  31. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/console_utils.py +0 -0
  32. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/finance_decimal.py +0 -0
  33. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/market_utils.py +0 -0
  34. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/technical_analysis.py +0 -0
  35. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/telemetrics.py +0 -0
  36. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/base/trade_utils.py +0 -0
  37. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/engine/__init__.py +0 -0
  38. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/engine/algo_engine.py +0 -0
  39. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/engine/event_engine.py +0 -0
  40. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/monitor/__init__.py +0 -0
  41. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/monitor/advanced_data_interface.py +0 -0
  42. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/profile/__init__.py +0 -0
  43. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/profile/cn.py +0 -0
  44. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/utils/__init__.py +0 -0
  45. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/algo_engine/utils/data_utils.py +0 -0
  46. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/setup.cfg +0 -0
  47. {PyAlgoEngine-0.5.0a0 → PyAlgoEngine-0.5.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.5.0a0
3
+ Version: 0.5.3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.5.0a0
3
+ Version: 0.5.3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -11,15 +11,15 @@ 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
- algo_engine/back_test/__init__.py
20
- algo_engine/back_test/__main__.py
21
- algo_engine/back_test/replay.py
22
- algo_engine/back_test/sim_match.py
18
+ algo_engine/backtest/__init__.py
19
+ algo_engine/backtest/__main__.py
20
+ algo_engine/backtest/metrics.py
21
+ algo_engine/backtest/replay.py
22
+ algo_engine/backtest/sim_match.py
23
23
  algo_engine/base/__init__.py
24
24
  algo_engine/base/console_utils.py
25
25
  algo_engine/base/finance_decimal.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
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.0a"
1
+ __version__ = "0.5.3"
2
2
 
3
3
  import logging
4
4
  import os
@@ -16,14 +16,14 @@ else:
16
16
  def set_logger(logger: logging.Logger):
17
17
  base.set_logger(logger=logger)
18
18
  engine.set_logger(logger=logger.getChild('Engine'))
19
- back_test.set_logger(logger=logger.getChild('BackTest'))
19
+ backtest.set_logger(logger=logger.getChild('BackTest'))
20
20
  strategy.set_logger(logger=logger.getChild('Strategy'))
21
21
  apps.set_logger(logger=logger.getChild('Apps'))
22
22
 
23
23
 
24
24
  from . import base
25
25
  from . import engine
26
- from . import back_test
26
+ from . import backtest
27
27
  from . import strategy
28
28
  from . import apps
29
29
 
@@ -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 backtest
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 threading import Lock
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="width", syncable=False),
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(children=[[plot], [range_selector]], sizing_mode="stretch_both")
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.backtest.metrics import TradeMetrics
9
+ from . import LOGGER
10
+ from .web_app import WebApp
11
+ from ...backtest import SimMatch, ProgressiveReplay
12
+ from ...base import MarketData, TradeReport, TradeInstruction
13
+ from ...profile import Profile, PROFILE
14
+
15
+
16
+ class Tester(object, metaclass=abc.ABCMeta):
17
+ def __init__(
18
+ self,
19
+ start_date: datetime.date,
20
+ end_date: datetime.date,
21
+ dtype: list[str] = None,
22
+ profile: Profile = None,
23
+ **kwargs
24
+ ):
25
+ self.start_date = start_date
26
+ self.end_date = end_date
27
+ self.dtype = ['TickData', 'TradeData'] if dtype is None else dtype
28
+ self.profile = PROFILE if profile is None else profile
29
+
30
+ self.timestamp = 0.
31
+ self.last_price = {}
32
+ self.subscription = set()
33
+ self.web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
34
+ self.metrics: dict[str, TradeMetrics] = {}
35
+ self.sim_match: dict[str, SimMatch] = {}
36
+
37
+ def register_ticker(self, ticker: str, **kwargs):
38
+ self.subscription.add(ticker)
39
+
40
+ self.metrics[ticker] = TradeMetrics()
41
+
42
+ self.web_app.register(ticker=ticker, **kwargs)
43
+
44
+ sim_match = self.sim_match[ticker] = SimMatch(
45
+ ticker=ticker,
46
+ instant_fill=kwargs.get('instant_fill', True)
47
+ )
48
+
49
+ # to add callback function to sim_match, use following codes.
50
+ # sim_match.on_order = self.on_order
51
+ # sim_match.on_report = self.on_report
52
+
53
+ def unregister_ticker(self, ticker: str, **kwargs):
54
+ self.subscription.remove(ticker)
55
+
56
+ self.metrics.pop(ticker)
57
+
58
+ # the web app does not provide an unregister method, however, this is not a requirement
59
+
60
+ sim_match = self.sim_match.pop(ticker)
61
+ sim_match.unregister()
62
+
63
+ def _launch_order(self, ticker: str, volume: float, limit_price: float):
64
+ order = TradeInstruction(ticker=ticker, side=np.sign(volume), volume=abs(float), timestamp=self.timestamp)
65
+ self.sim_match[ticker].launch_order(order=order)
66
+
67
+ def buy(self, ticker: str, volume: float = None, limit_price: float = None):
68
+ if volume is None:
69
+ trade_metrics = self.metrics[ticker]
70
+ exposure = trade_metrics.exposure
71
+ volume = -exposure if exposure < 0 else 1
72
+
73
+ if limit_price is None:
74
+ limit_price = self.last_price[ticker]
75
+
76
+ self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
77
+
78
+ def sell(self, ticker: str, volume: float = None, limit_price: float = None):
79
+ if volume is None:
80
+ trade_metrics = self.metrics[ticker]
81
+ exposure = trade_metrics.exposure
82
+ volume = -exposure if exposure > 0 else -1
83
+
84
+ if limit_price is None:
85
+ limit_price = self.last_price[ticker]
86
+
87
+ self._launch_order(ticker=ticker, volume=volume, limit_price=limit_price)
88
+
89
+ @abc.abstractmethod
90
+ def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
91
+ ...
92
+
93
+ @abc.abstractmethod
94
+ def on_market_data(self, market_data: MarketData, **kwargs):
95
+ ...
96
+
97
+ @abc.abstractmethod
98
+ def on_report(self, report: TradeReport, **kwargs):
99
+ ...
100
+
101
+ @abc.abstractmethod
102
+ def on_order(self, order: TradeInstruction, **kwargs):
103
+ ...
104
+
105
+ def bod(self, market_date: datetime.date, **kwargs):
106
+ pass
107
+
108
+ def eod(self, market_date: datetime.date, **kwargs):
109
+ pass
110
+
111
+ def run(self, **kwargs):
112
+ replay = ProgressiveReplay(
113
+ loader=self.load_data,
114
+ tickers=list(self.subscription),
115
+ dtype=['TickData', 'TradeData'],
116
+ start_date=self.start_date,
117
+ end_date=self.end_date,
118
+ bod=self.bod,
119
+ eod=self.eod,
120
+ tick_size=kwargs.get('progress_tick_size', 0.001),
121
+ )
122
+
123
+ _start_ts = time.time()
124
+
125
+ for market_data in replay:
126
+ self.on_market_data(market_data=market_data)
127
+ self.sim_match[market_data.ticker](market_data=market_data)
128
+ self.web_app.update(market_data=market_data)
129
+
130
+ self.timestamp = market_data.timestamp
131
+ self.last_price[market_data.ticker] = market_data.market_price
132
+
133
+ LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
134
+
135
+
136
+ class StrategyTester(Tester):
137
+ from ...strategy.strategy_engine import StrategyEngine
138
+
139
+ def __init__(self, start_date: datetime.date, end_date: datetime.date, data_loader, strategy: StrategyEngine, **kwargs):
140
+ self.data_loader = data_loader
141
+ self.strategy = strategy
142
+ self.event_engine = self.strategy.event_engine
143
+ self.topic_set = self.strategy.topic_set
144
+ self.multi_threading = kwargs.get('multi_threading', False)
145
+ self.lock = self.strategy.lock
146
+
147
+ super().__init__(
148
+ start_date=start_date,
149
+ end_date=end_date,
150
+ dtype=kwargs.pop('dtype', ['TickData', 'TradeData']),
151
+ profile=kwargs.pop('profile', PROFILE),
152
+ event_engine=strategy.event_engine,
153
+ topic_set=strategy.topic_set,
154
+ multi_threading=kwargs.pop('multi_threading', False),
155
+ **kwargs
156
+ )
157
+
158
+ def register_ticker(self, ticker: str, **kwargs):
159
+ super().register_ticker(ticker=ticker, **kwargs)
160
+
161
+ for ticker, sim_match in self.sim_match.items():
162
+ sim_match.register(event_engine=self.event_engine, topic_set=self.topic_set)
163
+
164
+ def register(self):
165
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.mds.on_market_data)
166
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.strategy.position_tracker.on_market_data)
167
+ self.event_engine.register_handler(topic=self.topic_set.realtime, handler=self.on_market_data)
168
+
169
+ self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.strategy.balance.on_order)
170
+ self.event_engine.register_handler(topic=self.topic_set.on_order, handler=self.on_order)
171
+ self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.strategy.balance.on_report)
172
+ self.event_engine.register_handler(topic=self.topic_set.on_report, handler=self.on_report)
173
+
174
+ def initialize_position_management(self):
175
+ for ticker in self.subscription:
176
+ risk_profile = self.strategy.position_tracker.dma.risk_profile
177
+
178
+ risk_profile.set_rule(ticker=ticker, key='max_trade_long', value=np.inf)
179
+ risk_profile.set_rule(ticker=ticker, key='max_trade_short', value=np.inf)
180
+ risk_profile.set_rule(ticker=ticker, key='max_exposure_long', value=np.inf)
181
+ risk_profile.set_rule(ticker=ticker, key='max_exposure_short', value=np.inf)
182
+
183
+ def load_data(self, ticker: str, market_date: datetime.date, dtype: Literal['TickData', 'TradeData', 'TransactionData', 'OrderBook']) -> list[MarketData]:
184
+ return self.data_loader(ticker=ticker, market_date=market_date, dtype=dtype)
185
+
186
+ def bod(self, market_date: datetime.date, **kwargs):
187
+ super().bod(market_date=market_date, **kwargs)
188
+ self.bod(market_date=market_date, **kwargs)
189
+
190
+ def eod(self, market_date: datetime.date, **kwargs):
191
+ super().bod(market_date=market_date, **kwargs)
192
+ self.bod(market_date=market_date, **kwargs)
193
+
194
+ def on_market_data(self, market_data: MarketData, **kwargs):
195
+ self.strategy.__call__(market_data=market_data, **kwargs)
196
+
197
+ if self.lock.locked():
198
+ self.lock.release()
199
+
200
+ def on_report(self, report: TradeReport, **kwargs):
201
+ self.strategy.on_report(report=report, **kwargs)
202
+
203
+ def on_order(self, order: TradeInstruction, **kwargs):
204
+ self.strategy.on_order(order=order, **kwargs)
205
+
206
+ def _launch_order(self, ticker: str, volume: float, limit_price: float):
207
+ self.strategy.open_pos(ticker=ticker, volume=abs(volume), trade_side=np.sign(volume))
208
+
209
+ def buy(self, ticker: str, volume: float = None, limit_price: float = None):
210
+ if ticker not in self.subscription:
211
+ raise ValueError(f'{ticker} not subscribed for trading!')
212
+
213
+ super().buy(ticker=ticker, volume=volume, limit_price=limit_price)
214
+
215
+ def sell(self, ticker: str, volume: float = None, limit_price: float = None):
216
+ if ticker not in self.subscription:
217
+ raise ValueError(f'{ticker} not subscribed for trading!')
218
+
219
+ super().sell(ticker=ticker, volume=volume, limit_price=limit_price)
220
+
221
+ def run(self, **kwargs):
222
+ if not self.event_engine.active:
223
+ self.event_engine.start()
224
+
225
+ replay = ProgressiveReplay(
226
+ loader=self.load_data,
227
+ tickers=list(self.subscription),
228
+ dtype=['TickData', 'TradeData'],
229
+ start_date=self.start_date,
230
+ end_date=self.end_date,
231
+ bod=self.bod,
232
+ eod=self.eod,
233
+ tick_size=kwargs.get('progress_tick_size', 0.001),
234
+ )
235
+
236
+ _start_ts = time.time()
237
+
238
+ for market_data in replay:
239
+ if self.multi_threading:
240
+ self.lock.acquire()
241
+ self.event_engine.put(topic=self.topic_set.push(market_data=market_data), market_data=market_data)
242
+ else:
243
+ self.strategy.mds.on_market_data(market_data=market_data)
244
+ self.strategy.position_tracker.on_market_data(market_data=market_data)
245
+ self.strategy.on_market_data(market_data=market_data)
246
+
247
+ if market_data.ticker in self.subscription:
248
+ self.sim_match[market_data.ticker](market_data=market_data)
249
+ self.web_app.update(market_data=market_data)
250
+
251
+ self.timestamp = market_data.timestamp
252
+ self.last_price[market_data.ticker] = market_data.market_price
253
+
254
+ LOGGER.info(f'All done! time_cost: {time.time() - _start_ts:,.3}s')
@@ -0,0 +1,127 @@
1
+ import argparse
2
+ import datetime
3
+ import pathlib
4
+ from threading import Thread
5
+
6
+ from .doc_server import CandleStick
7
+ from .. import LOGGER
8
+ from ..bokeh_server import DocManager, DocServer
9
+ from ...profile import Profile, PROFILE
10
+
11
+
12
+ class WebApp(object):
13
+ def __init__(self, start_date: datetime.date, end_date: datetime.date, name: str = 'WebApp.Backtest', address: str = '0.0.0.0', port: int = 8080, profile: Profile = None, **kwargs):
14
+ from flask import Flask
15
+ self.start_date = start_date
16
+ self.end_date = end_date
17
+ self.name = name
18
+ self.root_dir = pathlib.Path(__file__).parent
19
+ self.profile = PROFILE if profile is None else profile
20
+ self.host = address
21
+ self.port = port
22
+
23
+ self.flask = Flask(
24
+ import_name=self.name,
25
+ template_folder=self.root_dir.joinpath('templates'),
26
+ static_folder=self.root_dir.joinpath('static')
27
+ )
28
+ self.doc_manager = DocManager(host='localhost', port=port)
29
+ self.dashboard: dict[str, dict[str, DocServer]] = {}
30
+
31
+ def update(self, **kwargs):
32
+ for doc_server in self.doc_manager.doc_server.values():
33
+ doc_server.update(**kwargs)
34
+
35
+ def register(self, ticker: str, **kwargs):
36
+ if ticker in self.dashboard:
37
+ raise ValueError(f'Ticker {ticker} already registered.')
38
+
39
+ dashboard = self.dashboard[ticker] = {}
40
+ candlestick = dashboard[f'candlesticks'] = CandleStick(ticker=ticker, start_date=self.start_date, end_date=self.end_date, **kwargs)
41
+
42
+ self.doc_manager.register(url=f'/candlesticks/{ticker}', doc_server=candlestick)
43
+
44
+ def render_index(self):
45
+ from flask import render_template
46
+
47
+ dashboard_url = {ticker: f'{self.url}/{ticker}' for ticker in self.dashboard}
48
+
49
+ html = render_template(
50
+ 'index.html',
51
+ title=f'PyAlgoEngine.Backtest.App',
52
+ data=dashboard_url
53
+ )
54
+
55
+ return html
56
+
57
+ def render_dashboard(self, ticker: str):
58
+ from flask import render_template
59
+ from bokeh.embed import server_document
60
+
61
+ dashboard = self.dashboard[ticker]
62
+ bokeh_scripts = {}
63
+
64
+ for name, doc_server in dashboard.items():
65
+ url = self.doc_manager.doc_url[doc_server]
66
+ doc_script = server_document(url=f'http://{self.doc_manager.bokeh_host}:{self.doc_manager.bokeh_port}{url}')
67
+ bokeh_scripts[name] = doc_script
68
+
69
+ html = render_template(
70
+ 'dash.html',
71
+ ticker=ticker,
72
+ framework="flask",
73
+ **bokeh_scripts
74
+ )
75
+ return html
76
+
77
+ def serve(self, blocking: bool = True):
78
+ from waitress import serve
79
+
80
+ LOGGER.info(f'starting {self} service...')
81
+
82
+ self.doc_manager.start()
83
+ self.flask.route(rule='/', methods=["GET"])(self.render_index)
84
+
85
+ for ticker in self.dashboard:
86
+ def renderer():
87
+ return self.render_dashboard(ticker=ticker)
88
+
89
+ self.flask.route(rule=f'/{ticker}', methods=["GET"])(renderer)
90
+
91
+ if blocking:
92
+ return serve(app=self.flask, host=self.host, port=self.port)
93
+
94
+ t = Thread(target=serve, kwargs=dict(app=self.flask, host=self.host, port=self.port))
95
+ t.start()
96
+
97
+ # a monkey patch to resolve flask double logging issues
98
+ for hdl in (logger := self.flask.logger).handlers:
99
+ logger.removeHandler(hdl)
100
+
101
+ for hdl in (logger := LOGGER.root).handlers:
102
+ logger.removeHandler(hdl)
103
+
104
+ @property
105
+ def url(self) -> str:
106
+ if self.host == '0.0.0.0':
107
+ return f'http://localhost:{self.port}/'
108
+ else:
109
+ return f'http://{self.host}:{self.port}/'
110
+
111
+
112
+ def start_app(start_date: datetime.date, end_date: datetime.date, blocking: bool = True, **kwargs):
113
+ web_app = WebApp(start_date=start_date, end_date=end_date, **kwargs)
114
+ web_app.serve(blocking=blocking)
115
+
116
+
117
+ if __name__ == '__main__':
118
+ parser = argparse.ArgumentParser(description='Start Backtest.App')
119
+ parser.add_argument('--start_date', type=str, required=True, help='Start date in YYYY-MM-DD format')
120
+ parser.add_argument('--end_date', type=str, required=True, help='End date in YYYY-MM-DD format')
121
+
122
+ args = parser.parse_args()
123
+
124
+ start_app(
125
+ start_date=datetime.datetime.strptime(args.start_date, '%Y-%m-%d').date(),
126
+ end_date=datetime.datetime.strptime(args.end_date, '%Y-%m-%d').date(),
127
+ )