PyAlgoEngine 0.12.0.post3__cp313-cp313-win_amd64.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 (156) hide show
  1. algo_engine/__infra__.pxd +3 -0
  2. algo_engine/__init__.pxd +3 -0
  3. algo_engine/__init__.py +91 -0
  4. algo_engine/apps/__init__.py +17 -0
  5. algo_engine/apps/backtest/__init__.py +20 -0
  6. algo_engine/apps/backtest/doc_server.py +328 -0
  7. algo_engine/apps/backtest/static/styles/dash.css +48 -0
  8. algo_engine/apps/backtest/templates/dash.html +20 -0
  9. algo_engine/apps/backtest/templates/index.html +40 -0
  10. algo_engine/apps/backtest/tester.py +252 -0
  11. algo_engine/apps/backtest/web_app.py +125 -0
  12. algo_engine/apps/bokeh_server.py +245 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +40 -0
  15. algo_engine/apps/sim_input/__init__.py +23 -0
  16. algo_engine/apps/sim_input/client.py +412 -0
  17. algo_engine/apps/sim_input/sim_keyboard.py +88 -0
  18. algo_engine/apps/sim_input/sim_mouse.py +137 -0
  19. algo_engine/apps/sim_input/window.py +162 -0
  20. algo_engine/backtest/__init__.py +19 -0
  21. algo_engine/backtest/__main__.py +51 -0
  22. algo_engine/backtest/metrics.py +179 -0
  23. algo_engine/backtest/replay.py +635 -0
  24. algo_engine/backtest/sim_match.py +506 -0
  25. algo_engine/base/__infra__.pxd +3 -0
  26. algo_engine/base/__init__.pxd +3 -0
  27. algo_engine/base/__init__.py +60 -0
  28. algo_engine/base/c_allocator_protocol.c +10756 -0
  29. algo_engine/base/c_allocator_protocol.cp313-win_amd64.pyd +0 -0
  30. algo_engine/base/c_allocator_protocol.pxd +24 -0
  31. algo_engine/base/c_allocator_protocol.pyi +68 -0
  32. algo_engine/base/c_allocator_protocol.pyx +88 -0
  33. algo_engine/base/c_intern_string.c +5845 -0
  34. algo_engine/base/c_intern_string.cp313-win_amd64.pyd +0 -0
  35. algo_engine/base/c_intern_string.pxd +14 -0
  36. algo_engine/base/c_intern_string.pyi +22 -0
  37. algo_engine/base/c_intern_string.pyx +17 -0
  38. algo_engine/base/c_market_data/__infra__.pxd +196 -0
  39. algo_engine/base/c_market_data/__init__.pxd +196 -0
  40. algo_engine/base/c_market_data/__init__.py +24 -0
  41. algo_engine/base/c_market_data/c_candlestick.c +18960 -0
  42. algo_engine/base/c_market_data/c_candlestick.cp313-win_amd64.pyd +0 -0
  43. algo_engine/base/c_market_data/c_candlestick.pxd +27 -0
  44. algo_engine/base/c_market_data/c_candlestick.pyi +217 -0
  45. algo_engine/base/c_market_data/c_candlestick.pyx +255 -0
  46. algo_engine/base/c_market_data/c_internal.c +14023 -0
  47. algo_engine/base/c_market_data/c_internal.cp313-win_amd64.pyd +0 -0
  48. algo_engine/base/c_market_data/c_internal.pxd +14 -0
  49. algo_engine/base/c_market_data/c_internal.pyi +47 -0
  50. algo_engine/base/c_market_data/c_internal.pyx +41 -0
  51. algo_engine/base/c_market_data/c_market_data.c +30739 -0
  52. algo_engine/base/c_market_data/c_market_data.cp313-win_amd64.pyd +0 -0
  53. algo_engine/base/c_market_data/c_market_data.h +1464 -0
  54. algo_engine/base/c_market_data/c_market_data.pxd +418 -0
  55. algo_engine/base/c_market_data/c_market_data.pyi +550 -0
  56. algo_engine/base/c_market_data/c_market_data.pyx +688 -0
  57. algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
  58. algo_engine/base/c_market_data/c_market_data_buffer.cp313-win_amd64.pyd +0 -0
  59. algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  60. algo_engine/base/c_market_data/c_market_data_buffer.pxd +143 -0
  61. algo_engine/base/c_market_data/c_market_data_buffer.pyi +355 -0
  62. algo_engine/base/c_market_data/c_market_data_buffer.pyx +571 -0
  63. algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  64. algo_engine/base/c_market_data/c_tick.c +44230 -0
  65. algo_engine/base/c_market_data/c_tick.cp313-win_amd64.pyd +0 -0
  66. algo_engine/base/c_market_data/c_tick.pxd +53 -0
  67. algo_engine/base/c_market_data/c_tick.pyi +466 -0
  68. algo_engine/base/c_market_data/c_tick.pyx +673 -0
  69. algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
  70. algo_engine/base/c_market_data/c_trade_utils.cp313-win_amd64.pyd +0 -0
  71. algo_engine/base/c_market_data/c_trade_utils.pxd +53 -0
  72. algo_engine/base/c_market_data/c_trade_utils.pyi +602 -0
  73. algo_engine/base/c_market_data/c_trade_utils.pyx +609 -0
  74. algo_engine/base/c_market_data/c_transaction.c +23521 -0
  75. algo_engine/base/c_market_data/c_transaction.cp313-win_amd64.pyd +0 -0
  76. algo_engine/base/c_market_data/c_transaction.pxd +27 -0
  77. algo_engine/base/c_market_data/c_transaction.pyi +433 -0
  78. algo_engine/base/c_market_data/c_transaction.pyx +460 -0
  79. algo_engine/base/console_utils.py +1070 -0
  80. algo_engine/base/finance_decimal.py +258 -0
  81. algo_engine/base/telemetrics.py +104 -0
  82. algo_engine/engine/__infra__.pxd +10 -0
  83. algo_engine/engine/__init__.pxd +10 -0
  84. algo_engine/engine/__init__.py +40 -0
  85. algo_engine/engine/algo_engine.py +904 -0
  86. algo_engine/engine/c_event_engine.c +16289 -0
  87. algo_engine/engine/c_event_engine.cp313-win_amd64.pyd +0 -0
  88. algo_engine/engine/c_event_engine.pxd +25 -0
  89. algo_engine/engine/c_event_engine.pyi +68 -0
  90. algo_engine/engine/c_market_engine.c +24222 -0
  91. algo_engine/engine/c_market_engine.cp313-win_amd64.pyd +0 -0
  92. algo_engine/engine/c_market_engine.pxd +87 -0
  93. algo_engine/engine/c_market_engine.pyi +357 -0
  94. algo_engine/engine/event_engine.py +53 -0
  95. algo_engine/engine/trade_engine.py +2037 -0
  96. algo_engine/exchange_profile/__infra__.pxd +99 -0
  97. algo_engine/exchange_profile/__init__.pxd +99 -0
  98. algo_engine/exchange_profile/__init__.py +36 -0
  99. algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  100. algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
  101. algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  102. algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  103. algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
  104. algo_engine/exchange_profile/c_exchange_profile.cp313-win_amd64.pyd +0 -0
  105. algo_engine/exchange_profile/c_exchange_profile.pxd +336 -0
  106. algo_engine/exchange_profile/c_exchange_profile.pyi +867 -0
  107. algo_engine/exchange_profile/c_exchange_profile.pyx +1482 -0
  108. algo_engine/exchange_profile/c_profile_cn.c +7779 -0
  109. algo_engine/exchange_profile/c_profile_cn.cp313-win_amd64.pyd +0 -0
  110. algo_engine/exchange_profile/c_profile_cn.pxd +1 -0
  111. algo_engine/exchange_profile/c_profile_cn.pyi +3 -0
  112. algo_engine/exchange_profile/c_profile_cn.pyx +1 -0
  113. algo_engine/exchange_profile/c_profile_default.c +7779 -0
  114. algo_engine/exchange_profile/c_profile_default.cp313-win_amd64.pyd +0 -0
  115. algo_engine/exchange_profile/c_profile_default.pxd +1 -0
  116. algo_engine/exchange_profile/c_profile_default.pyi +3 -0
  117. algo_engine/exchange_profile/c_profile_default.pyx +1 -0
  118. algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
  119. algo_engine/exchange_profile/c_profile_dispatcher.cp313-win_amd64.pyd +0 -0
  120. algo_engine/exchange_profile/c_profile_dispatcher.pxd +1 -0
  121. algo_engine/exchange_profile/c_profile_dispatcher.pyi +3 -0
  122. algo_engine/exchange_profile/c_profile_dispatcher.pyx +1 -0
  123. algo_engine/includes/algo_engine/base/c_allocator_protocol.c +10756 -0
  124. algo_engine/includes/algo_engine/base/c_intern_string.c +5845 -0
  125. algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18960 -0
  126. algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14023 -0
  127. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30739 -0
  128. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.h +1464 -0
  129. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.c +29370 -0
  130. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_buffer.h +941 -0
  131. algo_engine/includes/algo_engine/base/c_market_data/c_market_data_config.h +29 -0
  132. algo_engine/includes/algo_engine/base/c_market_data/c_tick.c +44230 -0
  133. algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28665 -0
  134. algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23521 -0
  135. algo_engine/includes/algo_engine/engine/c_event_engine.c +16289 -0
  136. algo_engine/includes/algo_engine/engine/c_market_engine.c +24222 -0
  137. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  138. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_base.h +1148 -0
  139. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.c +968 -0
  140. algo_engine/includes/algo_engine/exchange_profile/c_ex_profile_cn.h +39 -0
  141. algo_engine/includes/algo_engine/exchange_profile/c_exchange_profile.c +52061 -0
  142. algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7779 -0
  143. algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7779 -0
  144. algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7779 -0
  145. algo_engine/monitor/__init__.py +15 -0
  146. algo_engine/monitor/advanced_data_interface.py +334 -0
  147. algo_engine/strategy/__init__.py +44 -0
  148. algo_engine/strategy/strategy_engine.py +441 -0
  149. algo_engine/utils/__init__.py +3 -0
  150. algo_engine/utils/commit_regularizer.py +49 -0
  151. algo_engine/utils/data_utils.py +296 -0
  152. pyalgoengine-0.12.0.post3.dist-info/METADATA +142 -0
  153. pyalgoengine-0.12.0.post3.dist-info/RECORD +156 -0
  154. pyalgoengine-0.12.0.post3.dist-info/WHEEL +5 -0
  155. pyalgoengine-0.12.0.post3.dist-info/licenses/LICENSE +21 -0
  156. pyalgoengine-0.12.0.post3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,3 @@
