PyAlgoEngine 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
  2. PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
  3. PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
  4. PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
  5. PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
  6. algo_engine/__init__.py +41 -0
  7. algo_engine/apps/__init__.py +17 -0
  8. algo_engine/apps/backtest/__init__.py +20 -0
  9. algo_engine/apps/backtest/doc_server.py +331 -0
  10. algo_engine/apps/backtest/tester.py +254 -0
  11. algo_engine/apps/backtest/web_app.py +127 -0
  12. algo_engine/apps/bokeh_server.py +205 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +39 -0
  15. algo_engine/backtest/__init__.py +19 -0
  16. algo_engine/backtest/__main__.py +51 -0
  17. algo_engine/backtest/metrics.py +179 -0
  18. algo_engine/backtest/replay.py +261 -0
  19. algo_engine/backtest/sim_match.py +295 -0
  20. algo_engine/base/__init__.py +40 -0
  21. algo_engine/base/console_utils.py +1070 -0
  22. algo_engine/base/finance_decimal.py +258 -0
  23. algo_engine/base/market_buffer.py +571 -0
  24. algo_engine/base/market_utils.py +3092 -0
  25. algo_engine/base/market_utils_nt.py +188 -0
  26. algo_engine/base/market_utils_posix.py +3004 -0
  27. algo_engine/base/technical_analysis.py +406 -0
  28. algo_engine/base/telemetrics.py +78 -0
  29. algo_engine/base/trade_utils.py +709 -0
  30. algo_engine/engine/__init__.py +28 -0
  31. algo_engine/engine/algo_engine.py +901 -0
  32. algo_engine/engine/event_engine.py +53 -0
  33. algo_engine/engine/market_engine.py +370 -0
  34. algo_engine/engine/trade_engine.py +2037 -0
  35. algo_engine/monitor/__init__.py +15 -0
  36. algo_engine/monitor/advanced_data_interface.py +239 -0
  37. algo_engine/profile/__init__.py +121 -0
  38. algo_engine/profile/cn.py +175 -0
  39. algo_engine/strategy/__init__.py +44 -0
  40. algo_engine/strategy/strategy_engine.py +440 -0
  41. algo_engine/utils/__init__.py +3 -0
  42. algo_engine/utils/commit_regularizer.py +49 -0
  43. algo_engine/utils/data_utils.py +251 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Bolun Han
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyAlgoEngine
3
+ Version: 0.7.4
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
@@ -0,0 +1,43 @@
1
+ algo_engine/__init__.py,sha256=-9L0Zng2Tmu--VA4yR1iOEfQT0A_-M8EHwYP1dIUda4,1131
2
+ algo_engine/apps/__init__.py,sha256=eoGAAMNvOufN3eY2U0U5lNqHpJ91FKS8s-wblk3fH8g,335
3
+ algo_engine/apps/bokeh_server.py,sha256=frh53jx3XkaXcBCYMde0E65P7Ta_SwrDTcyXggan8Ms,6868
4
+ algo_engine/apps/backtest/__init__.py,sha256=JjDJkK3B_fFWSL_Oq0MDxiUtC5F0yhMhIiCtLD8lH8s,408
5
+ algo_engine/apps/backtest/doc_server.py,sha256=jdjIs94_ARbSCU2Sxwvd5OcWPYVQS_Vq7yMieJWCJSU,12219
6
+ algo_engine/apps/backtest/tester.py,sha256=gdYI-8Rp8Ev_AFKRP8vGQGMPt9Ht8rujO1iM-dB9srQ,10059
7
+ algo_engine/apps/backtest/web_app.py,sha256=_dAeHsOZ2LWalxKsCgin8VZPAyswk4OuaaJUcZrwOrA,4440
8
+ algo_engine/apps/demo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ algo_engine/apps/demo/test.py,sha256=3Mhau6Sl5ozTjnqXGbbTxpb_mznV_JPEc5DswTkYU6A,1040
10
+ algo_engine/backtest/__init__.py,sha256=GDWUnLzTVfYkcSDnyhMVBFh3oVyMWg9HlLmbc6GYLUM,419
11
+ algo_engine/backtest/__main__.py,sha256=mx-OUl0n6J6zaGHSznmkoK7FkvWvLSv-oLsFJxwjeRU,1829
12
+ algo_engine/backtest/metrics.py,sha256=7siyOY5iVFZa9Kvvx8CFwWLwFUoydp3jN8orI-r4NQ8,7546
13
+ algo_engine/backtest/replay.py,sha256=MjIP9hgE78hi6EofabmUKb9lClKuj0CpeSB35Ej_-E0,9216
14
+ algo_engine/backtest/sim_match.py,sha256=1GiXWreB5mbCGdVzfvWtSBAfm3FQUS6UtRocjiy1s-A,13973
15
+ algo_engine/base/__init__.py,sha256=PJVmegsOUiTUSGNIngCR0KkgL1R4op2gBpKzBICMjFQ,2229
16
+ algo_engine/base/console_utils.py,sha256=0KUmO8KTnNeE40oXHblNrsQR3dmd5Tw3hoFaPk-1YTw,35811
17
+ algo_engine/base/finance_decimal.py,sha256=hDE9GfPlY1VdmdFPp3ffTINELRBEA8Gpj5XlDmp6WDs,8723
18
+ algo_engine/base/market_buffer.py,sha256=j1BLzBHCBLepg3qlLVNxQDO1247LjqBORIVd4c0qtpY,22155
19
+ algo_engine/base/market_utils.py,sha256=rBw8xpeDqSVhA3DJjegz2w1dP3zA2FaBuN9Abf90HQY,108316
20
+ algo_engine/base/market_utils_nt.py,sha256=3gGr-vK75RJ4BY3Gx09isCEvbKt3qU35ZOwQqEacaEs,5894
21
+ algo_engine/base/market_utils_posix.py,sha256=vXNTw2oydg4EFc3KmrwD9s9TBFhprpZu95-MlFkvdzY,104123
22
+ algo_engine/base/technical_analysis.py,sha256=NbRCdCHqKoKLuyyaUmCE9q0tbUGMY41pJEhnGooaHLI,16147
23
+ algo_engine/base/telemetrics.py,sha256=3NLvrX21WuRjm-6tfdK4Mako6-50OGpK_xmoU-RN9Y8,2617
24
+ algo_engine/base/trade_utils.py,sha256=RK0mAvax2pJWFYWskWeenQBJt9JKD56ImhWpCL0rrIM,22676
25
+ algo_engine/engine/__init__.py,sha256=XtSJZtXzvRrVh_ATRpxtDiIcdYMiz6iW3Rrc19lI-IA,1011
26
+ algo_engine/engine/algo_engine.py,sha256=C-o5mCeOU7VeEekWQucs5btd6fMgDyBbyFAT3WkqlGE,32206
27
+ algo_engine/engine/event_engine.py,sha256=KNHYtoFm76fte7WCIwQ8JXft2aGuMjEuxCy5Yezn6bM,1378
28
+ algo_engine/engine/market_engine.py,sha256=yK4MOyzWEppHMv-j8ag2f9bm10UwefFNw_GDx6y_nZ8,11966
29
+ algo_engine/engine/trade_engine.py,sha256=J8lv1a-HbixeJMku2UmzGkyOpD5Xvap8PCeLu-K5N5M,69557
30
+ algo_engine/monitor/__init__.py,sha256=lNVBjbfUzy4E6mhZJTv9Je1dV0OBx8w91dDysmxJ-rc,434
31
+ algo_engine/monitor/advanced_data_interface.py,sha256=yUZheIEThQVOL5cG2PdMrv_wGK1KZSLfVYAvYUSlpkM,8695
32
+ algo_engine/profile/__init__.py,sha256=_8xJKF0hQU53FqoEZzy2obFREoNiPmtA9wTxbor3rUk,4268
33
+ algo_engine/profile/cn.py,sha256=18CUQtZciN7G4BhIS0urGNbX4I5_TWGK2oCunMLpYHY,7274
34
+ algo_engine/strategy/__init__.py,sha256=GJSGL7zAIfMccVh9a32bvjb9oGe4Cr11jzCSS4jDS3I,1835
35
+ algo_engine/strategy/strategy_engine.py,sha256=V7OzYB-eMK2zR0XMtoEIegYmvGpHPy3_Y4kF7a0YMm0,16973
36
+ algo_engine/utils/__init__.py,sha256=RfLDKS9c0i8gVsPeyrHB4_ICOYImdmEqUxpRzl64M7s,121
37
+ algo_engine/utils/commit_regularizer.py,sha256=zNU2rDCGzh2bmoiktHn8VxYvpo2QUdEenuFU3E0p-qA,1775
38
+ algo_engine/utils/data_utils.py,sha256=2RxjBNBZKpO1knYu8ZF0dnPEVgPe9XHoyfxGf7eUPV8,9286
39
+ PyAlgoEngine-0.7.4.dist-info/LICENSE,sha256=bTmmkRlafTj1JKDkDoGfnLHD8OiuIdXaXDa35W3Ii4g,1066
40
+ PyAlgoEngine-0.7.4.dist-info/METADATA,sha256=hw6eqlG3y9MKLUnkYE5g8UQTUnVTmnDICEXl0xhAgjI,873
41
+ PyAlgoEngine-0.7.4.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
42
+ PyAlgoEngine-0.7.4.dist-info/top_level.txt,sha256=Pef3P5wI02TdvTt4Sn9rK06uwWy9-3oWhnC_TehQAWw,12
43
+ PyAlgoEngine-0.7.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ algo_engine
@@ -0,0 +1,41 @@
1
+ __version__ = "0.7.4"
2
+
3
+ import logging
4
+ import os
5
+ import traceback
6
+
7
+ from . import profile
8
+ from .base.telemetrics import LOGGER
9
+
10
+ if 'ALGO_DIR' in os.environ:
11
+ WORKING_DIRECTORY = os.path.realpath(os.environ['ALGO_DIR'])
12
+ else:
13
+ WORKING_DIRECTORY = str(os.getcwd())
14
+
15
+
16
+ def set_logger(logger: logging.Logger):
17
+ from . import base
18
+ from . import engine
19
+ from . import backtest
20
+ from . import strategy
21
+ from . import apps
22
+
23
+ base.set_logger(logger=logger)
24
+ engine.set_logger(logger=logger.getChild('Engine'))
25
+ backtest.set_logger(logger=logger.getChild('BackTest'))
26
+ strategy.set_logger(logger=logger.getChild('Strategy'))
27
+ apps.set_logger(logger=logger.getChild('Apps'))
28
+
29
+
30
+ LOGGER.info(f'AlgoEngine version {__version__}')
31
+
32
+ # import addon module
33
+ try:
34
+ from . import algo_addon
35
+
36
+ LOGGER.info(f'PyAlgoEngineAddons import successful, version {algo_addon.__version__}')
37
+ except ImportError:
38
+ algo_addon = None
39
+ LOGGER.debug(f'Install PyAlgoEngineAddons to use additional trading algos module\n{traceback.format_exc()}')
40
+
41
+ __all__ = ['LOGGER', 'base', 'engine', 'backtest', 'strategy', 'algo_addon']
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+ from .. import LOGGER
4
+
5
+ LOGGER = LOGGER.getChild('Apps')
6
+
7
+
8
+ def set_logger(logger: logging.Logger):
9
+ global LOGGER
10
+ LOGGER = logger
11
+
12
+ from . import backtest
13
+ backtest.set_logger(LOGGER.getChild('Backtester'))
14
+
15
+
16
+ from .bokeh_server import DocServer, DocTheme
17
+ from .backtest.tester import Tester, StrategyTester
@@ -0,0 +1,20 @@
1
+ import logging
2
+
3
+ from .. import LOGGER
4
+
5
+ LOGGER = LOGGER.getChild('Backtester')
6
+
7
+ from .doc_server import CandleStick, StickTheme
8
+ from .web_app import WebApp, start_app
9
+ from .tester import Tester
10
+
11
+
12
+ def set_logger(logger: logging.Logger):
13
+ global LOGGER
14
+ LOGGER = logger
15
+
16
+ doc_server.LOGGER = LOGGER
17
+ web_app.LOGGER = LOGGER
18
+
19
+
20
+ __all__ = ['CandleStick', 'StickTheme', 'WebApp', 'start_app', 'Tester']
@@ -0,0 +1,331 @@
1
+ import datetime
2
+ import pathlib
3
+ from functools import partial
4
+ from typing import TypedDict, NotRequired
5
+
6
+ import pandas as pd
7
+
8
+ from .. import DocServer, DocTheme
9
+ from ...base import MarketData, TradeData, TransactionData
10
+ from ...profile import Profile, PROFILE
11
+ from ...utils import ts_indices
12
+
13
+
14
+ class StickTheme(DocTheme):
15
+ stick_padding = 0.1
16
+ range_padding = 0.01
17
+
18
+ ColorStyle = TypedDict('ColorStyle', fields={'up': str, 'down': str})
19
+ ws_style = ColorStyle(up="green", down="red")
20
+ cn_style = ColorStyle(up="red", down="green")
21
+
22
+ def __init__(self, profile: Profile = PROFILE, style: ColorStyle = None):
23
+ self.profile = profile
24
+
25
+ if style is None:
26
+ if profile.profile_id == 'cn':
27
+ self.style = self.cn_style
28
+ else:
29
+ self.style = self.ws_style
30
+ else:
31
+ self.style = style
32
+
33
+ def stick_style(self, pct_change: float | int) -> dict:
34
+ style_dict = dict()
35
+
36
+ if pct_change > 0:
37
+ style_dict['stick_color'] = self.style['up']
38
+ else:
39
+ style_dict['stick_color'] = self.style['down']
40
+
41
+ return style_dict
42
+
43
+
44
+ class CandleStick(DocServer):
45
+ class ActiveBarData(TypedDict):
46
+ idx: int
47
+ ts_start: float
48
+ ts_end: float
49
+ open_price: float
50
+ close_price: float
51
+ high_price: float
52
+ low_price: float
53
+ volume: NotRequired[float]
54
+
55
+ def __init__(self, ticker: str, start_date: datetime.date, end_date: datetime.date, profile: Profile = PROFILE, interval: float = 60., x_axis: list[float] = None, theme: DocTheme = None, **kwargs):
56
+ self.ticker = ticker
57
+ self.start_date = start_date
58
+ self.end_date = end_date
59
+ self.profile = profile
60
+ self.interval = interval
61
+ self.indices = self.ts_indices() if x_axis is None else x_axis
62
+
63
+ assert self.indices, 'Must assign x_axis to render candlesticks!'
64
+
65
+ super().__init__(
66
+ theme=theme,
67
+ max_size=kwargs.get('max_size'),
68
+ update_interval=kwargs.get('update_interval', 0),
69
+ )
70
+
71
+ self.theme = StickTheme(profile=self.profile) if self.theme is None else self.theme
72
+ self.timestamp: float = 0.
73
+ self.active_bar_data: CandleStick.ActiveBarData | None = None
74
+ self._data = {
75
+ 'index': [],
76
+ 'market_time': [],
77
+ 'open_price': [],
78
+ 'cs.high_price': [],
79
+ 'cs.low_price': [],
80
+ 'close_price': [],
81
+ 'volume': [],
82
+ '_max_price': [],
83
+ '_min_price': [],
84
+ 'stick_color': []
85
+ }
86
+
87
+ def ts_indices(self) -> list[float]:
88
+ """generate integer indices
89
+ from start date to end date, with given interval, in seconds
90
+ """
91
+
92
+ calendar = self.profile.trade_calendar(start_date=self.start_date, end_date=self.end_date)
93
+ timestamps = []
94
+ for market_date in calendar:
95
+ _ts_indices = ts_indices(
96
+ market_date=market_date,
97
+ interval=self.interval,
98
+ session_start=self.profile.session_start,
99
+ session_end=self.profile.session_end,
100
+ session_break=self.profile.session_break,
101
+ time_zone=self.profile.time_zone,
102
+ ts_mode='both'
103
+ )
104
+
105
+ timestamps.extend(_ts_indices)
106
+
107
+ return timestamps
108
+
109
+ def loc_indices(self, timestamp: float, start_idx: int = 0) -> tuple[int, float]:
110
+ last_idx = idx = start_idx
111
+
112
+ while idx < len(self.indices):
113
+ ts = self.indices[idx]
114
+
115
+ if ts > timestamp:
116
+ break
117
+
118
+ last_idx = idx
119
+ idx += 1
120
+
121
+ return last_idx, self.indices[last_idx]
122
+
123
+ def update(self, **kwargs):
124
+ self.lock.acquire()
125
+
126
+ if 'market_data' in kwargs:
127
+ market_data: MarketData = kwargs['market_data']
128
+
129
+ if market_data.ticker != self.ticker:
130
+ return
131
+
132
+ if isinstance(market_data, (TradeData, TransactionData)):
133
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.price, volume=market_data.volume)
134
+ else:
135
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.market_price)
136
+ self.timestamp = market_data.timestamp
137
+ else:
138
+ kwargs = kwargs.copy()
139
+ timestamp = kwargs.pop('timestamp', self.timestamp)
140
+ ticker = kwargs.pop('ticker')
141
+ price = kwargs.pop('market_price', kwargs.pop('close_price'))
142
+ volume = kwargs.pop('volume', 0)
143
+
144
+ assert ticker is not None, 'Must assign a ticker for update function!'
145
+ assert price is not None, f'Must assign a market_price or close_price for {self.__class__} update function!'
146
+
147
+ if ticker != self.ticker:
148
+ return
149
+
150
+ self._on_obs(timestamp=timestamp, price=price, volume=volume, **kwargs)
151
+ self.timestamp = timestamp
152
+
153
+ self.lock.release()
154
+
155
+ def _on_obs(self, timestamp: float, price: float, volume: float = 0., **kwargs):
156
+ open_price = kwargs.get('open_price', price)
157
+ high_price = kwargs.get('high_price', price)
158
+ low_price = kwargs.get('low_price', price)
159
+
160
+ if self.active_bar_data is None:
161
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=0)
162
+ if timestamp < ts_idx:
163
+ return
164
+
165
+ self.active_bar_data = self.ActiveBarData(
166
+ idx=int_idx,
167
+ ts_start=ts_idx,
168
+ ts_end=ts_idx + self.interval,
169
+ open_price=open_price,
170
+ high_price=high_price,
171
+ low_price=low_price,
172
+ close_price=price,
173
+ volume=volume
174
+ )
175
+ elif timestamp <= self.active_bar_data['ts_end']:
176
+ if 'open_price' in kwargs:
177
+ self.active_bar_data['open_price'] = open_price
178
+
179
+ self.active_bar_data['high_price'] = max(high_price, self.active_bar_data['high_price'])
180
+ self.active_bar_data['low_price'] = min(low_price, self.active_bar_data['low_price'])
181
+ self.active_bar_data['close_price'] = price
182
+
183
+ self.active_bar_data['volume'] += volume
184
+
185
+ if timestamp >= self.active_bar_data['ts_end']:
186
+ self.pipe(sequence=self.data)
187
+
188
+ for doc_id in list(self.bokeh_documents):
189
+ doc = self.bokeh_documents[doc_id]
190
+ new_data = self.bokeh_data_pipe[doc_id]
191
+
192
+ self.pipe(sequence=new_data)
193
+
194
+ if not self.update_interval:
195
+ doc.add_next_tick_callback(partial(self.stream, doc_id=doc_id))
196
+
197
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=self.active_bar_data['idx'])
198
+ self.active_bar_data['idx'] = int_idx
199
+ self.active_bar_data['ts_start'] = ts_idx
200
+ self.active_bar_data['ts_end'] = ts_idx + self.interval
201
+ self.active_bar_data['open_price'] = price
202
+ self.active_bar_data['close_price'] = price
203
+ self.active_bar_data['high_price'] = price
204
+ self.active_bar_data['low_price'] = price
205
+ self.active_bar_data['volume'] = volume
206
+
207
+ def pipe(self, sequence: dict[str, list]):
208
+ sequence['index'].append(self.active_bar_data['idx'] + 0.5) # to ensure bar rendered in the center of the interval
209
+ sequence['market_time'].append(datetime.datetime.fromtimestamp(self.active_bar_data['ts_start'], tz=self.profile.time_zone))
210
+ sequence['open_price'].append(self.active_bar_data['open_price'])
211
+ sequence['close_price'].append(self.active_bar_data['close_price'])
212
+ sequence['cs.high_price'].append(self.active_bar_data['high_price'])
213
+ sequence['cs.low_price'].append(self.active_bar_data['low_price'])
214
+ sequence['volume'].append(self.active_bar_data['volume'])
215
+ sequence['_max_price'].append(max(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
216
+ sequence['_min_price'].append(min(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
217
+ sequence['stick_color'].append(self.theme.stick_style(self.active_bar_data['close_price'] - self.active_bar_data['open_price'])['stick_color'])
218
+
219
+ def layout(self, doc_id: int):
220
+ self._register_candlestick(doc_id=doc_id)
221
+
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
+
226
+ doc = self.bokeh_documents[doc_id]
227
+ source = self.bokeh_data_source[doc_id]
228
+
229
+ tools = [
230
+ PanTool(dimensions="width", syncable=False),
231
+ WheelPanTool(dimension="width", syncable=False),
232
+ BoxZoomTool(dimensions="auto", syncable=False),
233
+ WheelZoomTool(dimensions="width", syncable=False),
234
+ CrosshairTool(dimensions="both", syncable=False),
235
+ HoverTool(mode='vline', syncable=False, formatters={'@market_time': 'datetime'}),
236
+ ExamineTool(syncable=False),
237
+ ResetTool(syncable=False),
238
+ SaveTool(syncable=False)
239
+ ]
240
+
241
+ tooltips = [
242
+ ("market_time", "@market_time{%H:%M:%S}"),
243
+ ("close_price", "@close_price"),
244
+ ("open_price", "@open_price"),
245
+ ("high_price", "@{cs.high_price}"),
246
+ ("low_price", "@{cs.low_price}"),
247
+ ]
248
+
249
+ plot = figure(
250
+ title=f"{self.ticker} Candlestick",
251
+ x_range=Range1d(start=0, end=len(self.indices), bounds='auto'),
252
+ x_axis_type="linear",
253
+ # sizing_mode="stretch_both",
254
+ min_height=80,
255
+ tools=tools,
256
+ tooltips=tooltips,
257
+ y_axis_location="right",
258
+ )
259
+
260
+ _shadows = plot.segment(
261
+ name='candlestick.shade',
262
+ x0='index',
263
+ x1='index',
264
+ y0='cs.low_price',
265
+ y1='cs.high_price',
266
+ line_width=1,
267
+ color="black",
268
+ alpha=0.8,
269
+ source=source
270
+ )
271
+
272
+ _candlestick = plot.vbar(
273
+ name='candlestick',
274
+ x='index',
275
+ top='_max_price',
276
+ bottom='_min_price',
277
+ width=1 - self.theme.stick_padding,
278
+ color='stick_color',
279
+ alpha=0.5,
280
+ source=source
281
+ )
282
+
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)}
284
+ plot.xaxis.ticker.min_interval = 1.
285
+ tools[5].renderers = [_candlestick]
286
+
287
+ range_selector = figure(
288
+ y_range=plot.y_range,
289
+ min_height=20,
290
+ tools=[],
291
+ toolbar_location=None,
292
+ # sizing_mode="stretch_both"
293
+ )
294
+
295
+ range_tool = RangeTool(x_range=plot.x_range)
296
+ range_tool.overlay.fill_alpha = 0.5
297
+
298
+ range_selector.line('index', 'close_price', source=source)
299
+ range_selector.add_tools(range_tool)
300
+ range_selector.x_range.range_padding = self.theme.range_padding
301
+ range_selector.xaxis.visible = False
302
+ range_selector.xgrid.visible = False
303
+ range_selector.ygrid.visible = False
304
+
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
+ )
318
+ root.rows = ['80%', '20%']
319
+ root.width_policy = 'max'
320
+ root.height_policy = 'max'
321
+
322
+ doc.add_root(root)
323
+
324
+ def to_csv(self, filename: str | pathlib.Path):
325
+ df = pd.DataFrame(self.data).set_index(keys='market_time')
326
+ df = df[['open_price', 'high_price', 'low_price', 'close_price', 'volume']]
327
+ df.to_csv(filename)
328
+
329
+ @property
330
+ def data(self) -> dict[str, list]:
331
+ return self._data