PyAlgoEngine 0.4.3__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.
Files changed (47) hide show
  1. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/LICENSE +0 -0
  2. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/PKG-INFO +9 -1
  3. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/PKG-INFO +9 -1
  4. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/SOURCES.txt +13 -1
  5. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  6. pyalgoengine-0.5.2/PyAlgoEngine.egg-info/requires.txt +9 -0
  7. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  8. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/README.md +0 -0
  9. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/__init__.py +41 -39
  10. pyalgoengine-0.5.2/algo_engine/apps/__init__.py +17 -0
  11. pyalgoengine-0.5.2/algo_engine/apps/backtest/__init__.py +20 -0
  12. pyalgoengine-0.5.2/algo_engine/apps/backtest/doc_server.py +327 -0
  13. pyalgoengine-0.5.2/algo_engine/apps/backtest/tester.py +254 -0
  14. pyalgoengine-0.5.2/algo_engine/apps/backtest/web_app.py +127 -0
  15. pyalgoengine-0.5.2/algo_engine/apps/bokeh_server.py +162 -0
  16. pyalgoengine-0.5.2/algo_engine/apps/demo/__init__.py +0 -0
  17. pyalgoengine-0.5.2/algo_engine/apps/demo/test.py +39 -0
  18. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/back_test/__init__.py +19 -19
  19. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/back_test/__main__.py +0 -0
  20. pyalgoengine-0.5.2/algo_engine/back_test/metrics.py +179 -0
  21. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/back_test/replay.py +258 -258
  22. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/back_test/sim_match.py +295 -295
  23. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/__init__.py +28 -28
  24. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/console_utils.py +0 -0
  25. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/finance_decimal.py +0 -0
  26. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/market_utils.py +0 -0
  27. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/technical_analysis.py +0 -0
  28. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/telemetrics.py +78 -78
  29. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/base/trade_utils.py +0 -0
  30. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/engine/__init__.py +0 -0
  31. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/engine/algo_engine.py +0 -0
  32. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/engine/event_engine.py +0 -0
  33. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/engine/market_engine.py +12 -3
  34. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/engine/trade_engine.py +4 -4
  35. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/monitor/__init__.py +15 -15
  36. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/monitor/advanced_data_interface.py +239 -239
  37. pyalgoengine-0.5.2/algo_engine/profile/__init__.py +118 -0
  38. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/profile/cn.py +175 -187
  39. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/strategy/__init__.py +2 -2
  40. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/algo_engine/strategy/strategy_engine.py +40 -8
  41. pyalgoengine-0.5.2/algo_engine/utils/__init__.py +3 -0
  42. pyalgoengine-0.5.2/algo_engine/utils/commit_regularizer.py +49 -0
  43. pyalgoengine-0.5.2/algo_engine/utils/data_utils.py +250 -0
  44. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/setup.cfg +0 -0
  45. {PyAlgoEngine-0.4.3 → pyalgoengine-0.5.2}/setup.py +9 -2
  46. PyAlgoEngine-0.4.3/PyAlgoEngine.egg-info/requires.txt +0 -4
  47. PyAlgoEngine-0.4.3/algo_engine/profile/__init__.py +0 -72
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.4.3
3
+ Version: 0.5.2
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -14,6 +14,14 @@ Classifier: Operating System :: OS Independent
14
14
  Requires-Python: >=3.1
15
15
  Description-Content-Type: text/markdown
16
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"
17
25
 
18
26
  # PyAlgoEngine
19
27
  python algo trading engine
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.4.3
3
+ Version: 0.5.2
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -14,6 +14,14 @@ Classifier: Operating System :: OS Independent
14
14
  Requires-Python: >=3.1
15
15
  Description-Content-Type: text/markdown
16
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"
17
25
 
18
26
  # PyAlgoEngine
19
27
  python algo trading engine
@@ -7,8 +7,17 @@ PyAlgoEngine.egg-info/dependency_links.txt
7
7
  PyAlgoEngine.egg-info/requires.txt
8
8
  PyAlgoEngine.egg-info/top_level.txt
