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