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