Use a `DisplayState` in the graphics update loop

The graphics update loop is much easier to grok when all the UI
components which potentially need to be updated on a cycle are arranged
together in a high-level composite namespace, thus this new
`DisplayState` addition. Create and set this state on each
`LinkedSplits` chart set and add a new method `.graphics_cycle()` which
let's a caller trigger a graphics loop update manually. Use this method
in the fsp graphics manager such that a chain can update new history
output even if there is no real-time feed driving the display loop (eg.
when a market is "closed").
m4_corrections
Tyler Goodlet 2022-03-07 11:08:04 -05:00
parent 6d54cf1d7d
commit 37b492eba6
3 changed files with 76 additions and 55 deletions

View File

@ -19,7 +19,7 @@ High level chart-widget apis.
''' '''
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, TYPE_CHECKING
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -63,6 +63,8 @@ from ._interaction import ChartView
from ._forms import FieldsForm from ._forms import FieldsForm
from ._overlay import PlotItemOverlay from ._overlay import PlotItemOverlay
if TYPE_CHECKING:
from ._display import DisplayState
log = get_logger(__name__) log = get_logger(__name__)
@ -230,6 +232,7 @@ class GodWidget(QWidget):
# chart is already in memory so just focus it # chart is already in memory so just focus it
linkedsplits.show() linkedsplits.show()
linkedsplits.focus() linkedsplits.focus()
linkedsplits.graphics_cycle()
await trio.sleep(0) await trio.sleep(0)
# resume feeds *after* rendering chart view asap # resume feeds *after* rendering chart view asap
@ -349,10 +352,14 @@ class LinkedSplits(QWidget):
# chart-local graphics state that can be passed to # chart-local graphics state that can be passed to
# a ``graphic_update_cycle()`` call by any task wishing to # a ``graphic_update_cycle()`` call by any task wishing to
# update the UI for a given "chart instance". # update the UI for a given "chart instance".
self.display_state: dict[str, dict] = {} self.display_state: Optional[DisplayState] = None
self._symbol: Symbol = None self._symbol: Symbol = None
def graphics_cycle(self) -> None:
from . import _display
return _display.graphics_update_cycle(self.display_state)
@property @property
def symbol(self) -> Symbol: def symbol(self) -> Symbol:
return self._symbol return self._symbol

View File

