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
parent
2564acea1b
commit
30656eda39
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue