PyAlgoEngine 0.4.3__tar.gz → 0.5.0a0__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 (46) hide show
  1. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/PKG-INFO +20 -19
  2. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/PyAlgoEngine.egg-info/PKG-INFO +20 -19
  3. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/PyAlgoEngine.egg-info/SOURCES.txt +12 -1
  4. PyAlgoEngine-0.5.0a0/PyAlgoEngine.egg-info/requires.txt +9 -0
  5. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/__init__.py +41 -39
  6. PyAlgoEngine-0.5.0a0/algo_engine/apps/__init__.py +16 -0
  7. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/__init__.py +19 -0
  8. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/doc_server.py +319 -0
  9. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/metrics.py +179 -0
  10. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/tester.py +162 -0
  11. PyAlgoEngine-0.5.0a0/algo_engine/apps/backtest/web_app.py +84 -0
  12. PyAlgoEngine-0.5.0a0/algo_engine/apps/bokeh_server.py +131 -0
  13. PyAlgoEngine-0.5.0a0/algo_engine/apps/demo/__init__.py +0 -0
  14. PyAlgoEngine-0.5.0a0/algo_engine/apps/demo/test.py +38 -0
  15. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/back_test/__init__.py +19 -19
  16. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/back_test/replay.py +258 -258
  17. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/back_test/sim_match.py +295 -295
  18. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/__init__.py +28 -28
  19. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/telemetrics.py +78 -78
  20. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/monitor/__init__.py +15 -15
  21. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/monitor/advanced_data_interface.py +239 -239
  22. PyAlgoEngine-0.5.0a0/algo_engine/profile/__init__.py +118 -0
  23. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/profile/cn.py +175 -187
  24. PyAlgoEngine-0.5.0a0/algo_engine/utils/__init__.py +3 -0
  25. PyAlgoEngine-0.5.0a0/algo_engine/utils/data_utils.py +250 -0
  26. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/setup.cfg +4 -4
  27. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/setup.py +9 -2
  28. PyAlgoEngine-0.4.3/PyAlgoEngine.egg-info/requires.txt +0 -4
  29. PyAlgoEngine-0.4.3/algo_engine/profile/__init__.py +0 -72
  30. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/LICENSE +0 -0
  31. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  32. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  33. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/README.md +0 -0
  34. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/back_test/__main__.py +0 -0
  35. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/console_utils.py +0 -0
  36. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/finance_decimal.py +0 -0
  37. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/market_utils.py +0 -0
  38. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/technical_analysis.py +0 -0
  39. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/base/trade_utils.py +0 -0
  40. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/engine/__init__.py +0 -0
  41. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/engine/algo_engine.py +0 -0
  42. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/engine/event_engine.py +0 -0
  43. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/engine/market_engine.py +0 -0
  44. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/engine/trade_engine.py +0 -0
  45. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/strategy/__init__.py +0 -0
  46. {PyAlgoEngine-0.4.3 → PyAlgoEngine-0.5.0a0}/algo_engine/strategy/strategy_engine.py +0 -0
@@ -1,19 +1,20 @@
1
- Metadata-Version: 2.1
2
- Name: PyAlgoEngine
3
- Version: 0.4.3
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
-
18
- # PyAlgoEngine
19
- python algo trading engine
1
+ Metadata-Version: 2.1
2
+ Name: PyAlgoEngine
3
+ Version: 0.5.0a0
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
+ Provides-Extra: WebApps
17
+ License-File: LICENSE
18
+
19
+ # PyAlgoEngine
20
+ python algo trading engine
@@ -1,19 +1,20 @@
1
- Metadata-Version: 2.1
2
- Name: PyAlgoEngine
3
- Version: 0.4.3
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
-
18
- # PyAlgoEngine
19
- python algo trading engine
1
+ Metadata-Version: 2.1
2
+ Name: PyAlgoEngine
3
+ Version: 0.5.0a0
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
+ Provides-Extra: WebApps
17
+ License-File: LICENSE
18
+
19
+ # PyAlgoEngine
20
+ python algo trading engine
@@ -7,6 +7,15 @@ 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/metrics.py
15
+ algo_engine/apps/backtest/tester.py
16
+ algo_engine/apps/backtest/web_app.py
17
+ algo_engine/apps/demo/__init__.py
18
+ algo_engine/apps/demo/test.py
10
19
  algo_engine/back_test/__init__.py
