2020-11-06 17:23:14 +00:00
|
|
|
# piker: trading gear for hackers
|
2022-03-16 11:28:03 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
2020-11-06 17:23:14 +00:00
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2021-09-15 11:38:21 +00:00
|
|
|
'''
|
|
|
|
High level chart-widget apis.
|
2021-01-01 22:48:22 +00:00
|
|
|
|
2021-09-15 11:38:21 +00:00
|
|
|
'''
|
2021-12-20 18:15:16 +00:00
|
|
|
from __future__ import annotations
|
2022-09-07 01:18:41 +00:00
|
|
|
from typing import (
|
|
|
|
Iterator,
|
|
|
|
Optional,
|
|
|
|
TYPE_CHECKING,
|
|
|
|
)
|
2020-06-19 12:01:10 +00:00
|
|
|
|
2021-09-11 22:19:58 +00:00
|
|
|
from PyQt5 import QtCore, QtWidgets
|
2022-03-16 11:28:03 +00:00
|
|
|
from PyQt5.QtCore import (
|
|
|
|
Qt,
|
|
|
|
QLineF,
|
2022-03-20 16:53:44 +00:00
|
|
|
# QPointF,
|
2022-03-16 11:28:03 +00:00
|
|
|
)
|
2021-07-27 10:09:40 +00:00
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
QFrame,
|
|
|
|
QWidget,
|
2021-09-21 19:25:36 +00:00
|
|
|
QHBoxLayout,
|
|
|
|
QVBoxLayout,
|
|
|
|
QSplitter,
|
2021-07-27 10:09:40 +00:00
|
|
|
)
|
2020-06-15 14:48:00 +00:00
|
|
|
import pyqtgraph as pg
|
2020-07-17 13:06:20 +00:00
|
|
|
import trio
|
2020-06-14 19:09:32 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
from ._axes import (
|
2020-06-19 12:01:10 +00:00
|
|
|
DynamicDateAxis,
|
2020-06-15 14:48:00 +00:00
|
|
|
PriceAxis,
|
|
|
|
)
|
2021-07-12 13:18:11 +00:00
|
|
|
from ._cursor import (
|
2021-01-03 16:10:08 +00:00
|
|
|
Cursor,
|
2020-10-29 21:08:03 +00:00
|
|
|
ContentsLabel,
|
2020-12-29 13:43:25 +00:00
|
|
|
)
|
2022-04-14 13:38:25 +00:00
|
|
|
from ..data._sharedmem import ShmArray
|
2021-07-12 13:18:11 +00:00
|
|
|
from ._ohlc import BarItems
|
2022-06-03 17:55:34 +00:00
|
|
|
from ._curve import (
|
|
|
|
Curve,
|
|
|
|
StepCurve,
|
|
|
|
)
|
2020-09-29 16:28:54 +00:00
|
|
|
from ._style import (
|
2020-10-20 12:43:51 +00:00
|
|
|
hcolor,
|
2020-09-29 16:28:54 +00:00
|
|
|
CHART_MARGINS,
|
2020-10-20 12:43:51 +00:00
|
|
|
_xaxis_at,
|
2022-11-29 15:56:17 +00:00
|
|
|
# _min_points_to_show,
|
2020-09-29 16:28:54 +00:00
|
|
|
)
|
2022-11-24 19:48:30 +00:00
|
|
|
from ..data.feed import (
|
|
|
|
Feed,
|
|
|
|
Flume,
|
|
|
|
)
|
2021-02-16 11:42:48 +00:00
|
|
|
from ..data._source import Symbol
|
2020-07-17 13:06:20 +00:00
|
|
|
from ..log import get_logger
|
2021-03-08 14:05:37 +00:00
|
|
|
from ._interaction import ChartView
|
2021-09-15 11:38:21 +00:00
|
|
|
from ._forms import FieldsForm
|
2022-01-21 22:02:44 +00:00
|
|
|
from ._overlay import PlotItemOverlay
|
2022-12-14 17:05:35 +00:00
|
|
|
from ._dataviz import Viz
|
2022-09-07 21:50:10 +00:00
|
|
|
from ._search import SearchWidget
|
2022-10-31 18:13:02 +00:00
|
|
|
from . import _pg_overrides as pgo
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2022-03-07 16:08:04 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from ._display import DisplayState
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-07-04 21:48:31 +00:00
|
|
|
log = get_logger(__name__)
|
|
|
|
|
2020-06-14 19:09:32 +00:00
|
|
|
|
2021-07-27 10:09:40 +00:00
|
|
|
class GodWidget(QWidget):
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
|
|
|
"Our lord and savior, the holy child of window-shua, there is no
|
2021-12-20 18:15:16 +00:00
|
|
|
widget above thee." - 6|6
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
The highest level composed widget which contains layouts for
|
2021-09-21 19:25:36 +00:00
|
|
|
organizing charts as well as other sub-widgets used to control or
|
|
|
|
modify them.
|
2021-05-30 12:47:21 +00:00
|
|
|
|
|
|
|
'''
|
2022-09-07 21:50:10 +00:00
|
|
|
search: SearchWidget
|
2022-09-09 00:00:50 +00:00
|
|
|
mode_name: str = 'god'
|
2022-09-07 21:50:10 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
def __init__(
|
|
|
|
|
|
|
|
self,
|
|
|
|
parent=None,
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
2020-06-14 19:09:32 +00:00
|
|
|
super().__init__(parent)
|
2020-11-03 17:25:08 +00:00
|
|
|
|
2022-09-07 21:50:10 +00:00
|
|
|
self.search: Optional[SearchWidget] = None
|
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
self.hbox = QHBoxLayout(self)
|
2021-05-14 11:52:27 +00:00
|
|
|
self.hbox.setContentsMargins(0, 0, 0, 0)
|
2021-07-26 15:31:36 +00:00
|
|
|
self.hbox.setSpacing(6)
|
|
|
|
self.hbox.setAlignment(Qt.AlignTop)
|
2021-05-14 11:52:27 +00:00
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
self.vbox = QVBoxLayout()
|
2021-04-23 15:12:29 +00:00
|
|
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.vbox.setSpacing(2)
|
2021-07-26 15:31:36 +00:00
|
|
|
self.vbox.setAlignment(Qt.AlignTop)
|
2020-11-03 17:25:08 +00:00
|
|
|
|
2021-05-14 11:52:27 +00:00
|
|
|
self.hbox.addLayout(self.vbox)
|
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
# self.toolbar_layout = QHBoxLayout()
|
2021-06-15 22:19:59 +00:00
|
|
|
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# self.vbox.addLayout(self.toolbar_layout)
|
2020-11-03 17:25:08 +00:00
|
|
|
|
2020-06-14 19:09:32 +00:00
|
|
|
# self.init_timeframes_ui()
|
|
|
|
# self.init_strategy_ui()
|
2021-05-14 11:52:27 +00:00
|
|
|
# self.vbox.addLayout(self.hbox)
|
2021-05-10 14:17:06 +00:00
|
|
|
|
2023-01-06 02:05:23 +00:00
|
|
|
self._chart_cache: dict[
|
|
|
|
str,
|
|
|
|
tuple[LinkedSplits, LinkedSplits],
|
|
|
|
] = {}
|
2022-08-30 23:09:18 +00:00
|
|
|
|
|
|
|
self.hist_linked: Optional[LinkedSplits] = None
|
|
|
|
self.rt_linked: Optional[LinkedSplits] = None
|
2022-09-06 18:23:39 +00:00
|
|
|
self._active_cursor: Optional[Cursor] = None
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
# assigned in the startup func `_async_main()`
|
|
|
|
self._root_n: trio.Nursery = None
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2022-02-10 19:21:17 +00:00
|
|
|
self._widgets: dict[str, QWidget] = {}
|
|
|
|
self._resizing: bool = False
|
|
|
|
|
2022-09-09 00:00:50 +00:00
|
|
|
# TODO: do we need this, when would god get resized
|
|
|
|
# and the window does not? Never right?!
|
|
|
|
# self.reg_for_resize(self)
|
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
@property
|
|
|
|
def linkedsplits(self) -> LinkedSplits:
|
|
|
|
return self.rt_linked
|
|
|
|
|
2020-06-14 19:09:32 +00:00
|
|
|
# XXX: strat loader/saver that we don't need yet.
|
|
|
|
# def init_strategy_ui(self):
|
|
|
|
# self.strategy_box = StrategyBoxWidget(self)
|
|
|
|
# self.toolbar_layout.addWidget(self.strategy_box)
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
def set_chart_symbols(
|
2021-09-21 19:25:36 +00:00
|
|
|
self,
|
2022-11-14 21:25:19 +00:00
|
|
|
group_key: tuple[str], # of form <fqsn>.<providername>
|
2022-08-30 23:09:18 +00:00
|
|
|
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
2021-09-21 19:25:36 +00:00
|
|
|
|
|
|
|
) -> None:
|
|
|
|
# re-sort org cache symbol list in LIFO order
|
|
|
|
cache = self._chart_cache
|
2022-11-14 21:25:19 +00:00
|
|
|
cache.pop(group_key, None)
|
|
|
|
cache[group_key] = all_linked
|
2021-09-21 19:25:36 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
def get_chart_symbols(
|
2021-09-21 19:25:36 +00:00
|
|
|
self,
|
|
|
|
symbol_key: str,
|
2021-12-20 18:15:16 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
2021-09-21 19:25:36 +00:00
|
|
|
return self._chart_cache.get(symbol_key)
|
|
|
|
|
2022-11-07 20:33:52 +00:00
|
|
|
async def load_symbols(
|
2020-06-16 17:32:03 +00:00
|
|
|
self,
|
2022-11-14 21:25:19 +00:00
|
|
|
fqsns: list[str],
|
2021-04-12 22:04:26 +00:00
|
|
|
loglevel: str,
|
|
|
|
reset: bool = False,
|
2021-06-16 12:28:57 +00:00
|
|
|
|
|
|
|
) -> trio.Event:
|
2021-12-20 18:15:16 +00:00
|
|
|
'''
|
|
|
|
Load a new contract into the charting app.
|
2020-12-14 17:21:39 +00:00
|
|
|
|
|
|
|
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2021-06-16 12:28:57 +00:00
|
|
|
'''
|
2022-11-07 20:33:52 +00:00
|
|
|
# NOTE: for now we use the first symbol in the set as the "key"
|
|
|
|
# for the overlay of feeds on the chart.
|
2022-11-14 21:25:19 +00:00
|
|
|
group_key: tuple[str] = tuple(fqsns)
|
2021-05-10 14:17:06 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
all_linked = self.get_chart_symbols(group_key)
|
2021-06-16 12:28:57 +00:00
|
|
|
order_mode_started = trio.Event()
|
|
|
|
|
2021-04-23 15:12:29 +00:00
|
|
|
if not self.vbox.isEmpty():
|
2021-07-30 14:51:50 +00:00
|
|
|
|
2022-09-13 21:46:50 +00:00
|
|
|
# XXX: seems to make switching slower?
|
|
|
|
# qframe = self.hist_linked.chart.qframe
|
|
|
|
# if qframe.sidepane is self.search:
|
|
|
|
# qframe.hbox.removeWidget(self.search)
|
2022-09-09 00:00:50 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
for linked in [self.rt_linked, self.hist_linked]:
|
2022-09-07 01:18:41 +00:00
|
|
|
# XXX: this is CRITICAL especially with pixel buffer caching
|
2022-08-30 23:09:18 +00:00
|
|
|
linked.hide()
|
|
|
|
linked.unfocus()
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# XXX: pretty sure we don't need this
|
|
|
|
# remove any existing plots?
|
|
|
|
# XXX: ahh we might want to support cache unloading..
|
|
|
|
# self.vbox.removeWidget(linked)
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2021-04-13 14:21:19 +00:00
|
|
|
# switching to a new viewable chart
|
2022-08-30 23:09:18 +00:00
|
|
|
if all_linked is None or reset:
|
2021-09-15 11:38:21 +00:00
|
|
|
from ._display import display_symbol_data
|
2021-04-12 22:04:26 +00:00
|
|
|
|
|
|
|
# we must load a fresh linked charts set
|
2022-08-30 23:09:18 +00:00
|
|
|
self.rt_linked = rt_charts = LinkedSplits(self)
|
|
|
|
self.hist_linked = hist_charts = LinkedSplits(self)
|
2021-05-05 14:10:34 +00:00
|
|
|
|
|
|
|
# spawn new task to start up and update new sub-chart instances
|
2021-04-12 22:04:26 +00:00
|
|
|
self._root_n.start_soon(
|
2021-06-15 22:19:59 +00:00
|
|
|
display_symbol_data,
|
2021-04-12 22:04:26 +00:00
|
|
|
self,
|
2022-11-07 20:33:52 +00:00
|
|
|
fqsns,
|
2021-04-12 22:04:26 +00:00
|
|
|
loglevel,
|
2021-06-16 12:28:57 +00:00
|
|
|
order_mode_started,
|
2021-04-12 22:04:26 +00:00
|
|
|
)
|
2021-05-05 14:10:34 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# self.vbox.addWidget(hist_charts)
|
|
|
|
self.vbox.addWidget(rt_charts)
|
2022-11-14 21:25:19 +00:00
|
|
|
self.set_chart_symbols(
|
|
|
|
group_key,
|
2022-08-30 23:09:18 +00:00
|
|
|
(hist_charts, rt_charts),
|
|
|
|
)
|
|
|
|
|
|
|
|
for linked in [hist_charts, rt_charts]:
|
|
|
|
linked.show()
|
|
|
|
linked.focus()
|
2021-12-20 18:15:16 +00:00
|
|
|
|
|
|
|
await trio.sleep(0)
|
2021-05-25 15:55:24 +00:00
|
|
|
|
2021-06-16 12:28:57 +00:00
|
|
|
else:
|
|
|
|
# symbol is already loaded and ems ready
|
|
|
|
order_mode_started.set()
|
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
self.hist_linked, self.rt_linked = all_linked
|
2021-08-16 11:52:15 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
for linked in all_linked:
|
|
|
|
# TODO:
|
|
|
|
# - we'll probably want per-instrument/provider state here?
|
|
|
|
# change the order config form over to the new chart
|
|
|
|
|
|
|
|
# chart is already in memory so just focus it
|
|
|
|
linked.show()
|
|
|
|
linked.focus()
|
|
|
|
linked.graphics_cycle()
|
|
|
|
await trio.sleep(0)
|
2021-07-30 14:51:50 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# resume feeds *after* rendering chart view asap
|
2022-09-13 21:46:50 +00:00
|
|
|
chart = linked.chart
|
2022-08-30 23:09:18 +00:00
|
|
|
if chart:
|
|
|
|
chart.resume_all_feeds()
|
2021-07-30 18:23:46 +00:00
|
|
|
|
2022-09-18 16:33:54 +00:00
|
|
|
# TODO: we need a check to see if the chart
|
|
|
|
# last had the xlast in view, if so then shift so it's
|
|
|
|
# still in view, if the user was viewing history then
|
|
|
|
# do nothing yah?
|
|
|
|
self.rt_linked.chart.default_view()
|
2022-03-21 19:27:46 +00:00
|
|
|
|
2022-09-09 00:00:50 +00:00
|
|
|
# if a history chart instance is already up then
|
|
|
|
# set the search widget as its sidepane.
|
|
|
|
hist_chart = self.hist_linked.chart
|
|
|
|
if hist_chart:
|
|
|
|
hist_chart.qframe.set_sidepane(self.search)
|
|
|
|
|
2022-09-13 21:46:50 +00:00
|
|
|
# NOTE: this is really stupid/hard to follow.
|
|
|
|
# we have to reposition the active position nav
|
|
|
|
# **AFTER** applying the search bar as a sidepane
|
|
|
|
# to the newly switched to symbol.
|
|
|
|
await trio.sleep(0)
|
2022-09-18 16:33:54 +00:00
|
|
|
|
|
|
|
# TODO: probably stick this in some kinda `LooknFeel` API?
|
|
|
|
for tracker in self.rt_linked.mode.trackers.values():
|
|
|
|
pp_nav = tracker.nav
|
|
|
|
if tracker.live_pp.size:
|
|
|
|
pp_nav.show()
|
|
|
|
pp_nav.hide_info()
|
|
|
|
else:
|
|
|
|
pp_nav.hide()
|
2022-09-13 21:46:50 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# set window titlebar info
|
|
|
|
symbol = self.rt_linked.symbol
|
2021-04-13 14:21:19 +00:00
|
|
|
if symbol is not None:
|
|
|
|
self.window.setWindowTitle(
|
2022-03-18 19:07:48 +00:00
|
|
|
f'{symbol.front_fqsn()} '
|
2021-04-13 14:21:19 +00:00
|
|
|
f'tick:{symbol.tick_size}'
|
|
|
|
)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-06-16 12:28:57 +00:00
|
|
|
return order_mode_started
|
|
|
|
|
2021-08-02 22:52:22 +00:00
|
|
|
def focus(self) -> None:
|
2021-12-20 18:15:16 +00:00
|
|
|
'''
|
|
|
|
Focus the top level widget which in turn focusses the chart
|
2021-08-02 22:52:22 +00:00
|
|
|
ala "view mode".
|
|
|
|
|
|
|
|
'''
|
|
|
|
# go back to view-mode focus (aka chart focus)
|
|
|
|
self.clearFocus()
|
2022-09-09 00:00:50 +00:00
|
|
|
chart = self.rt_linked.chart
|
|
|
|
if chart:
|
|
|
|
chart.setFocus()
|
2021-08-02 22:52:22 +00:00
|
|
|
|
2022-09-09 00:00:50 +00:00
|
|
|
def reg_for_resize(
|
|
|
|
self,
|
|
|
|
widget: QWidget,
|
|
|
|
) -> None:
|
|
|
|
getattr(widget, 'on_resize')
|
|
|
|
self._widgets[widget.mode_name] = widget
|
|
|
|
|
|
|
|
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
2021-12-20 18:15:16 +00:00
|
|
|
'''
|
2022-09-09 00:00:50 +00:00
|
|
|
Top level god widget handler from window (the real yaweh) resize
|
|
|
|
events such that any registered widgets which wish to be
|
|
|
|
notified are invoked using our pythonic `.on_resize()` method
|
|
|
|
api.
|
2021-12-20 18:15:16 +00:00
|
|
|
|
|
|
|
Where we do UX magic to make things not suck B)
|
|
|
|
|
|
|
|
'''
|
2022-02-10 19:21:17 +00:00
|
|
|
if self._resizing:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._resizing = True
|
|
|
|
|
|
|
|
log.info('God widget resize')
|
|
|
|
for name, widget in self._widgets.items():
|
|
|
|
widget.on_resize()
|
|
|
|
|
|
|
|
self._resizing = False
|
2021-12-20 18:15:16 +00:00
|
|
|
|
2022-09-09 00:00:50 +00:00
|
|
|
# on_resize = on_win_resize
|
|
|
|
|
2022-09-06 18:23:39 +00:00
|
|
|
def get_cursor(self) -> Cursor:
|
2022-09-07 01:18:41 +00:00
|
|
|
return self._active_cursor
|
2022-09-06 18:23:39 +00:00
|
|
|
|
2022-09-07 01:18:41 +00:00
|
|
|
def iter_linked(self) -> Iterator[LinkedSplits]:
|
|
|
|
for linked in [self.hist_linked, self.rt_linked]:
|
|
|
|
yield linked
|
2022-09-06 18:23:39 +00:00
|
|
|
|
2022-09-12 17:51:37 +00:00
|
|
|
def resize_all(self) -> None:
|
|
|
|
'''
|
|
|
|
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
|
|
|
sensible default ratios of what space is detected as available
|
|
|
|
on the display / window.
|
|
|
|
|
|
|
|
'''
|
|
|
|
rt_linked = self.rt_linked
|
|
|
|
rt_linked.set_split_sizes()
|
|
|
|
self.rt_linked.resize_sidepanes()
|
|
|
|
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
|
|
|
self.search.on_resize()
|
|
|
|
|
2020-06-16 17:32:03 +00:00
|
|
|
|
2021-08-23 18:48:53 +00:00
|
|
|
class ChartnPane(QFrame):
|
2021-12-20 18:15:16 +00:00
|
|
|
'''
|
|
|
|
One-off ``QFrame`` composite which pairs a chart
|
2021-08-23 18:48:53 +00:00
|
|
|
+ sidepane (often a ``FieldsForm`` + other widgets if
|
|
|
|
provided) forming a, sort of, "chart row" with a side panel
|
|
|
|
for configuration and display of off-chart data.
|
|
|
|
|
|
|
|
See composite widgets docs for deats:
|
|
|
|
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
|
|
|
|
|
|
|
'''
|
2022-09-09 00:00:50 +00:00
|
|
|
sidepane: FieldsForm | SearchWidget
|
2021-09-21 19:25:36 +00:00
|
|
|
hbox: QHBoxLayout
|
2022-09-09 00:00:50 +00:00
|
|
|
chart: Optional[ChartPlotWidget] = None
|
2021-08-23 18:48:53 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
|
|
|
|
sidepane: FieldsForm,
|
|
|
|
parent=None,
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
2022-09-09 00:00:50 +00:00
|
|
|
self._sidepane = sidepane
|
2021-08-23 18:48:53 +00:00
|
|
|
self.chart = None
|
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
hbox = self.hbox = QHBoxLayout(self)
|
2021-08-23 18:48:53 +00:00
|
|
|
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
|
|
|
hbox.setContentsMargins(0, 0, 0, 0)
|
|
|
|
hbox.setSpacing(3)
|
|
|
|
|
2022-09-07 21:50:10 +00:00
|
|
|
def set_sidepane(
|
|
|
|
self,
|
2022-09-09 00:00:50 +00:00
|
|
|
sidepane: FieldsForm | SearchWidget,
|
2022-09-07 21:50:10 +00:00
|
|
|
) -> None:
|
|
|
|
|
|
|
|
# add sidepane **after** chart; place it on axis side
|
|
|
|
self.hbox.addWidget(
|
|
|
|
sidepane,
|
|
|
|
alignment=Qt.AlignTop
|
|
|
|
)
|
2022-09-09 00:00:50 +00:00
|
|
|
self._sidepane = sidepane
|
|
|
|
|
|
|
|
def sidepane(self) -> FieldsForm | SearchWidget:
|
|
|
|
return self._sidepane
|
2022-09-07 21:50:10 +00:00
|
|
|
|
2021-08-23 18:48:53 +00:00
|
|
|
|
2021-07-27 10:09:40 +00:00
|
|
|
class LinkedSplits(QWidget):
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
2021-09-21 19:25:36 +00:00
|
|
|
Composite that holds a central chart plus a set of (derived)
|
|
|
|
subcharts (usually computed from the original data) arranged in
|
|
|
|
a splitter for resizing.
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-06-19 12:01:10 +00:00
|
|
|
A single internal references to the data is maintained
|
|
|
|
for each chart and can be updated externally.
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
'''
|
2021-01-04 19:46:47 +00:00
|
|
|
def __init__(
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2021-01-04 19:46:47 +00:00
|
|
|
self,
|
2021-06-15 22:19:59 +00:00
|
|
|
godwidget: GodWidget,
|
2021-04-12 22:04:26 +00:00
|
|
|
|
2021-01-04 19:46:47 +00:00
|
|
|
) -> None:
|
2021-06-15 22:19:59 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
super().__init__()
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
# self.signals_visible: bool = False
|
2021-06-21 20:45:27 +00:00
|
|
|
self.cursor: Cursor = None # crosshair graphics
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
self.godwidget = godwidget
|
2020-07-17 13:06:20 +00:00
|
|
|
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
2021-09-15 11:38:21 +00:00
|
|
|
self.subplots: dict[tuple[str, ...], ChartPlotWidget] = {}
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-06-15 22:19:59 +00:00
|
|
|
self.godwidget = godwidget
|
2022-01-24 20:06:51 +00:00
|
|
|
# placeholder for last appended ``PlotItem``'s bottom axis.
|
|
|
|
self.xaxis_chart = None
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
self.splitter = QSplitter(QtCore.Qt.Vertical)
|
2022-01-22 19:28:14 +00:00
|
|
|
self.splitter.setMidLineWidth(0)
|
|
|
|
self.splitter.setHandleWidth(2)
|
2022-09-10 02:08:30 +00:00
|
|
|
self.splitter.splitterMoved.connect(self.on_splitter_adjust)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-09-21 19:25:36 +00:00
|
|
|
self.layout = QVBoxLayout(self)
|
2020-06-15 14:48:00 +00:00
|
|
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.layout.addWidget(self.splitter)
|
|
|
|
|
2022-03-07 12:20:17 +00:00
|
|
|
# chart-local graphics state that can be passed to
|
|
|
|
# a ``graphic_update_cycle()`` call by any task wishing to
|
|
|
|
# update the UI for a given "chart instance".
|
2022-03-07 16:08:04 +00:00
|
|
|
self.display_state: Optional[DisplayState] = None
|
2022-03-07 12:20:17 +00:00
|
|
|
|
2021-04-29 13:03:28 +00:00
|
|
|
self._symbol: Symbol = None
|
2021-01-04 19:46:47 +00:00
|
|
|
|
2022-09-10 02:08:30 +00:00
|
|
|
def on_splitter_adjust(
|
|
|
|
self,
|
|
|
|
pos: int,
|
|
|
|
index: int,
|
|
|
|
) -> None:
|
|
|
|
# print(f'splitter moved pos:{pos}, index:{index}')
|
|
|
|
godw = self.godwidget
|
|
|
|
if self is godw.rt_linked:
|
|
|
|
godw.search.on_resize()
|
|
|
|
|
2022-04-15 22:47:45 +00:00
|
|
|
def graphics_cycle(self, **kwargs) -> None:
|
2022-03-07 16:08:04 +00:00
|
|
|
from . import _display
|
2022-04-15 22:47:45 +00:00
|
|
|
ds = self.display_state
|
|
|
|
if ds:
|
2022-11-15 20:04:28 +00:00
|
|
|
return _display.graphics_update_cycle(
|
|
|
|
ds,
|
2023-02-09 21:26:32 +00:00
|
|
|
ds.quotes,
|
2022-11-15 20:04:28 +00:00
|
|
|
**kwargs,
|
|
|
|
)
|
2022-03-07 16:08:04 +00:00
|
|
|
|
2021-01-04 19:46:47 +00:00
|
|
|
@property
|
|
|
|
def symbol(self) -> Symbol:
|
|
|
|
return self._symbol
|
|
|
|
|
2020-06-19 12:01:10 +00:00
|
|
|
def set_split_sizes(
|
|
|
|
self,
|
2021-09-20 17:41:24 +00:00
|
|
|
prop: Optional[float] = None,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2020-06-19 12:01:10 +00:00
|
|
|
) -> None:
|
2022-09-11 21:24:09 +00:00
|
|
|
'''
|
|
|
|
Set the proportion of space allocated for linked subcharts.
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
'''
|
2022-09-07 14:18:52 +00:00
|
|
|
ln = len(self.subplots) or 1
|
2021-09-20 17:41:24 +00:00
|
|
|
|
2022-03-15 13:11:12 +00:00
|
|
|
# proportion allocated to consumer subcharts
|
2021-09-20 17:41:24 +00:00
|
|
|
if not prop:
|
2022-09-11 21:24:09 +00:00
|
|
|
prop = 3/8
|
2022-03-15 13:11:12 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
h = self.height()
|
2022-09-15 17:46:36 +00:00
|
|
|
histview_h = h * (6/16)
|
2022-08-30 23:09:18 +00:00
|
|
|
h = h - histview_h
|
|
|
|
|
2020-06-19 12:01:10 +00:00
|
|
|
major = 1 - prop
|
2022-08-30 23:09:18 +00:00
|
|
|
min_h_ind = int((h * prop) / ln)
|
|
|
|
sizes = [
|
|
|
|
int(histview_h),
|
|
|
|
int(h * major),
|
|
|
|
]
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# give all subcharts the same remaining proportional height
|
2021-09-20 17:41:24 +00:00
|
|
|
sizes.extend([min_h_ind] * ln)
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-09-10 02:08:30 +00:00
|
|
|
if self.godwidget.rt_linked is self:
|
|
|
|
self.splitter.setSizes(sizes)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-04-12 22:04:26 +00:00
|
|
|
def focus(self) -> None:
|
|
|
|
if self.chart is not None:
|
2021-04-13 14:21:19 +00:00
|
|
|
self.chart.focus()
|
2021-04-12 22:04:26 +00:00
|
|
|
|
|
|
|
def unfocus(self) -> None:
|
|
|
|
if self.chart is not None:
|
|
|
|
self.chart.clearFocus()
|
|
|
|
|
2020-12-14 17:21:39 +00:00
|
|
|
def plot_ohlc_main(
|
2020-06-16 17:32:03 +00:00
|
|
|
self,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2020-06-16 17:32:03 +00:00
|
|
|
symbol: Symbol,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm: ShmArray,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume: Flume,
|
2021-08-23 18:48:53 +00:00
|
|
|
sidepane: FieldsForm,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-11-15 20:04:28 +00:00
|
|
|
style: str = 'ohlc_bar',
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2023-01-24 00:33:46 +00:00
|
|
|
**add_plot_kwargs,
|
|
|
|
|
2022-01-12 23:29:07 +00:00
|
|
|
) -> ChartPlotWidget:
|
|
|
|
'''
|
|
|
|
Start up and show main (price) chart and all linked subcharts.
|
2020-12-14 17:21:39 +00:00
|
|
|
|
|
|
|
The data input struct array must include OHLC fields.
|
2021-09-28 12:34:58 +00:00
|
|
|
|
|
|
|
'''
|
2020-07-17 13:06:20 +00:00
|
|
|
# add crosshairs
|
2021-06-21 20:45:27 +00:00
|
|
|
self.cursor = Cursor(
|
2021-06-15 22:19:59 +00:00
|
|
|
linkedsplits=self,
|
2021-08-18 14:08:57 +00:00
|
|
|
digits=symbol.tick_size_digits,
|
2020-06-15 14:48:00 +00:00
|
|
|
)
|
2021-07-27 10:09:40 +00:00
|
|
|
|
2021-09-28 12:34:58 +00:00
|
|
|
# NOTE: atm the first (and only) OHLC price chart for the symbol
|
|
|
|
# is given a special reference but in the future there shouldn't
|
|
|
|
# be no distinction since we will have multiple symbols per
|
|
|
|
# view as part of "aggregate feeds".
|
2020-07-17 13:06:20 +00:00
|
|
|
self.chart = self.add_plot(
|
2022-11-14 21:25:19 +00:00
|
|
|
name=symbol.fqsn,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm=shm,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume=flume,
|
2020-12-14 17:21:39 +00:00
|
|
|
style=style,
|
2020-08-26 18:15:52 +00:00
|
|
|
_is_main=True,
|
2021-08-23 18:48:53 +00:00
|
|
|
sidepane=sidepane,
|
2023-01-24 00:33:46 +00:00
|
|
|
**add_plot_kwargs,
|
2020-07-17 13:06:20 +00:00
|
|
|
)
|
2020-08-19 19:32:09 +00:00
|
|
|
# add crosshair graphic
|
2021-06-21 20:45:27 +00:00
|
|
|
self.chart.addItem(self.cursor)
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2020-08-19 19:32:09 +00:00
|
|
|
# style?
|
2021-07-27 10:09:40 +00:00
|
|
|
self.chart.setFrameStyle(
|
2021-08-10 21:04:19 +00:00
|
|
|
QFrame.StyledPanel |
|
|
|
|
QFrame.Plain
|
2021-07-27 10:09:40 +00:00
|
|
|
)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-08-19 19:32:09 +00:00
|
|
|
return self.chart
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-07-17 13:06:20 +00:00
|
|
|
def add_plot(
|
|
|
|
self,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2020-07-17 13:06:20 +00:00
|
|
|
name: str,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm: ShmArray,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume: Flume,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
array_key: Optional[str] = None,
|
2020-12-14 17:21:39 +00:00
|
|
|
style: str = 'line',
|
2020-08-26 18:15:52 +00:00
|
|
|
_is_main: bool = False,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2021-07-27 10:09:40 +00:00
|
|
|
sidepane: Optional[QWidget] = None,
|
2023-01-24 00:33:46 +00:00
|
|
|
draw_kwargs: dict = {},
|
2021-07-27 10:09:40 +00:00
|
|
|
|
2020-10-16 16:18:14 +00:00
|
|
|
**cpw_kwargs,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
) -> ChartPlotWidget:
|
|
|
|
'''
|
|
|
|
Add (sub)plots to chart widget by key.
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
'''
|
2020-08-26 18:15:52 +00:00
|
|
|
if self.chart is None and not _is_main:
|
2020-08-19 19:32:09 +00:00
|
|
|
raise RuntimeError(
|
2020-12-14 17:21:39 +00:00
|
|
|
"A main plot must be created first with `.plot_ohlc_main()`")
|
2020-08-19 19:32:09 +00:00
|
|
|
|
2020-07-17 13:06:20 +00:00
|
|
|
# use "indicator axis" by default
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
# TODO: we gotta possibly assign this back
|
|
|
|
# to the last subplot on removal of some last subplot
|
|
|
|
xaxis = DynamicDateAxis(
|
2022-11-14 21:25:19 +00:00
|
|
|
None,
|
2021-07-26 23:40:39 +00:00
|
|
|
orientation='bottom',
|
|
|
|
linkedsplits=self
|
|
|
|
)
|
2022-01-24 20:06:51 +00:00
|
|
|
axes = {
|
2022-11-14 21:25:19 +00:00
|
|
|
'right': PriceAxis(None, orientation='right'),
|
|
|
|
'left': PriceAxis(None, orientation='left'),
|
2022-01-24 20:06:51 +00:00
|
|
|
'bottom': xaxis,
|
|
|
|
}
|
2020-11-13 15:39:30 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
if sidepane is not False:
|
|
|
|
parent = qframe = ChartnPane(
|
|
|
|
sidepane=sidepane,
|
|
|
|
parent=self.splitter,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
parent = self.splitter
|
|
|
|
qframe = None
|
|
|
|
|
2020-07-17 13:06:20 +00:00
|
|
|
cpw = ChartPlotWidget(
|
2020-12-14 17:21:39 +00:00
|
|
|
|
|
|
|
# this name will be used to register the primary
|
|
|
|
# graphics curve managed by the subchart
|
|
|
|
name=name,
|
2021-07-26 23:40:39 +00:00
|
|
|
data_key=array_key or name,
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
parent=parent,
|
2021-06-15 22:19:59 +00:00
|
|
|
linkedsplits=self,
|
2022-01-24 20:06:51 +00:00
|
|
|
axisItems=axes,
|
2020-10-16 16:18:14 +00:00
|
|
|
**cpw_kwargs,
|
2020-07-17 13:06:20 +00:00
|
|
|
)
|
2022-11-14 21:25:19 +00:00
|
|
|
# TODO: wow i can't believe how confusing garbage all this axes
|
|
|
|
# stuff iss..
|
|
|
|
for axis in axes.values():
|
|
|
|
axis.pi = cpw.plotItem
|
|
|
|
|
2022-01-21 13:30:00 +00:00
|
|
|
cpw.hideAxis('left')
|
2023-01-21 23:39:41 +00:00
|
|
|
# cpw.removeAxis('left')
|
2022-01-21 13:30:00 +00:00
|
|
|
cpw.hideAxis('bottom')
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2022-07-21 15:37:18 +00:00
|
|
|
if (
|
2023-01-16 04:53:57 +00:00
|
|
|
_xaxis_at == 'bottom'
|
|
|
|
and (
|
2022-07-21 15:37:18 +00:00
|
|
|
self.xaxis_chart
|
|
|
|
or (
|
|
|
|
not self.subplots
|
|
|
|
and self.xaxis_chart is None
|
|
|
|
)
|
|
|
|
)
|
|
|
|
):
|
2023-01-16 04:53:57 +00:00
|
|
|
# hide the previous x-axis chart's bottom axis since we're
|
|
|
|
# presumably being appended to the bottom subplot.
|
2022-07-21 15:37:18 +00:00
|
|
|
if self.xaxis_chart:
|
|
|
|
self.xaxis_chart.hideAxis('bottom')
|
2022-01-21 13:30:00 +00:00
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
# presuming we only want it at the true bottom of all charts.
|
|
|
|
# XXX: uses new api from our ``pyqtgraph`` fork.
|
|
|
|
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
|
2022-01-21 13:30:00 +00:00
|
|
|
# _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
|
|
|
|
# assert 'bottom' not in self.xaxis_chart.plotItem.axes
|
2022-01-24 20:06:51 +00:00
|
|
|
self.xaxis_chart = cpw
|
2022-01-21 13:30:00 +00:00
|
|
|
cpw.showAxis('bottom')
|
2022-01-24 20:06:51 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
if qframe is not None:
|
|
|
|
qframe.chart = cpw
|
|
|
|
qframe.hbox.addWidget(cpw)
|
2021-07-27 14:41:51 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# so we can look this up and add back to the splitter
|
|
|
|
# on a symbol switch
|
|
|
|
cpw.qframe = qframe
|
|
|
|
assert cpw.parent() == qframe
|
2021-07-30 14:51:50 +00:00
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
# add sidepane **after** chart; place it on axis side
|
2022-09-07 21:50:10 +00:00
|
|
|
qframe.set_sidepane(sidepane)
|
|
|
|
# qframe.hbox.addWidget(
|
|
|
|
# sidepane,
|
|
|
|
# alignment=Qt.AlignTop
|
|
|
|
# )
|
2022-08-30 23:09:18 +00:00
|
|
|
|
|
|
|
cpw.sidepane = sidepane
|
2021-07-27 10:09:40 +00:00
|
|
|
|
2022-09-06 12:36:28 +00:00
|
|
|
cpw.plotItem.vb.linked = self
|
2021-07-27 10:09:40 +00:00
|
|
|
cpw.setFrameStyle(
|
|
|
|
QtWidgets.QFrame.StyledPanel
|
2022-01-22 19:28:14 +00:00
|
|
|
# | QtWidgets.QFrame.Plain
|
2021-07-27 10:09:40 +00:00
|
|
|
)
|
2022-01-22 19:28:14 +00:00
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
# don't show the little "autoscale" A label.
|
2020-09-29 16:28:54 +00:00
|
|
|
cpw.hideButtons()
|
2021-07-27 10:09:40 +00:00
|
|
|
|
2020-11-03 17:25:08 +00:00
|
|
|
# XXX: gives us outline on backside of y-axis
|
|
|
|
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
2020-07-17 13:06:20 +00:00
|
|
|
|
2021-08-11 20:29:56 +00:00
|
|
|
# link chart x-axis to main chart
|
|
|
|
# this is 1/2 of where the `Link` in ``LinkedSplit``
|
|
|
|
# comes from ;)
|
2023-01-16 04:53:57 +00:00
|
|
|
cpw.cv.setXLink(self.chart)
|
|
|
|
|
|
|
|
# NOTE: above is the same as the following,
|
|
|
|
# link this subchart's axes to the main top level chart.
|
|
|
|
# if self.chart:
|
|
|
|
# cpw.cv.linkView(0, self.chart.cv)
|
2020-07-17 13:06:20 +00:00
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
add_label = False
|
|
|
|
anchor_at = ('top', 'left')
|
2020-10-21 14:46:56 +00:00
|
|
|
|
2020-07-17 13:06:20 +00:00
|
|
|
# draw curve graphics
|
2022-11-15 20:04:28 +00:00
|
|
|
if style == 'ohlc_bar':
|
2022-01-24 20:06:51 +00:00
|
|
|
|
2022-11-24 20:33:58 +00:00
|
|
|
viz = cpw.draw_ohlc(
|
2022-01-24 20:06:51 +00:00
|
|
|
name,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume=flume,
|
2023-01-24 00:33:46 +00:00
|
|
|
array_key=array_key,
|
|
|
|
**draw_kwargs,
|
2022-01-24 20:06:51 +00:00
|
|
|
)
|
|
|
|
self.cursor.contents_labels.add_label(
|
|
|
|
cpw,
|
2022-01-12 23:29:07 +00:00
|
|
|
name,
|
2022-01-24 20:06:51 +00:00
|
|
|
anchor_at=('top', 'left'),
|
|
|
|
update_func=ContentsLabel.update_from_ohlc,
|
|
|
|
)
|
2020-12-14 17:21:39 +00:00
|
|
|
|
|
|
|
elif style == 'line':
|
2022-01-24 20:06:51 +00:00
|
|
|
add_label = True
|
2022-11-24 19:48:30 +00:00
|
|
|
# graphics, data_key = cpw.draw_curve(
|
2022-11-24 20:33:58 +00:00
|
|
|
viz = cpw.draw_curve(
|
2022-01-22 19:28:14 +00:00
|
|
|
name,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume,
|
2022-01-22 19:28:14 +00:00
|
|
|
array_key=array_key,
|
2021-09-21 12:13:57 +00:00
|
|
|
color='default_light',
|
2023-01-24 00:33:46 +00:00
|
|
|
**draw_kwargs,
|
2022-01-22 19:28:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
elif style == 'step':
|
2022-01-24 20:06:51 +00:00
|
|
|
add_label = True
|
2022-11-24 19:48:30 +00:00
|
|
|
# graphics, data_key = cpw.draw_curve(
|
2022-11-24 20:33:58 +00:00
|
|
|
viz = cpw.draw_curve(
|
2022-01-22 19:28:14 +00:00
|
|
|
name,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume,
|
2022-01-22 19:28:14 +00:00
|
|
|
array_key=array_key,
|
|
|
|
step_mode=True,
|
2021-09-20 17:41:24 +00:00
|
|
|
color='davies',
|
|
|
|
fill_color='davies',
|
2023-01-24 00:33:46 +00:00
|
|
|
**draw_kwargs,
|
2022-01-22 19:28:14 +00:00
|
|
|
)
|
2020-07-17 13:06:20 +00:00
|
|
|
|
2020-12-14 17:21:39 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(f"Chart style {style} is currently unsupported")
|
|
|
|
|
2023-01-18 18:33:19 +00:00
|
|
|
# NOTE: back-link the new sub-chart to trigger y-autoranging in
|
|
|
|
# the (ohlc parent) main chart for this linked set.
|
2023-01-21 23:39:41 +00:00
|
|
|
# if self.chart:
|
|
|
|
# main_viz = self.chart.get_viz(self.chart.name)
|
|
|
|
# self.chart.view.enable_auto_yrange(
|
|
|
|
# src_vb=cpw.view,
|
|
|
|
# viz=main_viz,
|
|
|
|
# )
|
2023-01-18 18:33:19 +00:00
|
|
|
|
2022-11-24 20:33:58 +00:00
|
|
|
graphics = viz.graphics
|
|
|
|
data_key = viz.name
|
2022-11-24 19:48:30 +00:00
|
|
|
|
2022-11-15 20:04:28 +00:00
|
|
|
if _is_main:
|
|
|
|
assert style == 'ohlc_bar', 'main chart must be OHLC'
|
|
|
|
else:
|
2020-08-19 19:32:09 +00:00
|
|
|
# track by name
|
|
|
|
self.subplots[name] = cpw
|
2022-08-30 23:09:18 +00:00
|
|
|
if qframe is not None:
|
|
|
|
self.splitter.addWidget(qframe)
|
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
# add to cross-hair's known plots
|
|
|
|
# NOTE: add **AFTER** creating the underlying ``PlotItem``s
|
|
|
|
# since we require that global (linked charts wide) axes have
|
|
|
|
# been created!
|
2022-11-15 20:04:28 +00:00
|
|
|
if self.cursor:
|
|
|
|
if (
|
|
|
|
_is_main
|
|
|
|
or style != 'ohlc_bar'
|
|
|
|
):
|
|
|
|
self.cursor.add_plot(cpw)
|
|
|
|
if style != 'ohlc_bar':
|
|
|
|
self.cursor.add_curve_cursor(cpw, graphics)
|
|
|
|
|
|
|
|
if add_label:
|
|
|
|
self.cursor.contents_labels.add_label(
|
|
|
|
cpw,
|
|
|
|
data_key,
|
|
|
|
anchor_at=anchor_at,
|
|
|
|
)
|
2022-01-24 20:06:51 +00:00
|
|
|
|
2021-12-20 18:15:16 +00:00
|
|
|
self.resize_sidepanes()
|
2020-08-19 19:32:09 +00:00
|
|
|
return cpw
|
2020-06-15 15:40:41 +00:00
|
|
|
|
2021-09-16 23:28:26 +00:00
|
|
|
def resize_sidepanes(
|
|
|
|
self,
|
2022-09-07 21:50:10 +00:00
|
|
|
from_linked: Optional[LinkedSplits] = None,
|
|
|
|
|
2021-09-16 23:28:26 +00:00
|
|
|
) -> None:
|
2021-12-20 18:15:16 +00:00
|
|
|
'''
|
|
|
|
Size all sidepanes based on the OHLC "main" plot and its
|
|
|
|
sidepane width.
|
2021-09-16 23:28:26 +00:00
|
|
|
|
|
|
|
'''
|
2022-09-07 21:50:10 +00:00
|
|
|
if from_linked:
|
|
|
|
main_chart = from_linked.chart
|
|
|
|
else:
|
|
|
|
main_chart = self.chart
|
|
|
|
|
2022-08-30 23:09:18 +00:00
|
|
|
if main_chart and main_chart.sidepane:
|
2021-12-20 18:15:16 +00:00
|
|
|
sp_w = main_chart.sidepane.width()
|
|
|
|
for name, cpw in self.subplots.items():
|
|
|
|
cpw.sidepane.setMinimumWidth(sp_w)
|
|
|
|
cpw.sidepane.setMaximumWidth(sp_w)
|
2021-09-16 23:28:26 +00:00
|
|
|
|
2022-09-07 21:50:10 +00:00
|
|
|
if from_linked:
|
|
|
|
self.chart.sidepane.setMinimumWidth(sp_w)
|
|
|
|
|
2022-01-09 15:34:38 +00:00
|
|
|
|
2022-11-29 15:56:17 +00:00
|
|
|
# TODO: we should really drop using this type and instead just
|
|
|
|
# write our own wrapper around `PlotItem`..
|
2020-06-15 14:48:00 +00:00
|
|
|
class ChartPlotWidget(pg.PlotWidget):
|
2021-06-15 22:19:59 +00:00
|
|
|
'''
|
2023-01-16 04:53:57 +00:00
|
|
|
``GraphicsView`` subtype containing a ``.plotItem: PlotItem`` as well
|
|
|
|
as a `.pi_overlay: PlotItemOverlay`` which helps manage and overlay flow
|
|
|
|
graphics view multiple compose view boxes.
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-06-19 12:01:10 +00:00
|
|
|
- The added methods allow for plotting OHLC sequences from
|
2020-07-04 21:48:31 +00:00
|
|
|
``np.ndarray``s with appropriate field names.
|
2020-06-19 12:01:10 +00:00
|
|
|
- Overrides a ``pyqtgraph.PlotWidget`` (a ``GraphicsView`` containing
|
|
|
|
a single ``PlotItem``) to intercept and and re-emit mouse enter/exit
|
|
|
|
events.
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
|
2020-06-19 12:01:10 +00:00
|
|
|
eventually want multiple plots managed together?)
|
2021-06-15 22:19:59 +00:00
|
|
|
|
|
|
|
'''
|
2021-07-21 19:50:09 +00:00
|
|
|
sig_mouse_leave = QtCore.pyqtSignal(object)
|
|
|
|
sig_mouse_enter = QtCore.pyqtSignal(object)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-07-24 20:07:04 +00:00
|
|
|
mode_name: str = 'view'
|
2021-05-30 12:47:21 +00:00
|
|
|
|
2020-06-16 17:32:03 +00:00
|
|
|
# TODO: can take a ``background`` color setting - maybe there's
|
|
|
|
# a better one?
|
2022-01-24 20:06:51 +00:00
|
|
|
def mk_vb(self, name: str) -> ChartView:
|
|
|
|
cv = ChartView(name)
|
2022-11-04 20:28:10 +00:00
|
|
|
# link new view to chart's view set
|
|
|
|
cv.linked = self.linked
|
2022-01-24 20:06:51 +00:00
|
|
|
return cv
|
2020-06-16 17:32:03 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
# the "data view" we generate graphics from
|
2020-12-14 17:21:39 +00:00
|
|
|
name: str,
|
2021-07-26 23:40:39 +00:00
|
|
|
data_key: str,
|
2021-06-15 22:19:59 +00:00
|
|
|
linkedsplits: LinkedSplits,
|
2021-02-16 11:42:48 +00:00
|
|
|
|
|
|
|
view_color: str = 'papas_special',
|
|
|
|
pen_color: str = 'bracket',
|
|
|
|
|
2021-10-07 00:17:13 +00:00
|
|
|
# TODO: load from config
|
|
|
|
use_open_gl: bool = False,
|
|
|
|
|
2021-09-15 11:38:21 +00:00
|
|
|
static_yrange: Optional[tuple[float, float]] = None,
|
2021-02-16 11:42:48 +00:00
|
|
|
|
2022-10-31 18:13:02 +00:00
|
|
|
parent=None,
|
2020-06-15 14:48:00 +00:00
|
|
|
**kwargs,
|
|
|
|
):
|
2021-12-22 13:30:22 +00:00
|
|
|
'''
|
|
|
|
Configure initial display settings and connect view callback
|
|
|
|
handlers.
|
|
|
|
|
|
|
|
'''
|
2021-02-16 11:42:48 +00:00
|
|
|
self.view_color = view_color
|
|
|
|
self.pen_color = pen_color
|
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
# NOTE: must be set bfore calling ``.mk_vb()``
|
|
|
|
self.linked = linkedsplits
|
2022-08-30 23:09:18 +00:00
|
|
|
self.sidepane: Optional[FieldsForm] = None
|
2022-01-24 20:06:51 +00:00
|
|
|
|
|
|
|
# source of our custom interactions
|
2023-01-14 21:11:25 +00:00
|
|
|
self.cv = self.mk_vb(name)
|
2022-01-24 20:06:51 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
pi = pgo.PlotItem(
|
2023-01-14 21:11:25 +00:00
|
|
|
viewBox=self.cv,
|
2022-11-14 21:25:19 +00:00
|
|
|
name=name,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
pi.chart_widget = self
|
2020-08-31 21:18:35 +00:00
|
|
|
super().__init__(
|
2021-02-16 11:42:48 +00:00
|
|
|
background=hcolor(view_color),
|
2023-01-14 21:11:25 +00:00
|
|
|
viewBox=self.cv,
|
2020-08-31 21:18:35 +00:00
|
|
|
# parent=None,
|
|
|
|
# plotItem=None,
|
2020-10-19 18:18:06 +00:00
|
|
|
# antialias=True,
|
2022-10-31 18:13:02 +00:00
|
|
|
parent=parent,
|
|
|
|
plotItem=pi,
|
2020-08-31 21:18:35 +00:00
|
|
|
**kwargs
|
|
|
|
)
|
2022-01-24 20:06:51 +00:00
|
|
|
# give viewbox as reference to chart
|
|
|
|
# allowing for kb controls and interactions on **this** widget
|
|
|
|
# (see our custom view mode in `._interactions.py`)
|
2023-01-14 21:11:25 +00:00
|
|
|
self.cv.chart = self
|
|
|
|
|
|
|
|
self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
|
2022-01-24 20:06:51 +00:00
|
|
|
|
2022-01-09 15:34:38 +00:00
|
|
|
# ensure internal pi matches
|
|
|
|
assert self.cv is self.plotItem.vb
|
|
|
|
|
2021-10-07 00:17:13 +00:00
|
|
|
self.useOpenGL(use_open_gl)
|
2020-12-14 17:21:39 +00:00
|
|
|
self.name = name
|
2022-01-09 15:34:38 +00:00
|
|
|
self.data_key = data_key or name
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2021-03-17 12:36:34 +00:00
|
|
|
# scene-local placeholder for book graphics
|
2021-03-12 02:40:50 +00:00
|
|
|
# sizing to avoid overlap with data contents
|
|
|
|
self._max_l1_line_len: float = 0
|
|
|
|
|
2020-11-03 17:25:08 +00:00
|
|
|
# self.setViewportMargins(0, 0, 0, 0)
|
2022-02-08 20:52:50 +00:00
|
|
|
|
2021-09-26 21:14:30 +00:00
|
|
|
# registry of overlay curve names
|
2022-11-24 20:33:58 +00:00
|
|
|
self._vizs: dict[str, Viz] = {}
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2022-11-16 18:40:15 +00:00
|
|
|
self.feed: Feed | None = None
|
2021-08-16 11:52:15 +00:00
|
|
|
|
2020-08-26 18:15:52 +00:00
|
|
|
self._labels = {} # registry of underlying graphics
|
2020-08-30 16:29:29 +00:00
|
|
|
self._ysticks = {} # registry of underlying graphics
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2020-10-16 16:18:14 +00:00
|
|
|
self._static_yrange = static_yrange # for "known y-range style"
|
2020-10-20 12:43:51 +00:00
|
|
|
self._view_mode: str = 'follow'
|
2020-06-17 15:45:43 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
# show background grid
|
2021-02-16 11:42:48 +00:00
|
|
|
self.showGrid(x=False, y=True, alpha=0.3)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2022-06-28 13:37:12 +00:00
|
|
|
# indempotent startup flag for auto-yrange subsys
|
|
|
|
# to detect the "first time" y-domain graphics begin
|
|
|
|
# to be shown in the (main) graphics view.
|
|
|
|
self._on_screen: bool = False
|
|
|
|
|
2021-12-22 13:30:22 +00:00
|
|
|
def resume_all_feeds(self):
|
2022-11-16 18:40:15 +00:00
|
|
|
feed = self.feed
|
|
|
|
if feed:
|
|
|
|
try:
|
|
|
|
self.linked.godwidget._root_n.start_soon(feed.resume)
|
|
|
|
except RuntimeError:
|
|
|
|
# TODO: cancel the qtractor runtime here?
|
|
|
|
raise
|
2021-08-16 11:52:15 +00:00
|
|
|
|
2021-12-22 13:30:22 +00:00
|
|
|
def pause_all_feeds(self):
|
2022-11-16 18:40:15 +00:00
|
|
|
feed = self.feed
|
|
|
|
if feed:
|
|
|
|
self.linked.godwidget._root_n.start_soon(feed.pause)
|
2021-08-16 11:52:15 +00:00
|
|
|
|
2021-07-22 00:00:11 +00:00
|
|
|
@property
|
|
|
|
def view(self) -> ChartView:
|
2022-01-09 15:34:38 +00:00
|
|
|
return self.plotItem.vb
|
2021-07-22 00:00:11 +00:00
|
|
|
|
2021-04-13 14:21:19 +00:00
|
|
|
def focus(self) -> None:
|
2022-01-09 15:34:38 +00:00
|
|
|
self.view.setFocus()
|
2021-04-13 14:21:19 +00:00
|
|
|
|
2022-03-21 13:20:54 +00:00
|
|
|
def pre_l1_xs(self) -> tuple[float, float]:
|
2022-03-20 16:53:44 +00:00
|
|
|
'''
|
2022-03-21 13:20:54 +00:00
|
|
|
Return the view x-coord for the value just before
|
2022-03-20 16:53:44 +00:00
|
|
|
the L1 labels on the y-axis as well as the length
|
|
|
|
of that L1 label from the y-axis.
|
|
|
|
|
|
|
|
'''
|
2022-03-21 13:20:54 +00:00
|
|
|
line_end, marker_right, yaxis_x = self.marker_right_points()
|
2022-12-07 22:04:15 +00:00
|
|
|
line = self.view.mapToView(
|
2022-03-21 13:20:54 +00:00
|
|
|
QLineF(line_end, 0, yaxis_x, 0)
|
|
|
|
)
|
2022-12-07 22:04:15 +00:00
|
|
|
linex, linelen = line.x1(), line.length()
|
|
|
|
# print(
|
|
|
|
# f'line: {line}\n'
|
|
|
|
# f'linex: {linex}\n'
|
|
|
|
# f'linelen: {linelen}\n'
|
|
|
|
# )
|
|
|
|
return linex, linelen
|
2022-03-21 13:20:54 +00:00
|
|
|
|
|
|
|
def marker_right_points(
|
|
|
|
self,
|
|
|
|
marker_size: int = 20,
|
|
|
|
|
|
|
|
) -> (float, float, float):
|
|
|
|
'''
|
|
|
|
Return x-dimension, y-axis-aware, level-line marker oriented scene
|
|
|
|
values.
|
|
|
|
|
|
|
|
X values correspond to set the end of a level line, end of
|
|
|
|
a paried level line marker, and the right most side of the "right"
|
|
|
|
axis respectively.
|
|
|
|
|
|
|
|
'''
|
|
|
|
# TODO: compute some sensible maximum value here
|
|
|
|
# and use a humanized scheme to limit to that length.
|
2022-12-27 18:10:25 +00:00
|
|
|
from ._l1 import L1Label
|
|
|
|
l1_len = abs(L1Label._x_br_offset)
|
2022-03-20 16:53:44 +00:00
|
|
|
ryaxis = self.getAxis('right')
|
|
|
|
|
2022-03-21 13:20:54 +00:00
|
|
|
r_axis_x = ryaxis.pos().x()
|
2022-12-07 22:04:15 +00:00
|
|
|
up_to_l1_sc = r_axis_x - l1_len
|
2022-03-21 13:20:54 +00:00
|
|
|
marker_right = up_to_l1_sc - (1.375 * 2 * marker_size)
|
2023-01-03 02:11:36 +00:00
|
|
|
# line_end = marker_right - (6/16 * marker_size)
|
|
|
|
line_end = marker_right - marker_size
|
2022-03-21 13:20:54 +00:00
|
|
|
|
2022-12-07 22:04:15 +00:00
|
|
|
# print(
|
|
|
|
# f'r_axis_x: {r_axis_x}\n'
|
|
|
|
# f'up_to_l1_sc: {up_to_l1_sc}\n'
|
|
|
|
# f'marker_right: {marker_right}\n'
|
|
|
|
# f'line_end: {line_end}\n'
|
|
|
|
# )
|
2022-03-21 13:20:54 +00:00
|
|
|
return line_end, marker_right, r_axis_x
|
|
|
|
|
|
|
|
def default_view(
|
|
|
|
self,
|
2022-08-30 23:09:18 +00:00
|
|
|
bars_from_y: int = int(616 * 3/8),
|
|
|
|
y_offset: int = 0,
|
2022-06-28 13:37:12 +00:00
|
|
|
do_ds: bool = True,
|
2022-03-21 13:20:54 +00:00
|
|
|
|
|
|
|
) -> None:
|
2022-02-22 20:08:41 +00:00
|
|
|
'''
|
|
|
|
Set the view box to the "default" startup view of the scene.
|
|
|
|
|
|
|
|
'''
|
2022-11-29 15:56:17 +00:00
|
|
|
viz = self.get_viz(self.name)
|
2022-12-07 22:04:15 +00:00
|
|
|
|
2022-11-29 15:56:17 +00:00
|
|
|
if not viz:
|
2022-11-24 20:33:58 +00:00
|
|
|
log.warning(f'`Viz` for {self.name} not loaded yet?')
|
2022-02-22 20:08:41 +00:00
|
|
|
return
|
2020-10-20 12:43:51 +00:00
|
|
|
|
2022-12-07 22:04:15 +00:00
|
|
|
viz.default_view(
|
|
|
|
bars_from_y,
|
|
|
|
y_offset,
|
|
|
|
do_ds,
|
2020-10-15 19:08:56 +00:00
|
|
|
)
|
2022-06-28 13:37:12 +00:00
|
|
|
|
|
|
|
if do_ds:
|
2022-03-15 13:11:12 +00:00
|
|
|
self.linked.graphics_cycle()
|
2020-06-17 15:45:43 +00:00
|
|
|
|
2020-10-20 12:43:51 +00:00
|
|
|
def increment_view(
|
|
|
|
self,
|
2022-12-19 20:10:34 +00:00
|
|
|
datums: int = 1,
|
2022-04-04 17:47:34 +00:00
|
|
|
vb: Optional[ChartView] = None,
|
2022-02-22 20:08:41 +00:00
|
|
|
|
2020-10-20 12:43:51 +00:00
|
|
|
) -> None:
|
2022-12-19 20:10:34 +00:00
|
|
|
'''
|
|
|
|
Increment the data view ``datums``` steps toward y-axis thus
|
|
|
|
"following" the current time slot/step/bar.
|
2020-08-30 16:29:29 +00:00
|
|
|
|
2022-12-19 20:10:34 +00:00
|
|
|
'''
|
2022-04-04 17:47:34 +00:00
|
|
|
view = vb or self.view
|
2022-12-19 20:10:34 +00:00
|
|
|
viz = self.main_viz
|
|
|
|
l, r = viz.view_range()
|
|
|
|
x_shift = viz.index_step() * datums
|
|
|
|
|
|
|
|
if datums >= 300:
|
2022-12-07 22:04:15 +00:00
|
|
|
print("FUCKING FIX THE GLOBAL STEP BULLSHIT")
|
|
|
|
# breakpoint()
|
|
|
|
return
|
|
|
|
|
2022-04-04 17:47:34 +00:00
|
|
|
view.setXRange(
|
2022-12-19 20:10:34 +00:00
|
|
|
min=l + x_shift,
|
|
|
|
max=r + x_shift,
|
2021-09-21 19:25:36 +00:00
|
|
|
|
2020-12-14 17:21:39 +00:00
|
|
|
# TODO: holy shit, wtf dude... why tf would this not be 0 by
|
2020-10-20 12:43:51 +00:00
|
|
|
# default... speechless.
|
|
|
|
padding=0,
|
|
|
|
)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2022-01-12 23:29:07 +00:00
|
|
|
def overlay_plotitem(
|
|
|
|
self,
|
|
|
|
name: str,
|
2022-01-21 13:30:00 +00:00
|
|
|
index: Optional[int] = None,
|
|
|
|
axis_title: Optional[str] = None,
|
|
|
|
axis_side: str = 'right',
|
2022-01-17 14:46:17 +00:00
|
|
|
axis_kwargs: dict = {},
|
2022-01-12 23:29:07 +00:00
|
|
|
|
2022-10-31 18:13:02 +00:00
|
|
|
) -> pgo.PlotItem:
|
2022-01-21 13:30:00 +00:00
|
|
|
|
2022-01-12 23:29:07 +00:00
|
|
|
# Custom viewbox impl
|
|
|
|
cv = self.mk_vb(name)
|
|
|
|
cv.chart = self
|
|
|
|
|
2022-01-21 13:30:00 +00:00
|
|
|
allowed_sides = {'left', 'right'}
|
|
|
|
if axis_side not in allowed_sides:
|
|
|
|
raise ValueError(f'``axis_side``` must be in {allowed_sides}')
|
2022-10-31 18:13:02 +00:00
|
|
|
|
2022-01-12 23:29:07 +00:00
|
|
|
yaxis = PriceAxis(
|
2022-11-14 21:25:19 +00:00
|
|
|
plotitem=None,
|
2022-01-21 13:30:00 +00:00
|
|
|
orientation=axis_side,
|
2022-01-17 14:46:17 +00:00
|
|
|
**axis_kwargs,
|
2022-01-12 23:29:07 +00:00
|
|
|
)
|
|
|
|
|
2022-10-31 18:13:02 +00:00
|
|
|
pi = pgo.PlotItem(
|
2022-01-12 23:29:07 +00:00
|
|
|
parent=self.plotItem,
|
|
|
|
name=name,
|
|
|
|
enableMenu=False,
|
|
|
|
viewBox=cv,
|
|
|
|
axisItems={
|
|
|
|
# 'bottom': xaxis,
|
2022-01-21 13:30:00 +00:00
|
|
|
axis_side: yaxis,
|
2022-01-12 23:29:07 +00:00
|
|
|
},
|
|
|
|
default_axes=[],
|
|
|
|
)
|
2022-11-15 20:04:28 +00:00
|
|
|
# pi.vb.background.setOpacity(0)
|
2022-11-14 21:25:19 +00:00
|
|
|
yaxis.pi = pi
|
|
|
|
pi.chart_widget = self
|
2022-01-21 13:30:00 +00:00
|
|
|
pi.hideButtons()
|
2022-01-12 23:29:07 +00:00
|
|
|
|
2023-01-21 23:39:41 +00:00
|
|
|
# hide all axes not named by ``axis_side``
|
|
|
|
for axname in (
|
|
|
|
({'bottom'} | allowed_sides) - {axis_side}
|
|
|
|
):
|
|
|
|
pi.hideAxis(axname)
|
|
|
|
|
2022-01-21 13:30:00 +00:00
|
|
|
# compose this new plot's graphics with the current chart's
|
|
|
|
# existing one but with separate axes as neede and specified.
|
2022-01-12 23:29:07 +00:00
|
|
|
self.pi_overlay.add_plotitem(
|
2022-01-21 13:30:00 +00:00
|
|
|
pi,
|
|
|
|
index=index,
|
|
|
|
|
2022-11-04 20:28:10 +00:00
|
|
|
# only link x-axes and
|
|
|
|
# don't relay any ``ViewBox`` derived event
|
|
|
|
# handlers since we only care about keeping charts
|
|
|
|
# x-synced on interaction (at least for now).
|
2022-01-12 23:29:07 +00:00
|
|
|
link_axes=(0,),
|
|
|
|
)
|
2022-01-21 13:30:00 +00:00
|
|
|
|
|
|
|
# add axis title
|
|
|
|
# TODO: do we want this API to still work?
|
|
|
|
# raxis = pi.getAxis('right')
|
|
|
|
axis = self.pi_overlay.get_axis(pi, axis_side)
|
|
|
|
axis.set_title(axis_title or name, view=pi.getViewBox())
|
|
|
|
|
|
|
|
return pi
|
2022-01-12 23:29:07 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
def draw_curve(
|
|
|
|
self,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2020-08-26 18:15:52 +00:00
|
|
|
name: str,
|
2022-04-14 13:38:25 +00:00
|
|
|
shm: ShmArray,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume: Flume,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
|
|
|
array_key: Optional[str] = None,
|
2020-09-11 17:16:11 +00:00
|
|
|
overlay: bool = False,
|
2022-01-22 19:28:14 +00:00
|
|
|
color: Optional[str] = None,
|
2020-12-14 17:21:39 +00:00
|
|
|
add_label: bool = True,
|
2022-04-07 15:13:02 +00:00
|
|
|
pi: Optional[pg.PlotItem] = None,
|
2022-06-03 17:55:34 +00:00
|
|
|
step_mode: bool = False,
|
2022-11-14 21:25:19 +00:00
|
|
|
is_ohlc: bool = False,
|
|
|
|
add_sticky: None | str = 'right',
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
**graphics_kwargs,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-11-24 20:33:58 +00:00
|
|
|
) -> Viz:
|
2022-01-24 20:06:51 +00:00
|
|
|
'''
|
|
|
|
Draw a "curve" (line plot graphics) for the provided data in
|
2022-04-14 13:38:25 +00:00
|
|
|
the input shm array ``shm``.
|
2020-12-14 17:21:39 +00:00
|
|
|
|
2022-01-24 20:06:51 +00:00
|
|
|
'''
|
2021-09-26 21:14:30 +00:00
|
|
|
color = color or self.pen_color or 'default_light'
|
2021-07-26 23:40:39 +00:00
|
|
|
data_key = array_key or name
|
2021-07-27 14:41:51 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
pi = pi or self.plotItem
|
2020-12-26 22:51:01 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
if is_ohlc:
|
|
|
|
graphics = BarItems(
|
|
|
|
color=color,
|
|
|
|
name=name,
|
|
|
|
**graphics_kwargs,
|
|
|
|
)
|
2020-12-26 22:51:01 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
else:
|
|
|
|
curve_type = {
|
|
|
|
None: Curve,
|
|
|
|
'step': StepCurve,
|
|
|
|
# TODO:
|
|
|
|
# 'bars': BarsItems
|
|
|
|
}['step' if step_mode else None]
|
|
|
|
|
|
|
|
graphics = curve_type(
|
|
|
|
name=name,
|
|
|
|
color=color,
|
|
|
|
**graphics_kwargs,
|
|
|
|
)
|
2022-01-12 23:29:07 +00:00
|
|
|
|
2022-11-29 15:56:17 +00:00
|
|
|
viz = self._vizs[data_key] = Viz(
|
2022-11-24 19:48:30 +00:00
|
|
|
data_key,
|
|
|
|
pi,
|
|
|
|
shm,
|
|
|
|
flume,
|
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
is_ohlc=is_ohlc,
|
2022-11-29 15:56:17 +00:00
|
|
|
# register curve graphics with this viz
|
2022-11-14 21:25:19 +00:00
|
|
|
graphics=graphics,
|
2022-04-06 21:05:57 +00:00
|
|
|
)
|
2023-01-18 18:33:19 +00:00
|
|
|
|
|
|
|
# connect auto-yrange callbacks *from* this new
|
|
|
|
# view **to** this parent and likewise *from* the
|
|
|
|
# main/parent chart back *to* the created overlay.
|
|
|
|
pi.vb.enable_auto_yrange(
|
|
|
|
src_vb=self.view,
|
|
|
|
viz=viz,
|
|
|
|
)
|
|
|
|
|
2023-01-16 04:53:57 +00:00
|
|
|
pi.viz = viz
|
2022-11-29 15:56:17 +00:00
|
|
|
assert isinstance(viz.shm, ShmArray)
|
2022-04-06 21:05:57 +00:00
|
|
|
|
2022-01-12 23:29:07 +00:00
|
|
|
# TODO: this probably needs its own method?
|
2020-09-11 17:16:11 +00:00
|
|
|
if overlay:
|
2022-10-31 18:13:02 +00:00
|
|
|
if isinstance(overlay, pgo.PlotItem):
|
2022-01-12 23:29:07 +00:00
|
|
|
if overlay not in self.pi_overlay.overlays:
|
|
|
|
raise RuntimeError(
|
|
|
|
f'{overlay} must be from `.plotitem_overlay()`'
|
|
|
|
)
|
|
|
|
pi = overlay
|
2020-10-19 18:18:06 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
if add_sticky:
|
|
|
|
|
2023-01-21 23:39:41 +00:00
|
|
|
if pi is not self.plotItem:
|
|
|
|
# overlay = self.pi_overlay
|
|
|
|
# assert pi in overlay.overlays
|
|
|
|
overlay = self.pi_overlay
|
|
|
|
assert pi in overlay.overlays
|
|
|
|
axis = overlay.get_axis(
|
|
|
|
pi,
|
|
|
|
add_sticky,
|
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
axis = pi.getAxis(add_sticky)
|
|
|
|
|
|
|
|
if pi.name not in axis._stickies:
|
2022-11-14 21:25:19 +00:00
|
|
|
|
|
|
|
# TODO: UGH! just make this not here! we should
|
|
|
|
# be making the sticky from code which has access
|
|
|
|
# to the ``Symbol`` instance..
|
|
|
|
|
|
|
|
# if the sticky is for our symbol
|
|
|
|
# use the tick size precision for display
|
|
|
|
name = name or pi.name
|
|
|
|
sym = self.linked.symbol
|
|
|
|
digits = None
|
|
|
|
if name == sym.key:
|
|
|
|
digits = sym.tick_size_digits
|
|
|
|
|
|
|
|
# anchor_at = ('top', 'left')
|
|
|
|
|
|
|
|
# TODO: something instead of stickies for overlays
|
|
|
|
# (we need something that avoids clutter on x-axis).
|
|
|
|
axis.add_sticky(
|
|
|
|
pi=pi,
|
2022-12-23 19:44:14 +00:00
|
|
|
fg_color='black',
|
|
|
|
# bg_color=color,
|
2022-11-14 21:25:19 +00:00
|
|
|
digits=digits,
|
|
|
|
)
|
2020-10-23 01:21:14 +00:00
|
|
|
|
2022-03-06 22:16:07 +00:00
|
|
|
# NOTE: this is more or less the RENDER call that tells Qt to
|
|
|
|
# start showing the generated graphics-curves. This is kind of
|
|
|
|
# of edge-triggered call where once added any
|
|
|
|
# ``QGraphicsItem.update()`` calls are automatically displayed.
|
|
|
|
# Our internal graphics objects have their own "update from
|
|
|
|
# data" style method API that allows for real-time updates on
|
|
|
|
# the next render cycle; just note a lot of the real-time
|
|
|
|
# updates are implicit and require a bit of digging to
|
|
|
|
# understand.
|
2022-11-14 21:25:19 +00:00
|
|
|
pi.addItem(graphics)
|
2022-03-06 22:16:07 +00:00
|
|
|
|
2022-11-29 15:56:17 +00:00
|
|
|
return viz
|
2020-06-17 15:45:43 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
def draw_ohlc(
|
2020-08-30 16:29:29 +00:00
|
|
|
self,
|
|
|
|
name: str,
|
2022-11-14 21:25:19 +00:00
|
|
|
shm: ShmArray,
|
2022-11-24 19:48:30 +00:00
|
|
|
flume: Flume,
|
2021-07-26 23:40:39 +00:00
|
|
|
|
2022-11-14 21:25:19 +00:00
|
|
|
array_key: Optional[str] = None,
|
|
|
|
**draw_curve_kwargs,
|
2021-02-08 12:00:34 +00:00
|
|
|
|
2022-11-24 20:33:58 +00:00
|
|
|
) -> Viz:
|
2022-11-14 21:25:19 +00:00
|
|
|
'''
|
|
|
|
Draw OHLC datums to chart.
|
|
|
|
|
|
|
|
'''
|
|
|
|
return self.draw_curve(
|
2022-11-24 19:48:30 +00:00
|
|
|
name,
|
|
|
|
shm,
|
|
|
|
flume,
|
2022-11-14 21:25:19 +00:00
|
|
|
array_key=array_key,
|
|
|
|
is_ohlc=True,
|
|
|
|
**draw_curve_kwargs,
|
2020-08-30 16:29:29 +00:00
|
|
|
)
|
|
|
|
|
2022-04-04 03:52:09 +00:00
|
|
|
# TODO: pretty sure we can just call the cursor
|
|
|
|
# directly not? i don't wee why we need special "signal proxies"
|
|
|
|
# for this lul..
|
2020-06-15 14:48:00 +00:00
|
|
|
def enterEvent(self, ev): # noqa
|
|
|
|
# pg.PlotWidget.enterEvent(self, ev)
|
|
|
|
self.sig_mouse_enter.emit(self)
|
|
|
|
|
|
|
|
def leaveEvent(self, ev): # noqa
|
|
|
|
# pg.PlotWidget.leaveEvent(self, ev)
|
|
|
|
self.sig_mouse_leave.emit(self)
|
|
|
|
self.scene().leaveEvent(ev)
|
|
|
|
|
2022-01-09 15:34:38 +00:00
|
|
|
def maxmin(
|
|
|
|
self,
|
|
|
|
name: Optional[str] = None,
|
2022-05-30 13:37:33 +00:00
|
|
|
bars_range: Optional[tuple[
|
|
|
|
int, int, int, int, int, int
|
|
|
|
]] = None,
|
2022-01-09 15:34:38 +00:00
|
|
|
|
|
|
|
) -> tuple[float, float]:
|
|
|
|
'''
|
|
|
|
Return the max and min y-data values "in view".
|
|
|
|
|
|
|
|
If ``bars_range`` is provided use that range.
|
|
|
|
|
|
|
|
'''
|
2022-11-24 20:33:58 +00:00
|
|
|
# TODO: here we should instead look up the ``Viz.shm.array``
|
2022-02-08 20:52:50 +00:00
|
|
|
# and read directly from shm to avoid copying to memory first
|
|
|
|
# and then reading it again here.
|
2022-11-29 15:56:17 +00:00
|
|
|
viz_key = name or self.name
|
|
|
|
viz = self._vizs.get(viz_key)
|
|
|
|
if viz is None:
|
|
|
|
log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?")
|
2023-01-14 21:11:25 +00:00
|
|
|
return 0, 0
|
|
|
|
|
|
|
|
res = viz.maxmin()
|
2022-01-09 15:34:38 +00:00
|
|
|
|
2023-01-14 21:11:25 +00:00
|
|
|
if (
|
|
|
|
res is None
|
|
|
|
):
|
|
|
|
mxmn = 0, 0
|
|
|
|
if not self._on_screen:
|
|
|
|
self.default_view(do_ds=False)
|
|
|
|
self._on_screen = True
|
2022-01-09 15:34:38 +00:00
|
|
|
else:
|
2023-01-16 04:53:57 +00:00
|
|
|
x_range, read_slc, mxmn = res
|
2022-06-27 22:22:30 +00:00
|
|
|
|
2023-01-14 21:11:25 +00:00
|
|
|
return mxmn
|
2022-11-24 20:33:58 +00:00
|
|
|
|
|
|
|
def get_viz(
|
|
|
|
self,
|
|
|
|
key: str,
|
|
|
|
) -> Viz:
|
2022-12-17 01:53:55 +00:00
|
|
|
'''
|
|
|
|
Try to get an underlying ``Viz`` by key.
|
|
|
|
|
|
|
|
'''
|
2022-11-29 15:56:17 +00:00
|
|
|
return self._vizs.get(key)
|
2022-12-17 01:53:55 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def main_viz(self) -> Viz:
|
|
|
|
return self.get_viz(self.name)
|