From 553f00175798b43097314db126a9bffa3787e75e Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 18 Sep 2021 15:14:09 -0400 Subject: [PATCH] Add volume plot as default Toss in support for a "step mode" curve (unfinished atm) and use it to plot from the `volume` field of the ohlcv shm array (if available). changes to make it happen, - dynamically generate the fsp sidepane form from an input config `dict` |_ dynamically generate the underlying `pydantic` model |_ - add a "volume checker" helper func that inspects the shm array - toss in sidepane resize calls to avoid race where the ohlcv array is plotted too slowly compared to the volume and the chart somehow doesn't show.. - drop duplicate rsi2 cruft (previously used to test plots of the shm data) --- piker/ui/_chart.py | 49 +++++++++------- piker/ui/_display.py | 131 ++++++++++++++++++++++++------------------- 2 files changed, 103 insertions(+), 77 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.. diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 254fee76..1c3d3495 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( open_sidepane( linkedsplits, - display_name, + {display_name: conf}, ) as sidepane, ): @@ -457,9 +462,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 @@ -662,22 +668,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', @@ -685,10 +692,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 @@ -707,9 +714,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 @@ -809,20 +830,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): @@ -835,8 +847,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") @@ -869,7 +887,6 @@ async def display_symbol_data( async with ( maybe_open_vlm_display(linkedsplits, ohlcv), - open_order_mode( feed, chart,