1
+ from .base cimport *
2
+ from .engine cimport *
3
+ from .exchange_profile cimport *
@@ -0,0 +1,3 @@
1
+ from .base cimport *
2
+ from .engine cimport *
3
+ from .exchange_profile cimport *
@@ -0,0 +1,91 @@
1
+ __version__ = "0.12.0.post3"
2
+
3
+ import functools
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import traceback
8
+
9
+ from .base.telemetrics import LOGGER
10
+
11
+ if 'ALGO_DIR' in os.environ:
12
+ WORKING_DIRECTORY = os.path.realpath(os.environ['ALGO_DIR'])
13
+ else:
14
+ WORKING_DIRECTORY = str(os.getcwd())
15
+
16
+ from . import base
17
+ from . import exchange_profile
18
+ from . import engine
19
+ from . import backtest
20
+ from . import strategy
21
+ from . import apps
22
+ from . import monitor
23
+ from . import utils
24
+
25
+
26
+ def set_logger(logger: logging.Logger):
27
+ from . import base
28
+ from . import exchange_profile
29
+ from . import engine
30
+ from . import backtest
31
+ from . import strategy
32
+ from . import apps
33
+
34
+ base.set_logger(logger=logger)
35
+ exchange_profile.set_logger(logger=logger.getChild('ExchangeProfile'))
36
+ engine.set_logger(logger=logger.getChild('Engine'))
37
+ backtest.set_logger(logger=logger.getChild('BackTest'))
38
+ strategy.set_logger(logger=logger.getChild('Strategy'))
39
+ apps.set_logger(logger=logger.getChild('Apps'))
40
+
41
+
42
+ LOGGER.info(f'AlgoEngine version {__version__}')
43
+
44
+ # import addon module
45
+ try:
46
+ from . import algo_addon
47
+
48
+ LOGGER.info(f'PyAlgoEngineAddons import successful, version {algo_addon.__version__}')
49
+ except ImportError:
50
+ algo_addon = None
51
+ LOGGER.debug(f'Install PyAlgoEngineAddons to use additional trading algos module\n{traceback.format_exc()}')
52
+
53
+
54
+ @functools.cache
55
+ def get_include() -> list[str]:
56
+ import os
57
+ from .base import CONFIG
58
+
59
+ res_dir = pathlib.Path(__file__).parent
60
+ LOGGER.info(f'Building with <PyAlgoEngine> version: "{__version__}", resource directory: "{res_dir}", config: "{CONFIG}".')
61
+
62
+ scr_dir = [
63
+ os.path.realpath(res_dir),
64
+ os.path.realpath(res_dir / 'base'),
65
+ os.path.realpath(res_dir / 'base' / 'c_market_data'),
66
+ os.path.realpath(res_dir / 'exchange_profile'),
67
+ os.path.realpath(res_dir / 'engine'),
68
+ ]
69
+
70
+ # Auto-generated unified include root (mirrors every C header under
71
+ # algo_engine/include/algo_engine/...). Populated by setup.py's
72
+ # collect_headers step; expose it to downstream builds when present.
73
+ include_root = os.path.realpath(res_dir / 'includes')
74
+ if os.path.isdir(include_root):
75
+ scr_dir.append(include_root)
76
+
77
+ import cbase
78
+ dep_dir = cbase.get_include()
79
+
80
+ import event_engine
81
+ dep_dir += event_engine.get_include()
82
+
83
+ return scr_dir + dep_dir
84
+
85
+
86
+ __all__ = [
87
+ 'apps', 'backtest', 'base', 'engine', 'exchange_profile', 'monitor', 'profile', 'strategy', 'utils',
88
+ 'algo_addon',
89
+ 'get_include',
90
+ 'LOGGER'
91
+ ]
@@ -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,328 @@
1
+ import datetime
2
+ import pathlib
3
+ from functools import partial
4
+ from typing import NotRequired, TypedDict
5
+
6
+ import pandas as pd
7
+
8
+ from algo_engine.apps import DocServer, DocTheme
9
+ from algo_engine.base import MarketData, TradeData, TransactionData
10
+ from algo_engine.exchange_profile import PROFILE
11
+ from algo_engine.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', {'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, style: ColorStyle = None):
23
+ if style is None:
24
+ if PROFILE.profile_id in ['CN_STOCK']:
25
+ self.style = self.cn_style
26
+ else:
27
+ self.style = self.ws_style
28
+ else:
29
+ self.style = style
30
+
31
+ def stick_style(self, pct_change: float | int) -> dict:
32
+ style_dict = dict()
33
+
34
+ if pct_change > 0:
35
+ style_dict['stick_color'] = self.style['up']
36
+ else:
37
+ style_dict['stick_color'] = self.style['down']
38
+
39
+ return style_dict
40
+
41
+
42
+ class CandleStick(DocServer):
43
+ class ActiveBarData(TypedDict):
44
+ idx: int
45
+ ts_start: float
46
+ ts_end: float
47
+ open_price: float
48
+ close_price: float
49
+ high_price: float
50
+ low_price: float
51
+ volume: NotRequired[float]
52
+
53
+ def __init__(self, ticker: str, start_date: datetime.date, end_date: datetime.date, interval: float = 60., x_axis: list[float] = None, theme: DocTheme = None, **kwargs):
54
+ self.ticker = ticker
55
+ self.start_date = start_date
56
+ self.end_date = end_date
57
+ self.interval = interval
58
+ self.indices = self.ts_indices() if x_axis is None else x_axis
59
+
60
+ assert self.indices, 'Must assign x_axis to render candlesticks!'
61
+
62
+ super().__init__(
63
+ theme=theme,
64
+ max_size=kwargs.get('max_size'),
65
+ update_interval=kwargs.get('update_interval', 0),
66
+ )
67
+
68
+ self.theme = StickTheme() if self.theme is None else self.theme
69
+ self.timestamp: float = 0.
70
+ self.active_bar_data: CandleStick.ActiveBarData | None = None
71
+ self._data = {
72
+ 'index': [],
73
+ 'market_time': [],
74
+ 'open_price': [],
75
+ 'cs.high_price': [],
76
+ 'cs.low_price': [],
77
+ 'close_price': [],
78
+ 'volume': [],
79
+ '_max_price': [],
80
+ '_min_price': [],
81
+ 'stick_color': []
82
+ }
83
+
84
+ def ts_indices(self) -> list[float]:
85
+ """generate integer indices
86
+ from start date to end date, with given interval, in seconds
87
+ """
88
+
89
+ calendar = PROFILE.trade_calendar(start_date=self.start_date, end_date=self.end_date)
90
+ timestamps = []
91
+ for market_date in calendar:
92
+ _ts_indices = ts_indices(
93
+ market_date=market_date,
94
+ interval=self.interval,
95
+ session_start=PROFILE.session_start.to_pytime(),
96
+ session_end=PROFILE.session_end.to_pytime(),
97
+ session_break=[(session_break.break_start.to_pytime(), session_break.break_end.to_pytime()) for session_break in PROFILE.session_breaks],
98
+ time_zone=PROFILE.time_zone,
99
+ ts_mode='both'
100
+ )
101
+
102
+ timestamps.extend(_ts_indices)
103
+
104
+ return timestamps
105
+
106
+ def loc_indices(self, timestamp: float, start_idx: int = 0) -> tuple[int, float]:
107
+ last_idx = idx = start_idx
108
+
109
+ while idx < len(self.indices):
110
+ ts = self.indices[idx]
111
+
112
+ if ts > timestamp:
113
+ break
114
+
115
+ last_idx = idx
116
+ idx += 1
117
+
118
+ return last_idx, self.indices[last_idx]
119
+
120
+ def update(self, **kwargs):
121
+ self.lock.acquire()
122
+
123
+ if 'market_data' in kwargs:
124
+ market_data: MarketData = kwargs['market_data']
125
+
126
+ if market_data.ticker != self.ticker:
127
+ return
128
+
129
+ if isinstance(market_data, (TradeData, TransactionData)):
130
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.price, volume=market_data.volume)
131
+ else:
132
+ self._on_obs(timestamp=market_data.timestamp, price=market_data.market_price)
133
+ self.timestamp = market_data.timestamp
134
+ else:
135
+ kwargs = kwargs.copy()
136
+ timestamp = kwargs.pop('timestamp', self.timestamp)
137
+ ticker = kwargs.pop('ticker')
138
+ price = kwargs.pop('market_price', kwargs.pop('close_price'))
139
+ volume = kwargs.pop('volume', 0)
140
+
141
+ assert ticker is not None, 'Must assign a ticker for update function!'
142
+ assert price is not None, f'Must assign a market_price or close_price for {self.__class__} update function!'
143
+
144
+ if ticker != self.ticker:
145
+ return
146
+
147
+ self._on_obs(timestamp=timestamp, price=price, volume=volume, **kwargs)
148
+ self.timestamp = timestamp
149
+
150
+ self.lock.release()
151
+
152
+ def _on_obs(self, timestamp: float, price: float, volume: float = 0., **kwargs):
153
+ open_price = kwargs.get('open_price', price)
154
+ high_price = kwargs.get('high_price', price)
155
+ low_price = kwargs.get('low_price', price)
156
+
157
+ if self.active_bar_data is None:
158
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=0)
159
+ if timestamp < ts_idx:
160
+ return
161
+
162
+ self.active_bar_data = self.ActiveBarData(
163
+ idx=int_idx,
164
+ ts_start=ts_idx,
165
+ ts_end=ts_idx + self.interval,
166
+ open_price=open_price,
167
+ high_price=high_price,
168
+ low_price=low_price,
169
+ close_price=price,
170
+ volume=volume
171
+ )
172
+ elif timestamp <= self.active_bar_data['ts_end']:
173
+ if 'open_price' in kwargs:
174
+ self.active_bar_data['open_price'] = open_price
175
+
176
+ self.active_bar_data['high_price'] = max(high_price, self.active_bar_data['high_price'])
177
+ self.active_bar_data['low_price'] = min(low_price, self.active_bar_data['low_price'])
178
+ self.active_bar_data['close_price'] = price
179
+
180
+ self.active_bar_data['volume'] += volume
181
+
182
+ if timestamp >= self.active_bar_data['ts_end']:
183
+ self.pipe(sequence=self.data)
184
+
185
+ for doc_id in list(self.bokeh_documents):
186
+ doc = self.bokeh_documents[doc_id]
187
+ new_data = self.bokeh_data_pipe[doc_id]
188
+
189
+ self.pipe(sequence=new_data)
190
+
191
+ if not self.update_interval:
192
+ doc.add_next_tick_callback(partial(self.stream, doc_id=doc_id))
193
+
194
+ int_idx, ts_idx = self.loc_indices(timestamp=timestamp, start_idx=self.active_bar_data['idx'])
195
+ self.active_bar_data['idx'] = int_idx
196
+ self.active_bar_data['ts_start'] = ts_idx
197
+ self.active_bar_data['ts_end'] = ts_idx + self.interval
198
+ self.active_bar_data['open_price'] = price
199
+ self.active_bar_data['close_price'] = price
200
+ self.active_bar_data['high_price'] = price
201
+ self.active_bar_data['low_price'] = price
202
+ self.active_bar_data['volume'] = volume
203
+
204
+ def pipe(self, sequence: dict[str, list]):
205
+ sequence['index'].append(self.active_bar_data['idx'] + 0.5) # to ensure bar rendered in the center of the interval
206
+ sequence['market_time'].append(PROFILE.timestamp_to_datetime(self.active_bar_data['ts_start']))
207
+ sequence['open_price'].append(self.active_bar_data['open_price'])
208
+ sequence['close_price'].append(self.active_bar_data['close_price'])
209
+ sequence['cs.high_price'].append(self.active_bar_data['high_price'])
210
+ sequence['cs.low_price'].append(self.active_bar_data['low_price'])
211
+ sequence['volume'].append(self.active_bar_data['volume'])
212
+ sequence['_max_price'].append(max(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
213
+ sequence['_min_price'].append(min(self.active_bar_data['open_price'], self.active_bar_data['close_price']))
214
+ sequence['stick_color'].append(self.theme.stick_style(self.active_bar_data['close_price'] - self.active_bar_data['open_price'])['stick_color'])
215
+
216
+ def layout(self, doc_id: int):
217
+ self._register_candlestick(doc_id=doc_id)
218
+
219
+ def _register_candlestick(self, doc_id: int):
220
+ from bokeh.models import PanTool, WheelPanTool, WheelZoomTool, BoxZoomTool, ResetTool, ExamineTool, SaveTool, CrosshairTool, HoverTool, RangeTool, Range1d
221
+ from bokeh.plotting import figure, gridplot
222
+
223
+ doc = self.bokeh_documents[doc_id]
224
+ source = self.bokeh_data_source[doc_id]
225
+
226
+ tools = [
227
+ PanTool(dimensions="width", syncable=False),
228
+ WheelPanTool(dimension="width", syncable=False),
229
+ BoxZoomTool(dimensions="auto", syncable=False),
230
+ WheelZoomTool(dimensions="width", syncable=False),
231
+ CrosshairTool(dimensions="both", syncable=False),
232
+ HoverTool(mode='vline', syncable=False, formatters={'@market_time': 'datetime'}),
233
+ ExamineTool(syncable=False),
234
+ ResetTool(syncable=False),
235
+ SaveTool(syncable=False)
236
+ ]
237
+
238
+ tooltips = [
239
+ ("market_time", "@market_time{%H:%M:%S}"),
240
+ ("close_price", "@close_price"),
241
+ ("open_price", "@open_price"),
242
+ ("high_price", "@{cs.high_price}"),
243
+ ("low_price", "@{cs.low_price}"),
244
+ ]
245
+
246
+ plot = figure(
247
+ title=f"{self.ticker} Candlestick",
248
+ x_range=Range1d(start=0, end=len(self.indices), bounds='auto'),
249
+ x_axis_type="linear",
250
+ # sizing_mode="stretch_both",
251
+ min_height=80,
252
+ tools=tools,
253
+ tooltips=tooltips,
254
+ y_axis_location="right",
255
+ )
256
+
257
+ _shadows = plot.segment(
258
+ name='candlestick.shade',
259
+ x0='index',
260
+ x1='index',
261
+ y0='cs.low_price',
262
+ y1='cs.high_price',
263
+ line_width=1,
264
+ color="black",
265
+ alpha=0.8,
266
+ source=source
267
+ )
268
+
269
+ _candlestick = plot.vbar(
270
+ name='candlestick',
271
+ x='index',
272
+ top='_max_price',
273
+ bottom='_min_price',
274
+ width=1 - self.theme.stick_padding,
275
+ color='stick_color',
276
+ alpha=0.5,
277
+ source=source
278
+ )
279
+
280
+ plot.xaxis.major_label_overrides = {i: PROFILE.timestamp_to_datetime(ts).strftime('%Y-%m-%d %H:%M:%S') for i, ts in enumerate(self.indices)}
281
+ plot.xaxis.ticker.min_interval = 1.
282
+ tools[5].renderers = [_candlestick]
283
+
284
+ range_selector = figure(
285
+ y_range=plot.y_range,
286
+ min_height=20,
287
+ tools=[],
288
+ toolbar_location=None,
289
+ # sizing_mode="stretch_both"
290
+ )
291
+
292
+ range_tool = RangeTool(x_range=plot.x_range)
293
+ range_tool.overlay.fill_alpha = 0.5
294
+
295
+ range_selector.line('index', 'close_price', source=source)
296
+ range_selector.add_tools(range_tool)
297
+ range_selector.x_range.range_padding = self.theme.range_padding
298
+ range_selector.xaxis.visible = False
299
+ range_selector.xgrid.visible = False
300
+ range_selector.ygrid.visible = False
301
+
302
+ root = gridplot(
303
+ children=[
304
+ [plot],
305
+ [range_selector]
306
+ ],
307
+ sizing_mode="stretch_both",
308
+ merge_tools=True,
309
+ toolbar_options={
310
+ 'autohide': True,
311
+ 'active_drag': tools[0],
312
+ 'active_scroll': tools[3]
313
+ },
314
+ )
315
+ root.rows = ['80%', '20%']
316
+ root.width_policy = 'max'
317
+ root.height_policy = 'max'
318
+
319
+ doc.add_root(root)
320
+
321
+ def to_csv(self, filename: str | pathlib.Path):
322
+ df = pd.DataFrame(self.data).set_index(keys='market_time')
323
+ df = df[['open_price', 'high_price', 'low_price', 'close_price', 'volume']]
324
+ df.to_csv(filename)
325
+
326
+ @property
327
+ def data(self) -> dict[str, list]:
328
+ return self._data
@@ -0,0 +1,48 @@
1
+ /* styles.css */
2
+ html, body {
3
+ height: 100%;
4
+ width: 100%;
5
+ margin: 0;
6
+ padding: 0;
7
+ background-color: #121212;
8
+ color: #e0e0e0;
9
+ font-family: Arial, sans-serif;
10
+ }
11
+
12
+ .container {
13
+ display: flex;
14
+ flex-direction: column;
15
+ height: 100%;
16
+ width: 100%;
17
+ }
18
+
19
+ .header {
20
+ flex: 0 1 auto;
21
+ text-align: center;
22
+ padding: 10px;
23
+ background-color: #1f1f1f;
24
+ }
25
+
26
+ .header h1 {
27
+ margin: 0;
28
+ color: #e0e0e0;
29
+ }
30
+
31
+ .content {
32
+ }
33
+
34
+ .content.candlesticks {
35
+ display: flex;
36
+ justify-content: center;
37
+ align-items: center;
38
+ width: 100%;
39
+ height: 100%;
40
+ }
41
+
42
+ .content.candlesticks > div {
43
+ display: flex;
44
+ justify-content: center;
45
+ align-items: center;
46
+ width: 100%;
47
+ height: 100%;
48
+ }
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <title>Dashboard {{ ticker }}</title>
7
+ <link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='styles/dash.css') }}">
8
+ </head>
9
+
10
+ <body>
11
+ <div class="container">
12
+ <div class="header">
13
+ <h1>Backtest App for {{ ticker }}</h1>
14
+ </div>
15
+ <div class="content candlesticks">
16
+ {{ candlesticks|safe }}
17
+ </div>
18
+ </div>
19
+ </body>
20
+ </html>
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title }}</title>
7
+ <style>
8
+ table {
9
+ width: 50%;
10
+ margin: auto;
11
+ border-collapse: collapse;
12
+ }
13
+
14
+ th, td {
15
+ border: 1px solid black;
16
+ padding: 8px;
17
+ text-align: left;
18
+ }
19
+
20
+ th {
21
+ background-color: #f2f2f2;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <h2 style="text-align: center;">Backtest Dashboard Urls</h2>
27
+ <table>
28
+ <tr>
29
+ <th>Ticker</th>
30
+ <th>Dashboard Urls</th>
31
+ </tr>
32
+ {% for key, value in data.items() %}
33
+ <tr>
34
+ <td>{{ key }}</td>
35
+ <td><a href="{{ value }}">{{ value }}</a></td>
36
+ </tr>
37
+ {% endfor %}
38
+ </table>
39
+ </body>
40
+ </html>