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").mkts_backup
							parent
							
								
									c60d523428
								
							
						
					
					
						commit
						692e310a98
					
				| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -443,7 +443,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