11
20
  algo_engine/back_test/__main__.py
12
21
  algo_engine/back_test/replay.py
@@ -28,4 +37,6 @@ 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/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
@@ -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.0a"
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,16 @@
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
+ backtest.set_logger(LOGGER.getChild('Backtester'))
13
+
14
+
15
+ from .bokeh_server import DocServer, DocTheme
16
+ from . import backtest
@@ -0,0 +1,19 @@
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
+
10
+
11
+ def set_logger(logger: logging.Logger):
12
+ global LOGGER
13
+ LOGGER = logger
14
+
15
+ doc_server.LOGGER = LOGGER
16
+ web_app.LOGGER = LOGGER
17
+
18
+
19
+ __all__ = ['CandleStick', 'StickTheme', 'WebApp', 'start_app']
@@ -0,0 +1,319 @@
1
+ import datetime
2
+ import pathlib
3
+ from functools import partial
4
+ from threading import Lock
5
+ from typing import overload, TypedDict, NotRequired
6
+
7
+ 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
+
12
+ from .. import DocServer, DocTheme
13
+ from ...base import MarketData, TradeData, TransactionData
14
+ from ...profile import Profile, PROFILE
15
+ from ...utils import ts_indices
16
+
17
+
18
+ class StickTheme(DocTheme):
19
+ stick_padding = 0.1
20
+ range_padding = 0.01
21
+
22
+ ColorStyle = TypedDict('ColorStyle', fields={'up': str, 'down': str})
23
+ ws_style = ColorStyle(up="green", down="red")
24
+ cn_style = ColorStyle(up="red", down="green")
25
+
26
+ def __init__(self, profile: Profile = PROFILE, style: ColorStyle = None):
27
+ self.profile = profile
28
+
29
+ if style is None:
30
+ if profile.profile_id == 'cn':
31
+ self.style = self.cn_style
32
+ else:
33
+ self.style = self.ws_style
34
+ else:
35
+ self.style = style
36
+
37
+ def stick_style(self, pct_change: float | int) -> dict:
38
+ style_dict = dict()
39
+
40
+ if pct_change > 0:
41
+ style_dict['stick_color'] = self.style['up']
42
+ else:
43
+ style_dict['stick_color'] = self.style['down']
44
+
45
+ return style_dict
46
+
47
+
48
+ class CandleStick(DocServer):
49
+ class ActiveBarData(TypedDict):
50
+ idx: int
51
+ ts_start: float
52
+ ts_end: float
53
+ open_price: float
54
+ close_price: float
55
+ high_price: float
56
+ low_price: float
57
+ volume: NotRequired[float]
58
+
59
+ 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):
60
+ self.ticker = ticker
61
+ self.start_date = start_date
62
+ self.end_date = end_date
63
+ self.profile = profile
64
+ self.interval = interval
65
+ self.indices = self.ts_indices() if x_axis is None else x_axis
66
+
67
+ assert self.indices, 'Must assign x_axis to render candlesticks!'
68
+
69
+ super().__init__(
70
+ theme=theme,
71
+ max_size=kwargs.get('max_size'),
72
+ update_interval=kwargs.get('update_interval', 0),
73
+ )
74
+
75
+ self.theme = StickTheme(profile=self.profile) if self.theme is None else self.theme
76
+ self.timestamp: float = 0.
77
+ self.active_bar_data: CandleStick.ActiveBarData | None = None
78
+ self.data.update(
79
+ index=[],
80
+ market_time=[],
81
+ open_price=[],
82
+ high_price=[],
83
+ low_price=[],
84
+ close_price=[],
85
+ volume=[],
86
+ _max_price=[],
87
+ _min_price=[],
88
+ stick_color=[]
89
+ )
90
+
91
+ def ts_indices(self) -> list[float]:
92
+ """generate integer indices
93
+ from start date to end date, with given interval, in seconds
94
+ """
95
+
96
+ calendar = self.profile.trade_calendar(start_date=self.start_date, end_date=self.end_date)
97
+ timestamps = []
98
+ for market_date in calendar:
99
+ _ts_indices = ts_indices(
100
+ market_date=market_date,
101
+ interval=self.interval,
102
+ session_start=self.profile.session_start,
103
+ session_end=self.profile.session_end,
104
+ session_break=self.profile.session_break,
105
+ time_zone=self.profile.time_zone,
106
+ ts_mode='both'
107
+ )
108
+
109
+ timestamps.extend(_ts_indices)
110
+
111
+ return timestamps
112
+
113
+ def loc_indices(self, timestamp: float, start_idx: int = 0) -> tuple[int, float]:
114
+ last_idx = idx = start_idx
115
+
116
+ while idx < len(self.indices):
117
+ ts = self.indices[idx]
118
+
119
+ if ts > timestamp:
120
+ break
121
+
122
+ last_idx = idx
123
+ idx += 1
124
+
125
+ return last_idx, self.indices[last_idx]
126
+
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
+ def update(self, **kwargs):
140
+ self.lock.acquire()
141
+
142
+ if 'market_data' in kwargs:
143
+ market_data: MarketData = kwargs['market_data']
144
+
145
+ if isinstance(market_data, (TradeData, TransactionData)):
146
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.price, volume=market_data.volume)
147
+ else:
148
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.market_price)
149
+ self.timestamp = market_data.timestamp
150
+ else:
151
+ kwargs = kwargs.copy()
152
+ timestamp = kwargs.pop('timestamp', self.timestamp)
153
+ price = kwargs.pop('market_price', kwargs.pop('close_price'))
154
+ volume = kwargs.pop('volume', 0)
155
+
156
+ assert price is not None, f'Must assign a market_price or close_price for {self.__class__} update function!'
157
+
158
+ self._on_obs(timestamp=timestamp, price=price, volume=volume, **kwargs)
159
+ self.timestamp = timestamp
160
+
161
+ self.lock.release()
162
+
163
+ def _on_obs(self, timestamp: float, price: float, volume: float = 0., **kwargs):
164
+ open_price = kwargs.get('open_price', price)
165
+ high_price = kwargs.get('high_price', price)
166
+ low_price = kwargs.get('low_price', price)
167
+
168
+ if self.active_bar_data is None:
169
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=0)
170
+ if timestamp < ts_idx:
171
+ return
172
+
173
+ self.active_bar_data = self.ActiveBarData(
174
+ idx=int_idx,
175
+ ts_start=ts_idx,
176
+ ts_end=ts_idx + self.interval,
177
+ open_price=open_price,
178
+ high_price=high_price,
179
+ low_price=low_price,
180
+ close_price=price,
181
+ volume=volume
182
+ )
183
+ elif timestamp <= self.active_bar_data['ts_end']:
184
+ if 'open_price' in kwargs:
185
+ self.active_bar_data['open_price'] = open_price
186
+
187
+ self.active_bar_data['high_price'] = max(high_price, self.active_bar_data['high_price'])
188
+ self.active_bar_data['low_price'] = min(low_price, self.active_bar_data['low_price'])
189
+ self.active_bar_data['close_price'] = price
190
+
191
+ self.active_bar_data['volume'] += volume
192
+
193
+ if timestamp >= self.active_bar_data['ts_end']:
194
+ self.pipe(sequence=self.data)
195
+
196
+ for doc_id in list(self.bokeh_documents):
197
+ doc = self.bokeh_documents[doc_id]
198
+ new_data = self.bokeh_data_queue[doc_id]
199
+
200
+ self.pipe(sequence=new_data)
201
+
202
+ if not self.update_interval:
203
+ doc.add_next_tick_callback(partial(self.stream, doc_id=doc_id))
204
+
205
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=self.active_bar_data['idx'])
206
+ self.active_bar_data['idx'] = int_idx
207
+ self.active_bar_data['ts_start'] = ts_idx
208
+ self.active_bar_data['ts_end'] = ts_idx + self.interval
209
+ self.active_bar_data['open_price'] = price
210
+ self.active_bar_data['close_price'] = price
211
+ self.active_bar_data['high_price'] = price
212
+ self.active_bar_data['low_price'] = price
213
+ self.active_bar_data['volume'] = volume
214
+
215
+ def pipe(self, sequence: dict[str, list]):
216
+ sequence['index'].append(self.active_bar_data['idx'] + 0.5) # to ensure bar rendered in the center of the interval
217
+ sequence['market_time'].append(datetime.datetime.fromtimestamp(self.active_bar_data['ts_start'], tz=self.profile.time_zone))
218
+ sequence['open_price'].append(self.active_bar_data['open_price'])
219
+ sequence['close_price'].append(self.active_bar_data['close_price'])
220
+ sequence['high_price'].append(self.active_bar_data['high_price'])
221
+ sequence['low_price'].append(self.active_bar_data['low_price'])
222
+ sequence['volume'].append(self.active_bar_data['volume'])
223
+ sequence['_max_price'].append(max(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
224
+ sequence['_min_price'].append(min(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
225
+ sequence['stick_color'].append(self.theme.stick_style(self.active_bar_data['close_price'] - self.active_bar_data['open_price'])['stick_color'])
226
+
227
+ def layout(self, doc_id: int):
228
+ self._register_candlestick(doc_id=doc_id)
229
+
230
+ def _register_candlestick(self, doc_id: int):
231
+ doc = self.bokeh_documents[doc_id]
232
+ source = self.bokeh_source[doc_id]
233
+
234
+ tools = [
235
+ PanTool(dimensions="width", syncable=False),
236
+ WheelPanTool(dimension="width", syncable=False),
237
+ BoxZoomTool(dimensions="width", syncable=False),
238
+ WheelZoomTool(dimensions="width", syncable=False),
239
+ CrosshairTool(dimensions="both", syncable=False),
240
+ HoverTool(mode='vline', syncable=False, formatters={'@market_time': 'datetime'}),
241
+ ExamineTool(),
242
+ ResetTool(),
243
+ SaveTool()
244
+ ]
245
+ tooltips = [
246
+ ("market_time", "@market_time{%H:%M:%S}"),
247
+ ("close_price", "@close_price"),
248
+ ("open_price", "@open_price"),
249
+ ("high_price", "@high_price"),
250
+ ("low_price", "@low_price"),
251
+ ]
252
+
253
+ plot = figure(
254
+ title=f"{self.ticker} Candlestick",
255
+ x_range=Range1d(start=0, end=len(self.indices), bounds='auto'),
256
+ x_axis_type="linear",
257
+ sizing_mode="stretch_both",
258
+ min_height=80,
259
+ tools=tools,
260
+ tooltips=tooltips,
261
+ y_axis_location="right",
262
+ )
263
+
264
+ _shadows = plot.segment(
265
+ name='candlestick.shade',
266
+ x0='index',
267
+ x1='index',
268
+ y0='low_price',
269
+ y1='high_price',
270
+ line_width=1,
271
+ color="black",
272
+ alpha=0.8,
273
+ source=source
274
+ )
275
+
276
+ _candlestick = plot.vbar(
277
+ name='candlestick',
278
+ x='index',
279
+ top='_max_price',
280
+ bottom='_min_price',
281
+ width=1 - self.theme.stick_padding,
282
+ color='stick_color',
283
+ alpha=0.5,
284
+ source=source
285
+ )
286
+
287
+ 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
+ plot.xaxis.ticker.min_interval = 1.
289
+ tools[5].renderers = [_candlestick]
290
+ plot.toolbar.autohide = True
291
+ plot.toolbar.active_drag = tools[0]
292
+ plot.toolbar.active_scroll = tools[3]
293
+
294
+ range_selector = figure(
295
+ y_range=plot.y_range,
296
+ min_height=20,
297
+ tools=[],
298
+ toolbar_location=None,
299
+ sizing_mode="stretch_both"
300
+ )
301
+
302
+ range_tool = RangeTool(x_range=plot.x_range)
303
+ range_tool.overlay.fill_alpha = 0.5
304
+
305
+ range_selector.line('index', 'close_price', source=source)
306
+ range_selector.add_tools(range_tool)
307
+ range_selector.x_range.range_padding = self.theme.range_padding
308
+ range_selector.xaxis.visible = False
309
+ range_selector.xgrid.visible = False
310
+ range_selector.ygrid.visible = False
311
+
312
+ root = gridplot(children=[[plot], [range_selector]], sizing_mode="stretch_both")
313
+ root.rows = ['80%', '20%']
314
+ doc.add_root(root)
315
+
316
+ def to_csv(self, filename: str | pathlib.Path):
317
+ df = pd.DataFrame(self.data).set_index(keys='market_time')
318
+ df = df[['open_price', 'high_price', 'low_price', 'close_price', 'volume']]
319
+ df.to_csv(filename)