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