PyAlgoEngine 0.12.3__cp315-cp315-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 +107 -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 +11608 -0
  29. algo_engine/base/c_allocator_protocol.cp315-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 +111 -0
  33. algo_engine/base/c_intern_string.c +5908 -0
  34. algo_engine/base/c_intern_string.cp315-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 +18996 -0
  42. algo_engine/base/c_market_data/c_candlestick.cp315-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 +14059 -0
  47. algo_engine/base/c_market_data/c_internal.cp315-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 +30420 -0
  52. algo_engine/base/c_market_data/c_market_data.cp315-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 +414 -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 +701 -0
  57. algo_engine/base/c_market_data/c_market_data_buffer.c +29407 -0
  58. algo_engine/base/c_market_data/c_market_data_buffer.cp315-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 +44303 -0
  65. algo_engine/base/c_market_data/c_tick.cp315-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 +28702 -0
  70. algo_engine/base/c_market_data/c_trade_utils.cp315-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 +23558 -0
  75. algo_engine/base/c_market_data/c_transaction.cp315-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 +18 -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 +16325 -0
  87. algo_engine/engine/c_event_engine.cp315-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 +24258 -0
  91. algo_engine/engine/c_market_engine.cp315-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 +100 -0
  97. algo_engine/exchange_profile/__init__.pxd +100 -0
  98. algo_engine/exchange_profile/__init__.py +53 -0
  99. algo_engine/exchange_profile/c_ex_profile_base.c +87 -0
  100. algo_engine/exchange_profile/c_ex_profile_base.h +1204 -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 +52375 -0
  104. algo_engine/exchange_profile/c_exchange_profile.cp315-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 +883 -0
  107. algo_engine/exchange_profile/c_exchange_profile.pyx +1495 -0
  108. algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  109. algo_engine/exchange_profile/c_profile_cn.cp315-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 +7798 -0
  114. algo_engine/exchange_profile/c_profile_default.cp315-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 +7798 -0
  119. algo_engine/exchange_profile/c_profile_dispatcher.cp315-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 +11608 -0
  124. algo_engine/includes/algo_engine/base/c_intern_string.c +5908 -0
  125. algo_engine/includes/algo_engine/base/c_market_data/c_candlestick.c +18996 -0
  126. algo_engine/includes/algo_engine/base/c_market_data/c_internal.c +14059 -0
  127. algo_engine/includes/algo_engine/base/c_market_data/c_market_data.c +30420 -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 +29407 -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 +44303 -0
  133. algo_engine/includes/algo_engine/base/c_market_data/c_trade_utils.c +28702 -0
  134. algo_engine/includes/algo_engine/base/c_market_data/c_transaction.c +23558 -0
  135. algo_engine/includes/algo_engine/engine/c_event_engine.c +16325 -0
  136. algo_engine/includes/algo_engine/engine/c_market_engine.c +24258 -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 +1204 -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 +52375 -0
  142. algo_engine/includes/algo_engine/exchange_profile/c_profile_cn.c +7798 -0
  143. algo_engine/includes/algo_engine/exchange_profile/c_profile_default.c +7798 -0
  144. algo_engine/includes/algo_engine/exchange_profile/c_profile_dispatcher.c +7798 -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.3.dist-info/METADATA +142 -0
  153. pyalgoengine-0.12.3.dist-info/RECORD +156 -0
  154. pyalgoengine-0.12.3.dist-info/WHEEL +5 -0
  155. pyalgoengine-0.12.3.dist-info/licenses/LICENSE +21 -0
  156. pyalgoengine-0.12.3.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,107 @@
1
+ __version__ = "0.12.3"
2
+
3
+ import functools
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import traceback
8
+ from collections.abc import Mapping
9
+
10
+ from .base.telemetrics import LOGGER
11
+
12
+ if 'ALGO_DIR' in os.environ:
13
+ WORKING_DIRECTORY = os.path.realpath(os.environ['ALGO_DIR'])
14
+ else:
15
+ WORKING_DIRECTORY = str(os.getcwd())
16
+
17
+ from . import base
18
+ from . import exchange_profile
19
+ from . import engine
20
+ from . import backtest
21
+ from . import strategy
22
+ from . import apps
23
+ from . import monitor
24
+ from . import utils
25
+
26
+
27
+ def set_logger(logger: logging.Logger):
28
+ from . import base
29
+ from . import exchange_profile
30
+ from . import engine
31
+ from . import backtest
32
+ from . import strategy
33
+ from . import apps
34
+
35
+ base.set_logger(logger=logger)
36
+ exchange_profile.set_logger(logger=logger.getChild('ExchangeProfile'))
37
+ engine.set_logger(logger=logger.getChild('Engine'))
38
+ backtest.set_logger(logger=logger.getChild('BackTest'))
39
+ strategy.set_logger(logger=logger.getChild('Strategy'))
40
+ apps.set_logger(logger=logger.getChild('Apps'))
41
+
42
+
43
+ LOGGER.info(f'AlgoEngine version {__version__}')
44
+
45
+ # import addon module
46
+ try:
47
+ from . import algo_addon
48
+
49
+ LOGGER.info(f'PyAlgoEngineAddons import successful, version {algo_addon.__version__}')
50
+ except ImportError:
51
+ algo_addon = None
52
+ LOGGER.debug(f'Install PyAlgoEngineAddons to use additional trading algos module\n{traceback.format_exc()}')
53
+
54
+
55
+ def _format_config_view(config: Mapping, indent: int = 0) -> str:
56
+ """Render a (possibly nested) config view as indented bullet lines."""
57
+ lines = []
58
+ for key, value in config.items():
59
+ if isinstance(value, Mapping):
60
+ lines.append(f"{' ' * indent}- {key}:")
61
+ lines.append(_format_config_view(value, indent + 1))
62
+ else:
63
+ lines.append(f"{' ' * indent}- {key}: {value}")
64
+ return "\n".join(lines)
65
+
66
+
67
+ @functools.cache
68
+ def get_include() -> list[str]:
69
+ import os
70
+ from .base import CONFIG
71
+
72
+ res_dir = pathlib.Path(__file__).parent
73
+ LOGGER.info(
74
+ f'Building with <PyAlgoEngine> version: "{__version__}", resource directory: "{res_dir}", '
75
+ f"config:\n{_format_config_view(CONFIG)}"
76
+ )
77
+
78
+ scr_dir = [
79
+ os.path.realpath(res_dir),
80
+ os.path.realpath(res_dir / 'base'),
81
+ os.path.realpath(res_dir / 'base' / 'c_market_data'),
82
+ os.path.realpath(res_dir / 'exchange_profile'),
83
+ os.path.realpath(res_dir / 'engine'),
84
+ ]
85
+
86
+ # Auto-generated unified include root (mirrors every C header under
87
+ # algo_engine/include/algo_engine/...). Populated by setup.py's
88
+ # collect_headers step; expose it to downstream builds when present.
89
+ include_root = os.path.realpath(res_dir / 'includes')
90
+ if os.path.isdir(include_root):
91
+ scr_dir.append(include_root)
92
+
93
+ import cbase
94
+ dep_dir = cbase.get_include()
95
+
96
+ import event_engine
97
+ dep_dir += event_engine.get_include()
98
+
99
+ return scr_dir + dep_dir
100
+
101
+
102
+ __all__ = [
103
+ 'apps', 'backtest', 'base', 'engine', 'exchange_profile', 'monitor', 'profile', 'strategy', 'utils',
104
+ 'algo_addon',
105
+ 'get_include',
106
+ 'LOGGER'
107
+ ]
@@ -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>