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").
offline_history_loading
Tyler Goodlet 2022-03-07 11:08:04 -05:00
parent 2564acea1b
commit 30656eda39
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 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

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.
'''
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,

View File

@ -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()