From 6ec0fdcabf5e521ba1d7d8dd2818a46b3b898c30 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 22 Jan 2022 14:28:14 -0500 Subject: [PATCH 01/11] Add charting support for "step curves" via `style="step"` --- piker/ui/_chart.py | 49 +++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 780262fc..a9f8d70e 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -326,8 +326,8 @@ class LinkedSplits(QWidget): # self.xaxis.hide() self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) - self.splitter.setMidLineWidth(1) - self.splitter.setHandleWidth(0) + self.splitter.setMidLineWidth(0) + self.splitter.setHandleWidth(2) self.layout = QtWidgets.QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) @@ -341,8 +341,7 @@ class LinkedSplits(QWidget): def set_split_sizes( self, - # prop: float = 0.375, # proportion allocated to consumer subcharts - prop: float = 5/8, + prop: float = 0.375, # proportion allocated to consumer subcharts ) -> None: '''Set the proportion of space allocated for linked subcharts. @@ -495,8 +494,9 @@ class LinkedSplits(QWidget): cpw.plotItem.vb.linkedsplits = self cpw.setFrameStyle( QtWidgets.QFrame.StyledPanel - # | QtWidgets.QFrame.Plain) + # | QtWidgets.QFrame.Plain ) + cpw.hideButtons() # XXX: gives us outline on backside of y-axis @@ -515,7 +515,20 @@ class LinkedSplits(QWidget): cpw.draw_ohlc(name, array, array_key=array_key) elif style == 'line': - cpw.draw_curve(name, array, array_key=array_key) + cpw.draw_curve( + name, + array, + array_key=array_key, + color='default_lightest', + ) + + elif style == 'step': + cpw.draw_curve( + name, + array, + array_key=array_key, + step_mode=True, + ) else: raise ValueError(f"Chart style {style} is currently unsupported") @@ -523,14 +536,7 @@ class LinkedSplits(QWidget): if not _is_main: # track by name self.subplots[name] = cpw - - # if sidepane: - # # TODO: use a "panes" collection to manage this? - # qframe.setMaximumWidth(self.chart.sidepane.width()) - # qframe.setMinimumWidth(self.chart.sidepane.width()) - self.splitter.addWidget(qframe) - # scale split regions self.set_split_sizes() @@ -600,7 +606,7 @@ class ChartPlotWidget(pg.PlotWidget): # parent=None, # plotItem=None, # antialias=True, - useOpenGL=True, + # useOpenGL=True, **kwargs ) self.name = name @@ -784,7 +790,7 @@ class ChartPlotWidget(pg.PlotWidget): array_key: Optional[str] = None, overlay: bool = False, - color: str = 'default_light', + color: Optional[str] = None, add_label: bool = True, **pdi_kwargs, @@ -794,6 +800,8 @@ class ChartPlotWidget(pg.PlotWidget): the input array ``data``. """ + color = color or self.pen_color or 'default_light' + _pdi_defaults = { 'pen': pg.mkPen(hcolor(color)), } @@ -944,13 +952,13 @@ class ChartPlotWidget(pg.PlotWidget): yrange: Optional[tuple[float, float]] = None, range_margin: float = 0.06, ) -> None: - """Set the viewable y-range based on embedded data. + '''Set the viewable y-range based on embedded data. This adds auto-scaling like zoom on the scroll wheel such that data always fits nicely inside the current view of the data set. - """ + ''' set_range = True if self._static_yrange == 'axis': @@ -1003,15 +1011,17 @@ class ChartPlotWidget(pg.PlotWidget): a = self._arrays['ohlc'] ifirst = a[0]['index'] bars = a[lbar - ifirst:rbar - ifirst + 1] + if not len(bars): # likely no data loaded yet or extreme scrolling? log.error(f"WTF bars_range = {lbar}:{rbar}") return if self.data_key != self.linked.symbol.key: - bars = a[self.data_key] + bars = bars[self.data_key] ylow = np.nanmin(bars) - yhigh = np.nanmax((bars)) + yhigh = np.nanmax(bars) + # print(f'{(ylow, yhigh)}') else: # just the std ohlc bars ylow = np.nanmin(bars['low']) @@ -1072,7 +1082,6 @@ class ChartPlotWidget(pg.PlotWidget): # TODO: this should go onto some sort of # data-view strimg thinger..right? ohlc = self._shm.array - # ohlc = chart._shm.array # XXX: not sure why the time is so off here # looks like we're gonna have to do some fixing.. From 2a59ccf1bb5a6bee5c5c388bd8f07f5d0d5f1e68 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 22 Jan 2022 14:40:41 -0500 Subject: [PATCH 02/11] Incrementally yield to Qt loop to resize sidepanes Since our startup is very concurrent there is often races where widgets have not fully spawned before python (re-)sizing code has a chance to run sizing logic and thus incorrect dimensions are read. Instead ensure the Qt render loop gets to run in between such checks. Also add a `open_sidepane()` mngr for creating a minimal form widget for FSP subchart sidepanes which can be configured from an input `dict`. --- piker/ui/_display.py | 135 ++++++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 60 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 7859d23b..9728380a 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -20,11 +20,10 @@ Real-time display tasks for charting / graphics. ''' from contextlib import asynccontextmanager import time -from typing import Any from types import ModuleType import numpy as np -from pydantic import BaseModel +from pydantic import create_model import tractor import trio @@ -345,45 +344,51 @@ async def fan_out_spawn_fsp_daemons( # blocks here until all fsp actors complete -class FspConfig(BaseModel): - class Config: - validate_assignment = True - - name: str - period: int - - @asynccontextmanager async def open_sidepane( linked: LinkedSplits, - display_name: str, + conf: dict[str, dict[str, str]], -) -> FspConfig: +) -> FieldsForm: + + schema = {} + + assert len(conf) == 1 # for now + + # add (single) selection widget + for display_name, config in conf.items(): + schema[display_name] = { + 'label': '**fsp**:', + 'type': 'select', + 'default_value': [display_name], + } + + # add parameters for selection "options" + defaults = config.get('params', {}) + for name, default in defaults.items(): + + # add to ORM schema + schema.update({ + name: { + 'label': f'**{name}**:', + 'type': 'edit', + 'default_value': default, + }, + }) sidepane: FieldsForm = mk_form( parent=linked.godwidget, - fields_schema={ - 'name': { - 'label': '**fsp**:', - 'type': 'select', - 'default_value': [ - f'{display_name}' - ], - }, + fields_schema=schema, + ) - # TODO: generate this from input map - 'period': { - 'label': '**period**:', - 'type': 'edit', - 'default_value': 14, - }, - }, - ) - sidepane.model = FspConfig( + # https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation + FspConfig = create_model( + 'FspConfig', name=display_name, - period=14, + **defaults, ) + sidepane.model = FspConfig() # just a logger for now until we get fsp configs up and running. async def settings_change(key: str, value: str) -> bool: @@ -410,7 +415,7 @@ async def run_fsp( src_shm: ShmArray, fsp_func_name: str, display_name: str, - conf: dict[str, Any], + conf: dict[str, dict], group_status_key: str, loglevel: str, @@ -444,7 +449,7 @@ async def run_fsp( ctx.open_stream() as stream, open_sidepane( linkedsplits, - display_name, + {display_name: conf}, ) as sidepane, ): @@ -453,9 +458,10 @@ async def run_fsp( if conf.get('overlay'): chart = linkedsplits.chart chart.draw_curve( - name='vwap', + name=display_name, data=shm.array, overlay=True, + color='default_light', ) last_val_sticky = None @@ -658,22 +664,23 @@ async def maybe_open_vlm_display( ) -> ChartPlotWidget: - # make sure that the instrument supports volume history - # (sometimes this is not the case for some commodities and - # derivatives) - # volm = ohlcv.array['volume'] - # if ( - # np.all(np.isin(volm, -1)) or - # np.all(np.isnan(volm)) - # ): if not has_vlm(ohlcv): log.warning(f"{linked.symbol.key} does not seem to have volume info") else: - async with open_sidepane(linked, 'volume') as sidepane: + async with open_sidepane( + linked, { + 'volume': { + 'params': { + 'price_func': 'ohl3' + } + } + }, + ) as sidepane: + # built-in $vlm shm = ohlcv chart = linked.add_plot( - name='vlm', + name='volume', array=shm.array, array_key='volume', @@ -681,10 +688,10 @@ async def maybe_open_vlm_display( # curve by default ohlc=False, + style='step', - # vertical bars + # vertical bars, we do this internally ourselves # stepMode=True, - # static_yrange=(0, 100), ) # XXX: ONLY for sub-chart fsps, overlays have their @@ -703,9 +710,23 @@ async def maybe_open_vlm_display( last_val_sticky.update_from_data(-1, value) + chart.update_curve_from_array( + 'volume', + shm.array, + ) + # size view to data once at outset chart._set_yrange() + # size pain to parent chart + # TODO: this appears to nearly fix a bug where the vlm sidepane + # could be sized correctly nearly immediately (since the + # order pane is already sized), right now it doesn't seem to + # fully align until the VWAP fsp-actor comes up... + await trio.sleep(0) + chart.linked.resize_sidepanes() + await trio.sleep(0) + yield chart @@ -805,20 +826,11 @@ async def display_symbol_data( fsp_conf = { 'rsi': { 'fsp_func_name': 'rsi', - 'period': 14, + 'params': {'period': 14}, 'chart_kwargs': { 'static_yrange': (0, 100), }, }, - # # test for duplicate fsps on same chart - # 'rsi2': { - # 'fsp_func_name': 'rsi', - # 'period': 14, - # 'chart_kwargs': { - # 'static_yrange': (0, 100), - # }, - # }, - } if has_vlm(ohlcv): @@ -831,8 +843,14 @@ async def display_symbol_data( }, }) - async with ( + # NOTE: we must immediately tell Qt to show the OHLC chart + # to avoid a race where the subplots get added/shown to + # the linked set *before* the main price chart! + linkedsplits.show() + linkedsplits.focus() + await trio.sleep(0) + async with ( trio.open_nursery() as ln, ): # load initial fsp chain (otherwise known as "indicators") @@ -864,10 +882,7 @@ async def display_symbol_data( ) async with ( - # XXX: this slipped in during a commits refacotr, - # it's actually landing proper in #231 - # maybe_open_vlm_display(linkedsplits, ohlcv), - + maybe_open_vlm_display(linkedsplits, ohlcv), open_order_mode( feed, chart, From 04373fd62ae1644b71f53f6f2765463cba0117c8 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 18 Sep 2021 17:10:02 -0400 Subject: [PATCH 03/11] Drop rsi from display by default --- piker/ui/_display.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 9728380a..58a14676 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -824,13 +824,13 @@ async def display_symbol_data( # TODO: eventually we'll support some kind of n-compose syntax fsp_conf = { - 'rsi': { - 'fsp_func_name': 'rsi', - 'params': {'period': 14}, - 'chart_kwargs': { - 'static_yrange': (0, 100), - }, - }, + # 'rsi': { + # 'fsp_func_name': 'rsi', + # 'params': {'period': 14}, + # 'chart_kwargs': { + # 'static_yrange': (0, 100), + # }, + # }, } if has_vlm(ohlcv): From 4f9aa0d9653603226607195935f98ef915f62a65 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 13:41:24 -0400 Subject: [PATCH 04/11] Add dynamic subplot sizing logic, passthrouh step curve colors --- piker/ui/_chart.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index a9f8d70e..74096f2b 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -341,19 +341,28 @@ class LinkedSplits(QWidget): def set_split_sizes( self, - prop: float = 0.375, # proportion allocated to consumer subcharts + prop: Optional[float] = None, ) -> None: '''Set the proportion of space allocated for linked subcharts. ''' + ln = len(self.subplots) + + if not prop: + # proportion allocated to consumer subcharts + if ln < 2: + prop = 1/(.666 * 6) + elif ln >= 2: + prop = 3/8 + major = 1 - prop - min_h_ind = int((self.height() * prop) / len(self.subplots)) + min_h_ind = int((self.height() * prop) / ln) sizes = [int(self.height() * major)] - sizes.extend([min_h_ind] * len(self.subplots)) + sizes.extend([min_h_ind] * ln) - self.splitter.setSizes(sizes) # , int(self.height()*0.2) + self.splitter.setSizes(sizes) def focus(self) -> None: if self.chart is not None: @@ -528,6 +537,8 @@ class LinkedSplits(QWidget): array, array_key=array_key, step_mode=True, + color='davies', + fill_color='davies', ) else: @@ -800,12 +811,9 @@ class ChartPlotWidget(pg.PlotWidget): the input array ``data``. """ - color = color or self.pen_color or 'default_light' - - _pdi_defaults = { - 'pen': pg.mkPen(hcolor(color)), - } - pdi_kwargs.update(_pdi_defaults) + pdi_kwargs.update({ + 'color': color or self.pen_color or 'default_light' + }) data_key = array_key or name From 216afec19cf55d8e4ca9baf7ca3114f347e60b95 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 08:48:34 -0400 Subject: [PATCH 05/11] Add test logic for range based volume curve filling --- piker/ui/_chart.py | 14 +++++++++++++- piker/ui/_display.py | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 74096f2b..a9fee069 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -959,6 +959,7 @@ class ChartPlotWidget(pg.PlotWidget): *, yrange: Optional[tuple[float, float]] = None, range_margin: float = 0.06, + bars_range: Optional[tuple[int, int, int, int]] = None ) -> None: '''Set the viewable y-range based on embedded data. @@ -982,7 +983,18 @@ class ChartPlotWidget(pg.PlotWidget): # Determine max, min y values in viewable x-range from data. # Make sure min bars/datums on screen is adhered. - l, lbar, rbar, r = self.bars_range() + l, lbar, rbar, r = bars_range or self.bars_range() + + if self.name != 'volume': + vlm_chart = self.linked.subplots.get('volume') + if vlm_chart: + vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r)) + curve = vlm_chart._graphics['volume'] + # if rbar - lbar < 1500: + # # print('small range') + # curve._fill = True + # else: + # curve._fill = False # figure out x-range in view such that user can scroll "off" # the data set up to the point where ``_min_points_to_show`` diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 58a14676..64f2d487 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -19,8 +19,10 @@ Real-time display tasks for charting / graphics. ''' from contextlib import asynccontextmanager +from pprint import pformat import time from types import ModuleType +from typing import Optional import numpy as np from pydantic import create_model @@ -61,6 +63,7 @@ async def chart_from_quotes( stream: tractor.MsgStream, ohlcv: np.ndarray, wap_in_history: bool = False, + vlm_chart: Optional[ChartPlotWidget] = None, ) -> None: '''The 'main' (price) chart real-time update loop. @@ -149,7 +152,7 @@ async def chart_from_quotes( for tick in quote.get('ticks', ()): - # print(f"CHART: {quote['symbol']}: {tick}") + # log.info(f"quotes: {pformat(quote['symbol'])}: {pformat(tick)}") ticktype = tick.get('type') price = tick.get('price') size = tick.get('size') @@ -190,6 +193,10 @@ async def chart_from_quotes( # update vwap overlay line chart.update_curve_from_array('bar_wap', ohlcv.array) + if vlm_chart: + print(f"volume: {end['volume']}") + vlm_chart.update_curve_from_array('volume', ohlcv.array) + # l1 book events # throttle the book graphics updates at a lower rate # since they aren't as critical for a manual user @@ -666,6 +673,8 @@ async def maybe_open_vlm_display( if not has_vlm(ohlcv): log.warning(f"{linked.symbol.key} does not seem to have volume info") + yield + return else: async with open_sidepane( linked, { @@ -852,6 +861,7 @@ async def display_symbol_data( async with ( trio.open_nursery() as ln, + maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart, ): # load initial fsp chain (otherwise known as "indicators") ln.start_soon( @@ -872,6 +882,7 @@ async def display_symbol_data( feed.stream, ohlcv, wap_in_history, + vlm_chart, ) ln.start_soon( @@ -882,7 +893,6 @@ async def display_symbol_data( ) async with ( - maybe_open_vlm_display(linkedsplits, ohlcv), open_order_mode( feed, chart, From ea5b55945f77fb177f56ec349ec878c3074d9de1 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 13:42:54 -0400 Subject: [PATCH 06/11] Re-order grays by "lightness" --- piker/ui/_style.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index c1f151f4..34b5cb01 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -205,19 +205,26 @@ def hcolor(name: str) -> str: 'svags': '#0a0e14', # fifty shades + 'original': '#a9a9a9', 'gray': '#808080', # like the kick 'grayer': '#4c4c4c', 'grayest': '#3f3f3f', - 'i3': '#494D4F', - 'jet': '#343434', 'cadet': '#91A3B0', 'marengo': '#91A3B0', - 'charcoal': '#36454F', 'gunmetal': '#91A3B0', 'battleship': '#848482', - 'davies': '#555555', + + # bluish + 'charcoal': '#36454F', + + # default bars 'bracket': '#666666', # like the logo - 'original': '#a9a9a9', + + # work well for filled polygons which want a 'bracket' feel + # going light to dark + 'davies': '#555555', + 'i3': '#494D4F', + 'jet': '#343434', # from ``qdarkstyle`` palette 'default_darkest': DarkPalette.COLOR_BACKGROUND_1, From 40c874ce92e36052eded3515c8e9c8a531a5b4fc Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 08:13:57 -0400 Subject: [PATCH 07/11] Pass curve color through to y sticky label --- piker/ui/_chart.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index a9fee069..ea9eaa8c 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -528,7 +528,7 @@ class LinkedSplits(QWidget): name, array, array_key=array_key, - color='default_lightest', + color='default_light', ) elif style == 'step': @@ -789,7 +789,7 @@ class ChartPlotWidget(pg.PlotWidget): update_func=ContentsLabel.update_from_ohlc, ) - self._add_sticky(name) + self._add_sticky(name, bg_color='davies') return graphics @@ -863,7 +863,7 @@ class ChartPlotWidget(pg.PlotWidget): # TODO: something instead of stickies for overlays # (we need something that avoids clutter on x-axis). - self._add_sticky(name, bg_color='default_light') + self._add_sticky(name, bg_color=color) if self.linked.cursor: self.linked.cursor.add_curve_cursor(self, curve) @@ -877,6 +877,7 @@ class ChartPlotWidget(pg.PlotWidget): return curve + # TODO: make this a ctx mngr def _add_sticky( self, From 39fb2ee85d9c72bd38c3bc91faa8c539c7f64e18 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 15:25:36 -0400 Subject: [PATCH 08/11] Clean up some imports, shift around some commented code --- piker/ui/_chart.py | 82 ++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index ea9eaa8c..e4236aba 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -25,6 +25,9 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QFrame, QWidget, + QHBoxLayout, + QVBoxLayout, + QSplitter, # QSizePolicy, ) import numpy as np @@ -64,11 +67,11 @@ log = get_logger(__name__) class GodWidget(QWidget): ''' "Our lord and savior, the holy child of window-shua, there is no - widget above thee." - 6|6 + widget above thee." - 6||6 The highest level composed widget which contains layouts for - organizing lower level charts as well as other widgets used to - control or modify them. + organizing charts as well as other sub-widgets used to control or + modify them. ''' def __init__( @@ -80,19 +83,19 @@ class GodWidget(QWidget): super().__init__(parent) - self.hbox = QtWidgets.QHBoxLayout(self) + self.hbox = QHBoxLayout(self) self.hbox.setContentsMargins(0, 0, 0, 0) self.hbox.setSpacing(6) self.hbox.setAlignment(Qt.AlignTop) - self.vbox = QtWidgets.QVBoxLayout() + self.vbox = QVBoxLayout() self.vbox.setContentsMargins(0, 0, 0, 0) self.vbox.setSpacing(2) self.vbox.setAlignment(Qt.AlignTop) self.hbox.addLayout(self.vbox) - # self.toolbar_layout = QtWidgets.QHBoxLayout() + # self.toolbar_layout = QHBoxLayout() # self.toolbar_layout.setContentsMargins(0, 0, 0, 0) # self.vbox.addLayout(self.toolbar_layout) @@ -106,25 +109,8 @@ class GodWidget(QWidget): # assigned in the startup func `_async_main()` self._root_n: trio.Nursery = None - def set_chart_symbol( - self, - symbol_key: str, # of form . - linkedsplits: 'LinkedSplits', # type: ignore - - ) -> None: - # re-sort org cache symbol list in LIFO order - cache = self._chart_cache - cache.pop(symbol_key, None) - cache[symbol_key] = linkedsplits - - def get_chart_symbol( - self, - symbol_key: str, - ) -> 'LinkedSplits': # type: ignore - return self._chart_cache.get(symbol_key) - # def init_timeframes_ui(self): - # self.tf_layout = QtWidgets.QHBoxLayout() + # self.tf_layout = QHBoxLayout() # self.tf_layout.setSpacing(0) # self.tf_layout.setContentsMargins(0, 12, 0, 0) # time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN') @@ -145,6 +131,23 @@ class GodWidget(QWidget): # self.strategy_box = StrategyBoxWidget(self) # self.toolbar_layout.addWidget(self.strategy_box) + def set_chart_symbol( + self, + symbol_key: str, # of form . + linkedsplits: 'LinkedSplits', # type: ignore + + ) -> None: + # re-sort org cache symbol list in LIFO order + cache = self._chart_cache + cache.pop(symbol_key, None) + cache[symbol_key] = linkedsplits + + def get_chart_symbol( + self, + symbol_key: str, + ) -> 'LinkedSplits': # type: ignore + return self._chart_cache.get(symbol_key) + async def load_symbol( self, @@ -255,7 +258,7 @@ class ChartnPane(QFrame): ''' sidepane: FieldsForm - hbox: QtWidgets.QHBoxLayout + hbox: QHBoxLayout chart: Optional['ChartPlotWidget'] = None def __init__( @@ -271,7 +274,7 @@ class ChartnPane(QFrame): self.sidepane = sidepane self.chart = None - hbox = self.hbox = QtWidgets.QHBoxLayout(self) + hbox = self.hbox = QHBoxLayout(self) hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft) hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(3) @@ -281,21 +284,14 @@ class ChartnPane(QFrame): class LinkedSplits(QWidget): ''' - Widget that holds a central chart plus derived - subcharts computed from the original data set apart - by splitters for resizing. + Composite that holds a central chart plus a set of (derived) + subcharts (usually computed from the original data) arranged in + a splitter for resizing. A single internal references to the data is maintained for each chart and can be updated externally. ''' - long_pen = pg.mkPen('#006000') - long_brush = pg.mkBrush('#00ff00') - short_pen = pg.mkPen('#600000') - short_brush = pg.mkBrush('#ff0000') - - zoomIsDisabled = QtCore.pyqtSignal(bool) - def __init__( self, @@ -325,11 +321,11 @@ class LinkedSplits(QWidget): # self.xaxis_ind.setStyle(showValues=False) # self.xaxis.hide() - self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) + self.splitter = QSplitter(QtCore.Qt.Vertical) self.splitter.setMidLineWidth(0) self.splitter.setHandleWidth(2) - self.layout = QtWidgets.QVBoxLayout(self) + self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.splitter) @@ -749,6 +745,7 @@ class ChartPlotWidget(pg.PlotWidget): self._vb.setXRange( min=l + 1, max=r + 1, + # TODO: holy shit, wtf dude... why tf would this not be 0 by # default... speechless. padding=0, @@ -817,8 +814,11 @@ class ChartPlotWidget(pg.PlotWidget): data_key = array_key or name + # pg internals for reference. # curve = pg.PlotDataItem( # curve = pg.PlotCurveItem( + + # yah, we wrote our own B) curve = FastAppendCurve( y=data[data_key], x=data['index'], @@ -958,9 +958,11 @@ class ChartPlotWidget(pg.PlotWidget): def _set_yrange( self, *, + yrange: Optional[tuple[float, float]] = None, range_margin: float = 0.06, bars_range: Optional[tuple[int, int, int, int]] = None + ) -> None: '''Set the viewable y-range based on embedded data. @@ -986,11 +988,13 @@ class ChartPlotWidget(pg.PlotWidget): l, lbar, rbar, r = bars_range or self.bars_range() + # TODO: we need a loop for auto-scaled subplots to all + # be triggered by one another if self.name != 'volume': vlm_chart = self.linked.subplots.get('volume') if vlm_chart: vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r)) - curve = vlm_chart._graphics['volume'] + # curve = vlm_chart._graphics['volume'] # if rbar - lbar < 1500: # # print('small range') # curve._fill = True From dbdd7b64974c1a562df83fc07c5ccbe50e56e460 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 26 Sep 2021 17:14:30 -0400 Subject: [PATCH 09/11] Fix color passthrough, make overlays a `dict` --- piker/ui/_chart.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index e4236aba..c564d3de 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -56,6 +56,7 @@ from ._style import ( ) from ..data.feed import Feed from ..data._source import Symbol +from ..data._sharedmem import ShmArray from ..log import get_logger from ._interaction import ChartView from ._forms import FieldsForm @@ -632,7 +633,8 @@ class ChartPlotWidget(pg.PlotWidget): 'ohlc': array, } self._graphics = {} # registry of underlying graphics - self._overlays = set() # registry of overlay curve names + # registry of overlay curve names + self._overlays: dict[str, ShmArray] = {} self._feeds: dict[Symbol, Feed] = {} @@ -808,8 +810,9 @@ class ChartPlotWidget(pg.PlotWidget): the input array ``data``. """ + color = color or self.pen_color or 'default_light' pdi_kwargs.update({ - 'color': color or self.pen_color or 'default_light' + 'color': color }) data_key = array_key or name @@ -856,7 +859,7 @@ class ChartPlotWidget(pg.PlotWidget): if overlay: anchor_at = ('bottom', 'left') - self._overlays.add(name) + self._overlays[name] = None else: anchor_at = ('top', 'left') From dd752927a2f62bd761a6f4bf9a04be0c1e53732d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 08:14:22 -0400 Subject: [PATCH 10/11] Update vlm sticky --- piker/ui/_display.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 64f2d487..287ef4e4 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -19,7 +19,7 @@ Real-time display tasks for charting / graphics. ''' from contextlib import asynccontextmanager -from pprint import pformat +# from pprint import pformat import time from types import ModuleType from typing import Optional @@ -57,11 +57,12 @@ _clear_throttle_rate: int = 58 # Hz _book_throttle_rate: int = 16 # Hz -async def chart_from_quotes( +async def update_chart_from_quotes( chart: ChartPlotWidget, stream: tractor.MsgStream, ohlcv: np.ndarray, + wap_in_history: bool = False, vlm_chart: Optional[ChartPlotWidget] = None, @@ -78,7 +79,8 @@ async def chart_from_quotes( # - handle odd lot orders # - update last open price correctly instead # of copying it from last bar's close - # - 5 sec bar lookback-autocorrection like tws does? + # - 1-5 sec bar lookback-autocorrection like tws does? + # (would require a background history checker task) # update last price sticky last_price_sticky = chart._ysticks[chart.name] @@ -86,6 +88,9 @@ async def chart_from_quotes( *ohlcv.array[-1][['index', 'close']] ) + if vlm_chart: + vlm_sticky = vlm_chart._ysticks['volume'] + def maxmin(): # TODO: implement this # https://arxiv.org/abs/cs/0610046 @@ -127,11 +132,11 @@ async def chart_from_quotes( # TODO: # - in theory we should be able to read buffer data faster - # then msgs arrive.. needs some tinkering and testing + # then msgs arrive.. needs some tinkering and testing # - if trade volume jumps above / below prior L1 price - # levels this might be dark volume we need to - # present differently? + # levels this might be dark volume we need to + # present differently -> likely dark vlm tick_size = chart.linked.symbol.tick_size tick_margin = 2 * tick_size @@ -152,7 +157,8 @@ async def chart_from_quotes( for tick in quote.get('ticks', ()): - # log.info(f"quotes: {pformat(quote['symbol'])}: {pformat(tick)}") + # log.info( + # f"quotes: {pformat(quote['symbol'])}: {pformat(tick)}") ticktype = tick.get('type') price = tick.get('price') size = tick.get('size') @@ -193,9 +199,14 @@ async def chart_from_quotes( # update vwap overlay line chart.update_curve_from_array('bar_wap', ohlcv.array) + # TODO: show dark trades differently + # https://github.com/pikers/piker/issues/116 if vlm_chart: - print(f"volume: {end['volume']}") - vlm_chart.update_curve_from_array('volume', ohlcv.array) + # print(f"volume: {end['volume']}") + vlm_chart.update_curve_from_array( + 'volume', ohlcv.array + ) + vlm_sticky.update_from_data(*end[['index', 'volume']]) # l1 book events # throttle the book graphics updates at a lower rate @@ -697,9 +708,13 @@ async def maybe_open_vlm_display( # curve by default ohlc=False, + + # Draw vertical bars from zero. + # we do this internally ourselves since + # the curve item internals are pretty convoluted. style='step', - # vertical bars, we do this internally ourselves + # original pyqtgraph flag for reference # stepMode=True, ) @@ -877,7 +892,7 @@ async def display_symbol_data( # start graphics update loop(s)after receiving first live quote ln.start_soon( - chart_from_quotes, + update_chart_from_quotes, chart, feed.stream, ohlcv, From 37eeb0d74b6c381852428f29554a938f31dd288b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 09:35:37 -0400 Subject: [PATCH 11/11] Resize volume yaxis to in view range --- piker/ui/_display.py | 74 ++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 287ef4e4..2b825bea 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -101,7 +101,7 @@ async def update_chart_from_quotes( last_bars_range = chart.bars_range() l, lbar, rbar, r = last_bars_range - in_view = array[lbar - ifirst:rbar - ifirst] + in_view = array[lbar - ifirst:rbar - ifirst + 1] assert in_view.size @@ -112,11 +112,20 @@ async def update_chart_from_quotes( # sym = chart.name # mx, mn = np.nanmax(in_view[sym]), np.nanmin(in_view[sym]) - return last_bars_range, mx, max(mn, 0) + mx_vlm_in_view = 0 + if vlm_chart: + mx_vlm_in_view = np.max(in_view['volume']) + + return last_bars_range, mx, max(mn, 0), mx_vlm_in_view chart.default_view() - last_bars_range, last_mx, last_mn = maxmin() + ( + last_bars_range, + last_mx, + last_mn, + last_mx_vlm, + ) = maxmin() last, volume = ohlcv.array[-1][['close', 'volume']] @@ -139,7 +148,7 @@ async def update_chart_from_quotes( # present differently -> likely dark vlm tick_size = chart.linked.symbol.tick_size - tick_margin = 2 * tick_size + tick_margin = 3 * tick_size last_ask = last_bid = last_clear = time.time() chart.show() @@ -155,6 +164,36 @@ async def update_chart_from_quotes( now = time.time() + # brange, mx_in_view, mn_in_view = maxmin() + ( + brange, + mx_in_view, + mn_in_view, + mx_vlm_in_view, + ) = maxmin() + l, lbar, rbar, r = brange + mx = mx_in_view + tick_margin + mn = mn_in_view - tick_margin + + # NOTE: vlm may be written by the ``brokerd`` backend + # event though a tick sample is not emitted. + # TODO: show dark trades differently + # https://github.com/pikers/piker/issues/116 + array = ohlcv.array + + if vlm_chart: + # print(f"volume: {end['volume']}") + vlm_chart.update_curve_from_array('volume', array) + vlm_sticky.update_from_data(*array[-1][['index', 'volume']]) + + if ( + mx_vlm_in_view != last_mx_vlm or + mx_vlm_in_view > last_mx_vlm + ): + # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') + vlm_chart._set_yrange(yrange=(0, mx_vlm_in_view * 1.375)) + last_mx_vlm = mx_vlm_in_view + for tick in quote.get('ticks', ()): # log.info( @@ -180,16 +219,13 @@ async def update_chart_from_quotes( # set time of last graphics update last_clear = now - array = ohlcv.array - # update price sticky(s) end = array[-1] last_price_sticky.update_from_data( *end[['index', 'close']] ) - # plot bars - # update price bar + # update ohlc sampled price bars chart.update_ohlc_from_array( chart.name, array, @@ -199,15 +235,6 @@ async def update_chart_from_quotes( # update vwap overlay line chart.update_curve_from_array('bar_wap', ohlcv.array) - # TODO: show dark trades differently - # https://github.com/pikers/piker/issues/116 - if vlm_chart: - # print(f"volume: {end['volume']}") - vlm_chart.update_curve_from_array( - 'volume', ohlcv.array - ) - vlm_sticky.update_from_data(*end[['index', 'volume']]) - # l1 book events # throttle the book graphics updates at a lower rate # since they aren't as critical for a manual user @@ -231,11 +258,6 @@ async def update_chart_from_quotes( # compute max and min trade values to display in view # TODO: we need a streaming minmax algorithm here, see # def above. - brange, mx_in_view, mn_in_view = maxmin() - l, lbar, rbar, r = brange - - mx = mx_in_view + tick_margin - mn = mn_in_view - tick_margin # XXX: prettty sure this is correct? # if ticktype in ('trade', 'last'): @@ -259,16 +281,14 @@ async def update_chart_from_quotes( elif ticktype in ('bid', 'bsize'): l1.bid_label.update_fields({'level': price, 'size': size}) - # update min price in view to keep bid on screen - mn = min(price - tick_margin, mn) - # update max price in view to keep ask on screen + # in view y-range checking for auto-scale + # update the max/min price in view to keep bid/ask on screen mx = max(price + tick_margin, mx) - + mn = min(price - tick_margin, mn) if (mx > last_mx) or ( mn < last_mn ): # print(f'new y range: {(mn, mx)}') - chart._set_yrange( yrange=(mn, mx), # TODO: we should probably scale