@ -21,9 +21,10 @@ this module ties together quote and computational (fsp) streams with
graphics update methods via our custom ``pyqtgraph`` charting api. graphics update methods via our custom ``pyqtgraph`` charting api.
''' '''
from dataclasses import dataclass
from functools import partial from functools import partial
import time import time
from typing import Optional, Any from typing import Optional, Any, Callable
import numpy as np import numpy as np
import tractor import tractor
@ -31,6 +32,7 @@ import trio
from .. import brokers from .. import brokers
from ..data.feed import open_feed from ..data.feed import open_feed
from ._axes import YAxisLabel
from ._chart import ( from ._chart import (
ChartPlotWidget, ChartPlotWidget,
LinkedSplits, LinkedSplits,
@ -109,6 +111,33 @@ def chart_maxmin(
return last_bars_range, mx, max(mn, 0), mx_vlm_in_view 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( async def graphics_update_loop(
linked: LinkedSplits, linked: LinkedSplits,
@ -209,7 +238,7 @@ async def graphics_update_loop(
# async for quotes in iter_drain_quotes(): # async for quotes in iter_drain_quotes():
ds = linked.display_state = { ds = linked.display_state = DisplayState(**{
'quotes': {}, 'quotes': {},
'linked': linked, 'linked': linked,
'maxmin': maxmin, 'maxmin': maxmin,
@ -227,12 +256,12 @@ async def graphics_update_loop(
'last_mx': last_mx, 'last_mx': last_mx,
'last_mn': last_mn, 'last_mn': last_mn,
} }
} })
# main loop # main loop
async for quotes in stream: async for quotes in stream:
ds['quotes'] = quotes ds.quotes = quotes
quote_period = time.time() - last_quote quote_period = time.time() - last_quote
quote_rate = round( quote_rate = round(
@ -255,36 +284,30 @@ async def graphics_update_loop(
continue continue
# sync call to update all graphics/UX components. # sync call to update all graphics/UX components.
graphics_update_cycle(**ds) graphics_update_cycle(ds)
def graphics_update_cycle( def graphics_update_cycle(
quotes, ds: DisplayState,
linked,
maxmin,
ohlcv,
chart,
last_price_sticky,
vlm_chart,
vlm_sticky,
l1,
vars: dict[str, Any],
wap_in_history: bool = False, wap_in_history: bool = False,
) -> None: ) -> 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'] tick_margin = vars['tick_margin']
for sym, quote in quotes.items(): for sym, quote in ds.quotes.items():
brange, mx_in_view, mn_in_view, mx_vlm_in_view = ds.maxmin()
(
brange,
mx_in_view,
mn_in_view,
mx_vlm_in_view,
) = maxmin()
l, lbar, rbar, r = brange l, lbar, rbar, r = brange
mx = mx_in_view + tick_margin mx = mx_in_view + tick_margin
mn = mn_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. # event though a tick sample is not emitted.
# TODO: show dark trades differently # TODO: show dark trades differently
# https://github.com/pikers/piker/issues/116 # https://github.com/pikers/piker/issues/116
array = ohlcv.array
# NOTE: this used to be implemented in a dedicated # NOTE: this used to be implemented in a dedicated
# "increment tas": ``check_for_new_bars()`` but it doesn't # "increment tas": ``check_for_new_bars()`` but it doesn't
@ -313,11 +335,11 @@ def graphics_update_cycle(
if vlm_chart: if vlm_chart:
vlm_chart.update_curve_from_array('volume', array) 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 ( 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}') # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
vlm_chart.view._set_yrange( vlm_chart.view._set_yrange(
@ -382,6 +404,12 @@ def graphics_update_cycle(
# last_clear_updated: bool = False # last_clear_updated: bool = False
# for typ, tick in reversed(lasts.items()): # 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 # iterate in FIFO order per frame
for typ, tick in lasts.items(): for typ, tick in lasts.items():
@ -410,19 +438,16 @@ def graphics_update_cycle(
# update price sticky(s) # update price sticky(s)
end = array[-1] end = array[-1]
last_price_sticky.update_from_data( ds.last_price_sticky.update_from_data(
*end[['index', 'close']] *end[['index', 'close']]
) )
# update ohlc sampled price bars
chart.update_ohlc_from_array(
chart.name,
array,
)
if wap_in_history: if wap_in_history:
# update vwap overlay line # 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 # L1 book label-line updates
# XXX: is this correct for ib? # XXX: is this correct for ib?
@ -436,7 +461,9 @@ def graphics_update_cycle(
}.get(price) }.get(price)
if label is not None: 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 # TODO: on trades should we be knocking down
# the relevant L1 queue? # the relevant L1 queue?
@ -469,7 +496,7 @@ def graphics_update_cycle(
vars['last_mx'], vars['last_mn'] = mx, mn vars['last_mx'], vars['last_mn'] = mx, mn
# run synchronous update on all derived fsp subplots # 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( update_fsp_chart(
subchart, subchart,
subchart._shm, 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( async def display_symbol_data(
godwidget: GodWidget, godwidget: GodWidget,
provider: str, provider: str,

View File

@ -442,7 +442,7 @@ class FspAdmin:
async with stream.subscribe() as stream: async with stream.subscribe() as stream:
async for msg in stream: async for msg in stream:
if msg == 'update': if msg == 'update':
_display.trigger_update(self.linked) self.linked.graphics_cycle()
await complete.wait() await complete.wait()