Factor setup loop, 1 FSP chain, colors, throttling

Factor out the chart widget creation since it's only executed once
during rendering of the first feed/flow whilst keeping plotitem overlay
creation inside the (flume oriented) init loop. Only create one vlm and
FSP chart/chain for now until we figure out if we want FSPs overlayed by
default or selected based on the "front" symbol in use. Add a default
color-palette set using shades of gray when plotting overlays. Presume
that the display loop's quote throttle rate should be uniformly
distributed over all input symbol-feeds for now. Restore feed pausing on
mouse interaction.
multi_symbol_input
Tyler Goodlet 2022-11-16 17:33:34 -05:00
parent 6986be1b21
commit 1d83b43efe
1 changed files with 184 additions and 162 deletions

View File

@ -22,6 +22,7 @@ graphics update methods via our custom ``pyqtgraph`` charting api.
''' '''
from functools import partial from functools import partial
import itertools
import time import time
from typing import Optional, Any, Callable from typing import Optional, Any, Callable
@ -73,7 +74,7 @@ from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
# TODO: load this from a config.toml! # TODO: load this from a config.toml!
_quote_throttle_rate: int = 16 # Hz _quote_throttle_rate: int = round(60 * 6/16) # Hz
# a working tick-type-classes template # a working tick-type-classes template
@ -93,7 +94,7 @@ def chart_maxmin(
chart: ChartPlotWidget, chart: ChartPlotWidget,
fqsn: str, fqsn: str,
# ohlcv_shm: ShmArray, # ohlcv_shm: ShmArray,
vlm_chart: Optional[ChartPlotWidget] = None, vlm_chart: ChartPlotWidget | None = None,
) -> tuple[ ) -> tuple[
@ -487,16 +488,16 @@ async def graphics_update_loop(
fast_chart.linked.isHidden() fast_chart.linked.isHidden()
or not rt_pi.isVisible() or not rt_pi.isVisible()
): ):
print(f'{fqsn} skipping update -> CHART HIDDEN') print(f'{fqsn} skipping update for HIDDEN CHART')
fast_chart.pause_all_feeds()
continue continue
# if fast_chart.linked.isHidden():
# fast_chart.pause_all_feeds()
# ic = fast_chart.view._ic ic = fast_chart.view._ic
# if ic: if ic:
# fast_chart.pause_all_feeds() fast_chart.pause_all_feeds()
# await ic.wait() print(f'{fqsn} PAUSING DURING INTERACTION')
# fast_chart.resume_all_feeds() await ic.wait()
fast_chart.resume_all_feeds()
# sync call to update all graphics/UX components. # sync call to update all graphics/UX components.
graphics_update_cycle( graphics_update_cycle(
@ -837,6 +838,9 @@ def graphics_update_cycle(
# volume chart logic.. # volume chart logic..
# TODO: can we unify this with the above loop? # TODO: can we unify this with the above loop?
if vlm_chart: if vlm_chart:
# print(f"DOING VLM {fqsn}")
vlm_flows = vlm_chart._flows
# always update y-label # always update y-label
ds.vlm_sticky.update_from_data( ds.vlm_sticky.update_from_data(
*array[-1][['index', 'volume']] *array[-1][['index', 'volume']]
@ -879,49 +883,49 @@ def graphics_update_cycle(
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
vars['last_mx_vlm'] = mx_vlm_in_view vars['last_mx_vlm'] = mx_vlm_in_view
# update all downstream FSPs # update all downstream FSPs
for curve_name, flow in vlm_chart._flows.items(): for curve_name, flow in vlm_flows.items():
if ( if (
curve_name not in {'volume', fqsn} curve_name not in {'volume', fqsn}
and flow.render and flow.render
and ( and (
liv and do_rt_update liv and do_rt_update
or do_append or do_append
) )
# and not flow.is_ohlc # and not flow.is_ohlc
# and curve_name != fqsn # and curve_name != fqsn
): ):
update_fsp_chart( update_fsp_chart(
vlm_chart, vlm_chart,
flow, flow,
curve_name, curve_name,
array_key=curve_name, array_key=curve_name,
# do_append=uppx < update_uppx, # do_append=uppx < update_uppx,
do_append=do_append, do_append=do_append,
) )
# is this even doing anything? # is this even doing anything?
# (pretty sure it's the real-time # (pretty sure it's the real-time
# resizing from last quote?) # resizing from last quote?)
fvb = flow.plot.vb fvb = flow.plot.vb
fvb._set_yrange( fvb._set_yrange(
name=curve_name, name=curve_name,
) )
elif ( elif (
curve_name != 'volume' curve_name != 'volume'
and not do_append and not do_append
and liv and liv
and uppx >= 1 and uppx >= 1
# even if we're downsampled bigly # even if we're downsampled bigly
# draw the last datum in the final # draw the last datum in the final
# px column to give the user the mx/mn # px column to give the user the mx/mn
# range of that set. # range of that set.
): ):
# always update the last datum-element # always update the last datum-element
# graphic for all flows # graphic for all flows
# print(f'drawing last {flow.name}') # print(f'drawing last {flow.name}')
flow.draw_last(array_key=curve_name) flow.draw_last(array_key=curve_name)
async def link_views_with_region( async def link_views_with_region(
@ -1124,7 +1128,8 @@ async def display_symbol_data(
# limit to at least display's FPS # limit to at least display's FPS
# avoiding needless Qt-in-guest-mode context switches # avoiding needless Qt-in-guest-mode context switches
tick_throttle=_quote_throttle_rate, tick_throttle=round(_quote_throttle_rate/len(fqsns)),
# tick_throttle=round(_quote_throttle_rate),
) as feed: ) as feed:
@ -1155,34 +1160,122 @@ async def display_symbol_data(
rt_chart: None | ChartPlotWidget = None rt_chart: None | ChartPlotWidget = None
hist_chart: None | ChartPlotWidget = None hist_chart: None | ChartPlotWidget = None
vlm_chart: None | ChartPlotWidget = None
bg_chart_color = 'grayest' # TODO: I think some palette's based on asset group types
bg_last_bar_color = 'grayer' # would be good, for eg:
# - underlying and opts contracts
# - index and underlyings + futures
# - gradient in "lightness" based on liquidity, or lifetime in derivs?
palette = itertools.cycle([
# curve color, last bar curve color
['i3', 'gray'],
['grayer', 'bracket'],
['grayest', 'i3'],
['default_dark', 'default'],
])
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {} pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}
# load in ohlc data to a common linked but split chart set. # load in ohlc data to a common linked but split chart set.
for fqsn, flume in feed.flumes.items(): fitems: list[
rt_linked._symbol = flume.symbol tuple[str, Flume]
hist_linked._symbol = flume.symbol ] = list(feed.flumes.items())
ohlcv: ShmArray = flume.rt_shm # for the "first"/selected symbol we create new chart widgets
hist_ohlcv: ShmArray = flume.hist_shm # and sub-charts for FSPs
fqsn, flume = fitems[0]
symbol = flume.symbol rt_linked._symbol = flume.symbol
fqsn = symbol.fqsn hist_linked._symbol = flume.symbol
if hist_chart is None: ohlcv: ShmArray = flume.rt_shm
hist_chart = hist_linked.plot_ohlc_main( hist_ohlcv: ShmArray = flume.hist_shm
symbol,
hist_ohlcv, symbol = flume.symbol
# in the case of history chart we explicitly set `False` brokername = symbol.brokers[0]
# to avoid internal pane creation. fqsn = symbol.fqsn
# sidepane=False,
sidepane=godwidget.search, hist_chart = hist_linked.plot_ohlc_main(
symbol,
hist_ohlcv,
# in the case of history chart we explicitly set `False`
# to avoid internal pane creation.
# sidepane=False,
sidepane=godwidget.search,
)
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
# don't show when not focussed
hist_linked.cursor.always_show_xlabel = False
rt_chart = rt_linked.plot_ohlc_main(
symbol,
ohlcv,
# in the case of history chart we explicitly set `False`
# to avoid internal pane creation.
sidepane=pp_pane,
)
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
# for pause/resume on mouse interaction
rt_chart.feed = feed
async with trio.open_nursery() as ln:
# if available load volume related built-in display(s)
vlm_charts: dict[
str,
None | ChartPlotWidget
] = {}.fromkeys(feed.flumes)
if (
not symbol.broker_info[brokername].get('no_vlm', False)
and has_vlm(ohlcv)
and vlm_chart is None
):
vlm_charts[fqsn] = await ln.start(
open_vlm_displays,
rt_linked,
flume,
) )
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
else: # load (user's) FSP set (otherwise known as "indicators")
# from an input config.
ln.start_soon(
start_fsp_displays,
rt_linked,
flume,
loading_sym_key,
loglevel,
)
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
# plot historical vwap if available
wap_in_history = False
# if (
# brokermod._show_wap_in_history
# and 'bar_wap' in bars.dtype.fields
# ):
# wap_in_history = True
# rt_chart.draw_curve(
# name='bar_wap',
# shm=ohlcv,
# color='default_light',
# add_label=False,
# )
for fqsn, flume in fitems[1:]:
# get a new color from the palette
bg_chart_color, bg_last_bar_color = next(palette)
rt_linked._symbol = flume.symbol
hist_linked._symbol = flume.symbol
ohlcv: ShmArray = flume.rt_shm
hist_ohlcv: ShmArray = flume.hist_shm
symbol = flume.symbol
fqsn = symbol.fqsn
hist_pi = hist_chart.overlay_plotitem( hist_pi = hist_chart.overlay_plotitem(
name=fqsn, name=fqsn,
axis_title=fqsn, axis_title=fqsn,
@ -1212,29 +1305,8 @@ async def display_symbol_data(
# ``.draw_curve()``. # ``.draw_curve()``.
flow = hist_chart._flows[fqsn] flow = hist_chart._flows[fqsn]
assert flow.plot is hist_pi assert flow.plot is hist_pi
# group_mxmn = partial(
# multi_maxmin,
# names=fqsns,
# chart=hist_chart,
# )
# hist_pi.vb._maxmin = group_mxmn
pis.setdefault(fqsn, [None, None])[1] = hist_pi pis.setdefault(fqsn, [None, None])[1] = hist_pi
# don't show when not focussed
hist_linked.cursor.always_show_xlabel = False
# create main OHLC chart
if rt_chart is None:
rt_chart = rt_linked.plot_ohlc_main(
symbol,
ohlcv,
# in the case of history chart we explicitly set `False`
# to avoid internal pane creation.
sidepane=pp_pane,
)
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
else:
rt_pi = rt_chart.overlay_plotitem( rt_pi = rt_chart.overlay_plotitem(
name=fqsn, name=fqsn,
axis_title=fqsn, axis_title=fqsn,
@ -1258,8 +1330,6 @@ async def display_symbol_data(
rt_chart.maxmin, rt_chart.maxmin,
name=fqsn, name=fqsn,
) )
# multi_maxmin,
# names=fqsn
# TODO: we need a better API to do this.. # TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop # specially store ref to shm for lookup in display loop
@ -1267,77 +1337,26 @@ async def display_symbol_data(
# ``.draw_curve()``. # ``.draw_curve()``.
flow = rt_chart._flows[fqsn] flow = rt_chart._flows[fqsn]
assert flow.plot is rt_pi assert flow.plot is rt_pi
# group_mxmn = partial(
# multi_maxmin,
# names=fqsns,
# chart=rt_chart,
# )
# rt_pi.vb._maxmin = group_mxmn
pis.setdefault(fqsn, [None, None])[0] = rt_pi pis.setdefault(fqsn, [None, None])[0] = rt_pi
rt_chart._feeds[symbol.key] = feed
rt_chart.setFocus() rt_chart.setFocus()
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?! # NOTE: we must immediately tell Qt to show the OHLC chart
# plot historical vwap if available # to avoid a race where the subplots get added/shown to
wap_in_history = False # the linked set *before* the main price chart!
# if ( rt_linked.show()
# brokermod._show_wap_in_history rt_linked.focus()
# and 'bar_wap' in bars.dtype.fields await trio.sleep(0)
# ):
# wap_in_history = True
# rt_chart.draw_curve(
# name='bar_wap',
# shm=ohlcv,
# color='default_light',
# add_label=False,
# )
# NOTE: we must immediately tell Qt to show the OHLC chart # XXX: if we wanted it at the bottom?
# to avoid a race where the subplots get added/shown to # rt_linked.splitter.addWidget(hist_linked)
# the linked set *before* the main price chart! rt_linked.focus()
rt_linked.show()
rt_linked.focus()
await trio.sleep(0)
# XXX: if we wanted it at the bottom? godwidget.resize_all()
# rt_linked.splitter.addWidget(hist_linked)
rt_linked.focus()
godwidget.resize_all() # add all additional symbols as overlays
vlms: dict[str, ChartPlotWidget] = {}
async with trio.open_nursery() as ln:
for fqsn, flume in feed.flumes.items(): for fqsn, flume in feed.flumes.items():
vlm_chart: Optional[ChartPlotWidget] = None
ohlcv: ShmArray = flume.rt_shm
hist_ohlcv: ShmArray = flume.hist_shm
symbol = flume.symbol
brokername = symbol.brokers[0]
# if available load volume related built-in display(s)
if (
not symbol.broker_info[brokername].get('no_vlm', False)
and has_vlm(ohlcv)
):
vlms[fqsn] = vlm_chart = await ln.start(
open_vlm_displays,
rt_linked,
flume,
)
# load (user's) FSP set (otherwise known as "indicators")
# from an input config.
ln.start_soon(
start_fsp_displays,
rt_linked,
flume,
loading_sym_key,
loglevel,
)
# size view to data prior to order mode init # size view to data prior to order mode init
rt_chart.default_view() rt_chart.default_view()
rt_linked.graphics_cycle() rt_linked.graphics_cycle()
@ -1352,9 +1371,9 @@ async def display_symbol_data(
godwidget.resize_all() godwidget.resize_all()
if not vlm_chart: # trigger another view reset if no sub-chart
# trigger another view reset if no sub-chart hist_chart.default_view()
rt_chart.default_view() rt_chart.default_view()
# let Qt run to render all widgets and make sure the # let Qt run to render all widgets and make sure the
# sidepanes line up vertically. # sidepanes line up vertically.
@ -1376,6 +1395,7 @@ async def display_symbol_data(
# sbar._status_groups[loading_sym_key][1]() # sbar._status_groups[loading_sym_key][1]()
hist_linked.graphics_cycle() hist_linked.graphics_cycle()
rt_chart.default_view()
await trio.sleep(0) await trio.sleep(0)
bars_in_mem = int(len(hist_ohlcv.array)) bars_in_mem = int(len(hist_ohlcv.array))
@ -1400,9 +1420,10 @@ async def display_symbol_data(
feed, feed,
pis, pis,
wap_in_history, wap_in_history,
vlms, vlm_charts,
) )
rt_chart.default_view()
await trio.sleep(0) await trio.sleep(0)
mode: OrderMode mode: OrderMode
@ -1416,5 +1437,6 @@ async def display_symbol_data(
): ):
rt_linked.mode = mode rt_linked.mode = mode
# let the app run.. bby rt_chart.default_view()
await trio.sleep_forever() hist_chart.default_view()
await trio.sleep_forever() # let the app run.. bby