9
9
  algo_engine/__init__.py
10
+ algo_engine/apps/__init__.py
11
+ algo_engine/apps/bokeh_server.py
12
+ algo_engine/apps/backtest/__init__.py
13
+ algo_engine/apps/backtest/doc_server.py
14
+ algo_engine/apps/backtest/tester.py
15
+ algo_engine/apps/backtest/web_app.py
16
+ algo_engine/apps/demo/__init__.py
17
+ algo_engine/apps/demo/test.py
10
18
  algo_engine/back_test/__init__.py
11
19
  algo_engine/back_test/__main__.py
20
+ algo_engine/back_test/metrics.py
12
21
  algo_engine/back_test/replay.py
13
22
  algo_engine/back_test/sim_match.py
14
23
  algo_engine/base/__init__.py
@@ -28,4 +37,7 @@ algo_engine/monitor/advanced_data_interface.py
28
37
  algo_engine/profile/__init__.py
29
38
  algo_engine/profile/cn.py
30
39
  algo_engine/strategy/__init__.py
31
- algo_engine/strategy/strategy_engine.py
40
+ algo_engine/strategy/strategy_engine.py
41
+ algo_engine/utils/__init__.py
42
+ algo_engine/utils/commit_regularizer.py
43
+ algo_engine/utils/data_utils.py
@@ -0,0 +1,9 @@
1
+ numpy
2
+ pandas
3
+ exchange_calendars
4
+ PyEventEngine
5
+
6
+ [WebApps]
7
+ flask
8
+ waitress
9
+ bokeh
File without changes
@@ -1,39 +1,41 @@
1
- __version__ = "0.4.3"
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
- base.set_logger(logger=logger)
18
- engine.set_logger(logger=logger.getChild('Engine'))
19
- back_test.set_logger(logger=logger.getChild('BackTest'))
20
- strategy.set_logger(logger=logger.getChild('Strategy'))
21
-
22
-
23
- from . import base
24
- from . import engine
25
- from . import back_test
26
- from . import strategy
27
-
28
- engine.LOGGER.info(f'AlgoEngine version {__version__}')
29
-
30
- # import addon module
31
- try:
32
- from . import algo_addon
33
-
34
- engine.LOGGER.info(f'PyAlgoEngineAddons import successful, version {algo_addon.__version__}')
35
- except ImportError:
36
- algo_addon = None
37
- engine.LOGGER.debug(f'Install PyAlgoEngineAddons to use additional trading algos module\n{traceback.format_exc()}')
38
-
39
- __all__ = ['LOGGER', 'base', 'engine', 'back_test', 'strategy', 'algo_addon']
1
+ __version__ = "0.5.2"
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
+ base.set_logger(logger=logger)
18
+ engine.set_logger(logger=logger.getChild('Engine'))
19
+ back_test.set_logger(logger=logger.getChild('BackTest'))
20
+ strategy.set_logger(logger=logger.getChild('Strategy'))
21
+ apps.set_logger(logger=logger.getChild('Apps'))
22
+
23
+
24
+ from . import base
25
+ from . import engine
26
+ from . import back_test
27
+ from . import strategy
28
+ from . import apps
29
+
30
+ engine.LOGGER.info(f'AlgoEngine version {__version__}')
31
+
32
+ # import addon module
33
+ try:
34
+ from . import algo_addon
35
+
36
+ engine.LOGGER.info(f'PyAlgoEngineAddons import successful, version {algo_addon.__version__}')
37
+ except ImportError:
38
+ algo_addon = None
39
+ engine.LOGGER.debug(f'Install PyAlgoEngineAddons to use additional trading algos module\n{traceback.format_exc()}')
40
+
41
+ __all__ = ['LOGGER', 'base', 'engine', 'back_test', '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,327 @@
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.update(
75
+ index=[],
76
+ market_time=[],
77
+ open_price=[],
78
+ high_price=[],
79
+ 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_queue[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['high_price'].append(self.active_bar_data['high_price'])
213
+ sequence['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_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", "@high_price"),
246
+ ("low_price", "@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='low_price',
265
+ y1='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)