diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 2f000e8a..d3774b7e 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -19,7 +19,7 @@ High level chart-widget apis. ''' from __future__ import annotations -from typing import Optional +from typing import Optional, TYPE_CHECKING from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import Qt @@ -63,6 +63,8 @@ from ._interaction import ChartView from ._forms import FieldsForm from ._overlay import PlotItemOverlay +if TYPE_CHECKING: + from ._display import DisplayState log = get_logger(__name__) @@ -230,6 +232,7 @@ class GodWidget(QWidget): # chart is already in memory so just focus it linkedsplits.show() linkedsplits.focus() + linkedsplits.graphics_cycle() await trio.sleep(0) # resume feeds *after* rendering chart view asap @@ -349,10 +352,14 @@ class LinkedSplits(QWidget): # 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". - self.display_state: dict[str, dict] = {} + self.display_state: Optional[DisplayState] = None self._symbol: Symbol = None + def graphics_cycle(self) -> None: + from . import _display + return _display.graphics_update_cycle(self.display_state) + @property def symbol(self) -> Symbol: return self._symbol diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 04d08fc4..ae6d0c77 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -21,9 +21,10 @@ this module ties together quote and computational (fsp) streams with graphics update methods via our custom ``pyqtgraph`` charting api. ''' +from dataclasses import dataclass from functools import partial import time -from typing import Optional, Any +from typing import Optional, Any, Callable import numpy as np import tractor @@ -31,6 +32,7 @@ import trio from .. import brokers from ..data.feed import open_feed +from ._axes import YAxisLabel from ._chart import ( ChartPlotWidget, LinkedSplits, @@ -109,6 +111,33 @@ def chart_maxmin( return last_bars_range, mx, max(mn, 0), mx_vlm_in_view +@dataclass +class DisplayState: + ''' + Chart-local real-time graphics state container. + + ''' + quotes: dict[str, Any] + + maxmin: Callable + ohlcv: ShmArray + + # high level chart handles + linked: LinkedSplits + chart: ChartPlotWidget + vlm_chart: ChartPlotWidget + + # axis labels + l1: L1Labels + last_price_sticky: YAxisLabel + vlm_sticky: YAxisLabel + + # misc state tracking + vars: dict[str, Any] + + wap_in_history: bool = False + + async def graphics_update_loop( linked: LinkedSplits, @@ -209,7 +238,7 @@ async def graphics_update_loop( # async for quotes in iter_drain_quotes(): - ds = linked.display_state = { + ds = linked.display_state = DisplayState(**{ 'quotes': {}, 'linked': linked, 'maxmin': maxmin, @@ -227,12 +256,12 @@ async def graphics_update_loop( 'last_mx': last_mx, 'last_mn': last_mn, } - } + }) # main loop async for quotes in stream: - ds['quotes'] = quotes + ds.quotes = quotes quote_period = time.time() - last_quote quote_rate = round( @@ -255,36 +284,30 @@ async def graphics_update_loop( continue # sync call to update all graphics/UX components. - graphics_update_cycle(**ds) + graphics_update_cycle(ds) def graphics_update_cycle( - quotes, - linked, - maxmin, - ohlcv, - chart, - last_price_sticky, - vlm_chart, - vlm_sticky, - l1, - - vars: dict[str, Any], - + ds: DisplayState, wap_in_history: bool = False, ) -> None: + # TODO: eventually optimize this whole graphics stack with ``numba`` + # hopefully XD + + # unpack multi-referenced components + chart = ds.chart + vlm_chart = ds.vlm_chart + l1 = ds.l1 + + ohlcv = ds.ohlcv + array = ohlcv.array + vars = ds.vars tick_margin = vars['tick_margin'] - for sym, quote in quotes.items(): - - ( - brange, - mx_in_view, - mn_in_view, - mx_vlm_in_view, - ) = maxmin() + for sym, quote in ds.quotes.items(): + brange, mx_in_view, mn_in_view, mx_vlm_in_view = ds.maxmin() l, lbar, rbar, r = brange mx = mx_in_view + tick_margin mn = mn_in_view - tick_margin @@ -293,7 +316,6 @@ def graphics_update_cycle( # event though a tick sample is not emitted. # TODO: show dark trades differently # https://github.com/pikers/piker/issues/116 - array = ohlcv.array # NOTE: this used to be implemented in a dedicated # "increment tas": ``check_for_new_bars()`` but it doesn't @@ -313,11 +335,11 @@ def graphics_update_cycle( if vlm_chart: vlm_chart.update_curve_from_array('volume', array) - vlm_sticky.update_from_data(*array[-1][['index', 'volume']]) + ds.vlm_sticky.update_from_data(*array[-1][['index', 'volume']]) if ( - mx_vlm_in_view != vars['last_mx_vlm'] or - mx_vlm_in_view > vars['last_mx_vlm'] + mx_vlm_in_view != vars['last_mx_vlm'] + or mx_vlm_in_view > vars['last_mx_vlm'] ): # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') vlm_chart.view._set_yrange( @@ -382,6 +404,12 @@ def graphics_update_cycle( # last_clear_updated: bool = False # for typ, tick in reversed(lasts.items()): + # update ohlc sampled price bars + chart.update_ohlc_from_array( + chart.name, + array, + ) + # iterate in FIFO order per frame for typ, tick in lasts.items(): @@ -410,19 +438,16 @@ def graphics_update_cycle( # update price sticky(s) end = array[-1] - last_price_sticky.update_from_data( + ds.last_price_sticky.update_from_data( *end[['index', 'close']] ) - # update ohlc sampled price bars - chart.update_ohlc_from_array( - chart.name, - array, - ) - if wap_in_history: # update vwap overlay line - chart.update_curve_from_array('bar_wap', ohlcv.array) + chart.update_curve_from_array( + 'bar_wap', + array, + ) # L1 book label-line updates # XXX: is this correct for ib? @@ -436,7 +461,9 @@ def graphics_update_cycle( }.get(price) if label is not None: - label.update_fields({'level': price, 'size': size}) + label.update_fields( + {'level': price, 'size': size} + ) # TODO: on trades should we be knocking down # the relevant L1 queue? @@ -469,7 +496,7 @@ def graphics_update_cycle( vars['last_mx'], vars['last_mn'] = mx, mn # run synchronous update on all derived fsp subplots - for name, subchart in linked.subplots.items(): + for name, subchart in ds.linked.subplots.items(): update_fsp_chart( subchart, subchart._shm, @@ -492,19 +519,6 @@ def graphics_update_cycle( ) -def trigger_update( - linked: LinkedSplits, - -) -> None: - ''' - Manually trigger a graphics update from global state. - - Generally used from remote actors who wish to trigger a UI refresh. - - ''' - graphics_update_cycle(**linked.display_state) - - async def display_symbol_data( godwidget: GodWidget, provider: str, diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index 41572a58..678b5d8e 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -442,7 +442,7 @@ class FspAdmin: async with stream.subscribe() as stream: async for msg in stream: if msg == 'update': - _display.trigger_update(self.linked) + self.linked.graphics_cycle() await complete.wait()