Make graphics-update-loop multi-sym aware B)

Initial support for real-time multi-symbol overlay charts using an
aggregate feed delivered by `Feed.open_multi_stream()`.

The setup steps for constructing the overlayed plot items is still very
very rough and will likely provide incentive for better refactoring high
level "charting APIs". For each fqsn passed into `display_symbol_data()`
we now synchronously,
- create a single call to `LinkedSplits.plot_ohlc_main() -> `ChartPlotWidget`
  where we cache the chart in scope and for all other "sibling" fqsns
  we,
- make a call to `ChartPlotWidget.overlay_plotitem()` -> `PlotItem`, hide its axes,
  make another call with this plotitem input to
  `ChartPlotWidget.draw_curve()`, set a sym-specific view box auto-yrange maxmin callback,
  register the plotitem in a global `pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}`

Once all plots have been created we then asynchronously for each symbol,
- maybe create a volume chart and register it in a similar task-global
  table: `vlms: dict[str, ChartPlotWidget] = {}`
- start fsp displays for each symbol

Then common entrypoints are entered once for all symbols:
- a single `graphics_update_loop()` loop-task is started wherein
  real-time graphics update components for each symbol are created,
      * `L1Labels`
      * y-axis last clearing price stickies
      * `maxmin()` auto-ranger
      * `DisplayState` (stored in a table `dss: dict[str, DisplayState] = {}`)
      * an `increment_history_view()` task
  and a single call to `Feed.open_multi_stream()` is used to create
  a symbol-multiplexed quote stream which drives a single loop over all
  symbols wherein for each quote the appropriate components are looked
  up and passed to `graphics_update_cycle()`.
- a single call to `open_order_mode()` is made with the first symbol
  provided as input, though eventually we want to support passing in the
  entire list.

Further internal implementation details:
- special tweaks to the `pg.LinearRegionItem` setup wherein the region
  is added with a zero opacity and *after* all plotitem overlays to
  avoid and issue where overlays weren't being shown within the region
  area in the history chart.
- all symbol-specific graphics oriented update calls are adjusted to
  pass in the fqsn:
  * `update_fsp_chart()`
  * `ChartView._set_yrange()`
  * ChartPlotWidget.update_graphics_from_flow()`
- avoid a double increment on sample step updates by not calling the
  increment on any vlm chart since it seems the vlm-ohlc chart linking
  already takes care of this now?
- use global counters for the last epoch time step to avoid incrementing
  all views more then once per new time step given underlying shm array
  buffers may be on different array-index values from one another.
multi_symbol_input
Tyler Goodlet 2022-11-15 15:05:05 -05:00
parent be3dc69290
commit 7aec238f5f
1 changed files with 832 additions and 603 deletions

View File

@ -36,6 +36,9 @@ from ..data.feed import (
Flume, Flume,
) )
from ..data.types import Struct from ..data.types import Struct
from ..data._sharedmem import (
ShmArray,
)
from ._axes import YAxisLabel from ._axes import YAxisLabel
from ._chart import ( from ._chart import (
ChartPlotWidget, ChartPlotWidget,
@ -50,14 +53,12 @@ from ._fsp import (
has_vlm, has_vlm,
open_vlm_displays, open_vlm_displays,
) )
from ..data._sharedmem import (
ShmArray,
)
from ..data._source import tf_in_1s
from ._forms import ( from ._forms import (
FieldsForm, FieldsForm,
mk_order_pane_layout, mk_order_pane_layout,
) )
from . import _pg_overrides as pgo
# from ..data._source import tf_in_1s
from .order_mode import ( from .order_mode import (
open_order_mode, open_order_mode,
OrderMode, OrderMode,
@ -90,7 +91,8 @@ _tick_groups = {
# https://github.com/lemire/pythonmaxmin # https://github.com/lemire/pythonmaxmin
def chart_maxmin( def chart_maxmin(
chart: ChartPlotWidget, chart: ChartPlotWidget,
ohlcv_shm: ShmArray, fqsn: str,
# ohlcv_shm: ShmArray,
vlm_chart: Optional[ChartPlotWidget] = None, vlm_chart: Optional[ChartPlotWidget] = None,
) -> tuple[ ) -> tuple[
@ -106,7 +108,7 @@ def chart_maxmin(
''' '''
last_bars_range = chart.bars_range() last_bars_range = chart.bars_range()
out = chart.maxmin() out = chart.maxmin(name=fqsn)
if out is None: if out is None:
return (last_bars_range, 0, 0, 0) return (last_bars_range, 0, 0, 0)
@ -131,6 +133,10 @@ def chart_maxmin(
) )
_i_last: int = 0
_i_last_append: int = 0
class DisplayState(Struct): class DisplayState(Struct):
''' '''
Chart-local real-time graphics state container. Chart-local real-time graphics state container.
@ -140,6 +146,7 @@ class DisplayState(Struct):
quotes: dict[str, Any] quotes: dict[str, Any]
maxmin: Callable maxmin: Callable
flume: Flume
ohlcv: ShmArray ohlcv: ShmArray
hist_ohlcv: ShmArray hist_ohlcv: ShmArray
@ -173,14 +180,18 @@ class DisplayState(Struct):
update_state: bool = True, update_state: bool = True,
update_uppx: float = 16, update_uppx: float = 16,
is_1m: bool = False,
) -> tuple: ) -> tuple:
shm = shm or self.ohlcv shm = shm or self.ohlcv
chart = chart or self.chart chart = chart or self.chart
state = state or self.vars # state = state or self.vars
if not update_state: if (
not update_state
and state
):
state = state.copy() state = state.copy()
# compute the first available graphic's x-units-per-pixel # compute the first available graphic's x-units-per-pixel
@ -194,26 +205,56 @@ class DisplayState(Struct):
# "curve" length is already automatic. # "curve" length is already automatic.
# increment the view position by the sample offset. # increment the view position by the sample offset.
i_step = shm.index # i_step = shm.index
i_diff = i_step - state['i_last'] i_step = shm.array[-1]['time']
state['i_last'] = i_step # i_diff = i_step - state['i_last']
# state['i_last'] = i_step
global _i_last, _i_last_append
i_diff = i_step - _i_last
# update global state
if (
# state is None
not is_1m
and i_diff > 0
):
_i_last = i_step
append_diff = i_step - state['i_last_append'] # append_diff = i_step - state['i_last_append']
append_diff = i_step - _i_last_append
# real-time update necessary?
_, _, _, r = chart.bars_range()
liv = r >= shm.index
# update the "last datum" (aka extending the flow graphic with # update the "last datum" (aka extending the flow graphic with
# new data) only if the number of unit steps is >= the number of # new data) only if the number of unit steps is >= the number of
# such unit steps per pixel (aka uppx). Iow, if the zoom level # such unit steps per pixel (aka uppx). Iow, if the zoom level
# is such that a datum(s) update to graphics wouldn't span # is such that a datum(s) update to graphics wouldn't span
# to a new pixel, we don't update yet. # to a new pixel, we don't update yet.
do_append = (append_diff >= uppx) do_append = (
if do_append: append_diff >= uppx
state['i_last_append'] = i_step and i_diff
)
if (
do_append
and not is_1m
):
_i_last_append = i_step
# fqsn = self.flume.symbol.fqsn
# print(
# f'DOING APPEND => {fqsn}\n'
# f'i_step:{i_step}\n'
# f'i_diff:{i_diff}\n'
# f'last:{_i_last}\n'
# f'last_append:{_i_last_append}\n'
# f'append_diff:{append_diff}\n'
# f'r: {r}\n'
# f'liv: {liv}\n'
# f'uppx: {uppx}\n'
# )
do_rt_update = uppx < update_uppx do_rt_update = uppx < update_uppx
_, _, _, r = chart.bars_range()
liv = r >= i_step
# TODO: pack this into a struct # TODO: pack this into a struct
return ( return (
uppx, uppx,
@ -229,9 +270,10 @@ async def graphics_update_loop(
nurse: trio.Nursery, nurse: trio.Nursery,
godwidget: GodWidget, godwidget: GodWidget,
flume: Flume, feed: Feed,
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {},
wap_in_history: bool = False, wap_in_history: bool = False,
vlm_chart: Optional[ChartPlotWidget] = None, vlm_charts: dict[str, ChartPlotWidget] = {},
) -> None: ) -> None:
''' '''
@ -255,27 +297,34 @@ async def graphics_update_loop(
fast_chart = linked.chart fast_chart = linked.chart
hist_chart = godwidget.hist_linked.chart hist_chart = godwidget.hist_linked.chart
assert hist_chart
dss: dict[str, DisplayState] = {}
for fqsn, flume in feed.flumes.items():
ohlcv = flume.rt_shm ohlcv = flume.rt_shm
hist_ohlcv = flume.hist_shm hist_ohlcv = flume.hist_shm
symbol = flume.symbol
fqsn = symbol.fqsn
# update last price sticky # update last price sticky
last_price_sticky = fast_chart.plotItem.getAxis( fast_pi = fast_chart._flows[fqsn].plot
'right')._stickies.get(fast_chart.name) last_price_sticky = fast_pi.getAxis('right')._stickies[fqsn]
last_price_sticky.update_from_data( last_price_sticky.update_from_data(
*ohlcv.array[-1][['index', 'close']] *ohlcv.array[-1][['index', 'close']]
) )
last_price_sticky.show()
hist_last_price_sticky = hist_chart.plotItem.getAxis( slow_pi = hist_chart._flows[fqsn].plot
'right')._stickies.get(hist_chart.name) hist_last_price_sticky = slow_pi.getAxis('right')._stickies[fqsn]
hist_last_price_sticky.update_from_data( hist_last_price_sticky.update_from_data(
*hist_ohlcv.array[-1][['index', 'close']] *hist_ohlcv.array[-1][['index', 'close']]
) )
vlm_chart = vlm_charts[fqsn]
maxmin = partial( maxmin = partial(
chart_maxmin, chart_maxmin,
fast_chart, fast_chart,
ohlcv, fqsn,
vlm_chart, vlm_chart,
) )
last_bars_range: tuple[float, float] last_bars_range: tuple[float, float]
@ -288,15 +337,18 @@ async def graphics_update_loop(
last, volume = ohlcv.array[-1][['close', 'volume']] last, volume = ohlcv.array[-1][['close', 'volume']]
symbol = fast_chart.linked.symbol symbol = flume.symbol
l1 = L1Labels( l1 = L1Labels(
fast_chart.plotItem, fast_pi,
# determine precision/decimal lengths # determine precision/decimal lengths
digits=symbol.tick_size_digits, digits=symbol.tick_size_digits,
size_digits=symbol.lot_size_digits, size_digits=symbol.lot_size_digits,
) )
fast_chart._l1_labels = l1 # TODO: this is just wrong now since we can have multiple L1-label
# sets, so instead we should have the l1 associated with the
# plotitem or y-axis likely?
# fast_chart._l1_labels = l1
# TODO: # TODO:
# - in theory we should be able to read buffer data faster # - in theory we should be able to read buffer data faster
@ -306,17 +358,19 @@ async def graphics_update_loop(
# levels this might be dark volume we need to # levels this might be dark volume we need to
# present differently -> likely dark vlm # present differently -> likely dark vlm
tick_size = fast_chart.linked.symbol.tick_size tick_size = symbol.tick_size
tick_margin = 3 * tick_size tick_margin = 3 * tick_size
fast_chart.show() fast_chart.show()
last_quote = time.time() last_quote = time.time()
# global _i_last
i_last = ohlcv.index i_last = ohlcv.index
ds = linked.display_state = DisplayState(**{ dss[fqsn] = ds = linked.display_state = DisplayState(**{
'godwidget': godwidget, 'godwidget': godwidget,
'quotes': {}, 'quotes': {},
'maxmin': maxmin, 'maxmin': maxmin,
'flume': flume,
'ohlcv': ohlcv, 'ohlcv': ohlcv,
'hist_ohlcv': hist_ohlcv, 'hist_ohlcv': hist_ohlcv,
'chart': fast_chart, 'chart': fast_chart,
@ -335,8 +389,8 @@ async def graphics_update_loop(
}) })
if vlm_chart: if vlm_chart:
vlm_sticky = vlm_chart.plotItem.getAxis( vlm_pi = vlm_chart._flows['volume'].plot
'right')._stickies.get('volume') vlm_sticky = vlm_pi.getAxis('right')._stickies['volume']
ds.vlm_chart = vlm_chart ds.vlm_chart = vlm_chart
ds.vlm_sticky = vlm_sticky ds.vlm_sticky = vlm_sticky
@ -379,6 +433,7 @@ async def graphics_update_loop(
chart=hist_chart, chart=hist_chart,
shm=ds.hist_ohlcv, shm=ds.hist_ohlcv,
state=state, state=state,
is_1m=True,
# update_state=False, # update_state=False,
) )
# print( # print(
@ -391,16 +446,20 @@ async def graphics_update_loop(
do_append do_append
and liv and liv
): ):
hist_chart.increment_view(steps=i_diff) # hist_chart.increment_view(steps=i_diff)
hist_chart.view._set_yrange(yrange=hist_chart.maxmin()) flow = hist_chart._flows[fqsn]
flow.plot.vb._set_yrange(
# yrange=hist_chart.maxmin(name=fqsn)
)
# hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
nurse.start_soon(increment_history_view) nurse.start_soon(increment_history_view)
# main real-time quotes update loop # main real-time quotes update loop
stream: tractor.MsgStream = flume.stream stream: tractor.MsgStream
async with feed.open_multi_stream() as stream:
assert stream
async for quotes in stream: async for quotes in stream:
ds.quotes = quotes
quote_period = time.time() - last_quote quote_period = time.time() - last_quote
quote_rate = round( quote_rate = round(
1/quote_period, 1) if quote_period > 0 else float('inf') 1/quote_period, 1) if quote_period > 0 else float('inf')
@ -416,11 +475,22 @@ async def graphics_update_loop(
last_quote = time.time() last_quote = time.time()
# chart isn't active/shown so skip render cycle and pause feed(s) for sym, quote in quotes.items():
if fast_chart.linked.isHidden(): ds = dss[sym]
# print('skipping update') ds.quotes = quote
fast_chart.pause_all_feeds()
rt_pi, hist_pi = pis[sym]
# chart isn't active/shown so skip render cycle and
# pause feed(s)
if (
fast_chart.linked.isHidden()
or not rt_pi.isVisible()
):
print(f'{fqsn} skipping update -> CHART HIDDEN')
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:
@ -429,11 +499,15 @@ async def graphics_update_loop(
# fast_chart.resume_all_feeds() # fast_chart.resume_all_feeds()
# sync call to update all graphics/UX components. # sync call to update all graphics/UX components.
graphics_update_cycle(ds) graphics_update_cycle(
ds,
quote,
)
def graphics_update_cycle( def graphics_update_cycle(
ds: DisplayState, ds: DisplayState,
quote: dict,
wap_in_history: bool = False, wap_in_history: bool = False,
trigger_all: bool = False, # flag used by prepend history updates trigger_all: bool = False, # flag used by prepend history updates
prepend_update_index: Optional[int] = None, prepend_update_index: Optional[int] = None,
@ -445,6 +519,12 @@ def graphics_update_cycle(
chart = ds.chart chart = ds.chart
# TODO: just pass this as a direct ref to avoid so many attr accesses? # TODO: just pass this as a direct ref to avoid so many attr accesses?
hist_chart = ds.godwidget.hist_linked.chart hist_chart = ds.godwidget.hist_linked.chart
assert hist_chart
flume = ds.flume
sym = flume.symbol
fqsn = sym.fqsn
main_flow = chart._flows[fqsn]
profiler = Profiler( profiler = Profiler(
msg=f'Graphics loop cycle for: `{chart.name}`', msg=f'Graphics loop cycle for: `{chart.name}`',
@ -461,13 +541,13 @@ def graphics_update_cycle(
# rt "HFT" chart # rt "HFT" chart
l1 = ds.l1 l1 = ds.l1
ohlcv = ds.ohlcv # ohlcv = ds.ohlcv
ohlcv = flume.rt_shm
array = ohlcv.array array = ohlcv.array
vars = ds.vars vars = ds.vars
tick_margin = vars['tick_margin'] tick_margin = vars['tick_margin']
for sym, quote in ds.quotes.items():
( (
uppx, uppx,
liv, liv,
@ -477,6 +557,41 @@ def graphics_update_cycle(
do_rt_update, do_rt_update,
) = ds.incr_info() ) = ds.incr_info()
# don't real-time "shift" the curve to the
# left unless we get one of the following:
if (
(
do_append
and liv
)
or trigger_all
):
# print(f'INCREMENTING {fqsn}')
chart.increment_view(steps=i_diff)
main_flow.plot.vb._set_yrange(
# yrange=(mn, mx),
)
# NOTE: since vlm and ohlc charts are axis linked now we don't
# need the double increment request?
# if vlm_chart:
# vlm_chart.increment_view(steps=i_diff)
profiler('view incremented')
ticks_frame = quote.get('ticks', ())
frames_by_type: dict[str, dict] = {}
lasts = {}
# build tick-type "frames" of tick sequences since
# likely the tick arrival rate is higher then our
# (throttled) quote stream rate.
# iterate in FIFO order per tick-frame
# if sym != fqsn:
# continue
# TODO: we should only run mxmn when we know # TODO: we should only run mxmn when we know
# an update is due via ``do_append`` above. # an update is due via ``do_append`` above.
( (
@ -488,7 +603,6 @@ def graphics_update_cycle(
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
profiler('`ds.maxmin()` call') profiler('`ds.maxmin()` call')
if ( if (
@ -502,35 +616,13 @@ def graphics_update_cycle(
log.debug('Skipping prepend graphics cycle: frame not in view') log.debug('Skipping prepend graphics cycle: frame not in view')
return return
# don't real-time "shift" the curve to the
# left unless we get one of the following:
if (
(do_append and liv)
or trigger_all
):
chart.increment_view(steps=i_diff)
chart.view._set_yrange(yrange=(mn, mx))
if vlm_chart:
vlm_chart.increment_view(steps=i_diff)
profiler('view incremented')
ticks_frame = quote.get('ticks', ())
frames_by_type: dict[str, dict] = {}
lasts = {}
# build tick-type "frames" of tick sequences since
# likely the tick arrival rate is higher then our
# (throttled) quote stream rate.
for tick in ticks_frame: for tick in ticks_frame:
price = tick.get('price') price = tick.get('price')
ticktype = tick.get('type') ticktype = tick.get('type')
if ticktype == 'n/a' or price == -1: # if ticktype == 'n/a' or price == -1:
# okkk.. # # okkk..
continue # continue
# keys are entered in olded-event-inserted-first order # keys are entered in olded-event-inserted-first order
# since we iterate ``ticks_frame`` in standard order # since we iterate ``ticks_frame`` in standard order
@ -545,9 +637,11 @@ def graphics_update_cycle(
# frame_counts = { # frame_counts = {
# typ: len(frame) for typ, frame in frames_by_type.items() # typ: len(frame) for typ, frame in frames_by_type.items()
# } # }
# print(f'{pformat(frame_counts)}') # print(
# print(f'framed: {pformat(frames_by_type)}') # f'{pformat(frame_counts)}\n'
# print(f'lasts: {pformat(lasts)}') # f'framed: {pformat(frames_by_type)}\n'
# f'lasts: {pformat(lasts)}\n'
# )
# TODO: eventually we want to separate out the utrade (aka # TODO: eventually we want to separate out the utrade (aka
# dark vlm prices) here and show them as an additional # dark vlm prices) here and show them as an additional
@ -566,11 +660,15 @@ def graphics_update_cycle(
or trigger_all or trigger_all
): ):
chart.update_graphics_from_flow( chart.update_graphics_from_flow(
chart.name, fqsn,
# chart.name,
do_append=do_append, do_append=do_append,
) )
main_flow.draw_last(array_key=fqsn)
hist_chart.update_graphics_from_flow( hist_chart.update_graphics_from_flow(
chart.name, fqsn,
# chart.name,
do_append=do_append, do_append=do_append,
) )
@ -578,7 +676,6 @@ def graphics_update_cycle(
# since the current range should at least be updated # since the current range should at least be updated
# to it's max/min on the last pixel. # to it's max/min on the last pixel.
# iterate in FIFO order per tick-frame
for typ, tick in lasts.items(): for typ, tick in lasts.items():
price = tick.get('price') price = tick.get('price')
@ -606,19 +703,13 @@ def graphics_update_cycle(
# set. # set.
# update price sticky(s) # update price sticky(s)
end = array[-1] end_ic = array[-1][['index', 'close']]
ds.last_price_sticky.update_from_data( ds.last_price_sticky.update_from_data(*end_ic)
*end[['index', 'close']] ds.hist_last_price_sticky.update_from_data(*end_ic)
)
ds.hist_last_price_sticky.update_from_data(
*end[['index', 'close']]
)
if wap_in_history: if wap_in_history:
# update vwap overlay line # update vwap overlay line
chart.update_graphics_from_flow( chart.update_graphics_from_flow('bar_wap')
'bar_wap',
)
# L1 book label-line updates # L1 book label-line updates
# XXX: is this correct for ib? # XXX: is this correct for ib?
@ -667,7 +758,8 @@ def graphics_update_cycle(
liv liv
and not chart._static_yrange == 'axis' and not chart._static_yrange == 'axis'
): ):
main_vb = chart.view # main_vb = chart.view
main_vb = chart._flows[fqsn].plot.vb
if ( if (
main_vb._ic is None main_vb._ic is None
or not main_vb._ic.is_set() or not main_vb._ic.is_set()
@ -680,7 +772,7 @@ def graphics_update_cycle(
# slap in orders outside the current # slap in orders outside the current
# L1 (only) book range. # L1 (only) book range.
# range_margin=0.1, # range_margin=0.1,
yrange=(mn, mx), # yrange=(mn, mx),
) )
# check if slow chart needs a resize # check if slow chart needs a resize
@ -695,9 +787,13 @@ def graphics_update_cycle(
chart=hist_chart, chart=hist_chart,
shm=ds.hist_ohlcv, shm=ds.hist_ohlcv,
update_state=False, update_state=False,
is_1m=True,
) )
if hist_liv: if hist_liv:
hist_chart.view._set_yrange(yrange=hist_chart.maxmin()) flow = hist_chart._flows[fqsn]
flow.plot.vb._set_yrange(
# yrange=hist_chart.maxmin(name=fqsn),
)
# XXX: update this every draw cycle to make L1-always-in-view work. # XXX: update this every draw cycle to make L1-always-in-view work.
vars['last_mx'], vars['last_mn'] = mx, mn vars['last_mx'], vars['last_mn'] = mx, mn
@ -706,7 +802,11 @@ def graphics_update_cycle(
# TODO: should the "main" (aka source) flow be special? # TODO: should the "main" (aka source) flow be special?
for curve_name, flow in chart._flows.items(): for curve_name, flow in chart._flows.items():
# update any overlayed fsp flows # update any overlayed fsp flows
if curve_name != chart.data_key: if (
# curve_name != chart.data_key
curve_name != fqsn
and not flow.is_ohlc
):
update_fsp_chart( update_fsp_chart(
chart, chart,
flow, flow,
@ -719,9 +819,9 @@ def graphics_update_cycle(
# 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.
if ( if (
not do_append liv
# and not do_append
# and not do_rt_update # and not do_rt_update
and liv
): ):
flow.draw_last( flow.draw_last(
array_key=curve_name, array_key=curve_name,
@ -773,14 +873,18 @@ 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
for curve_name, flow in vlm_chart._flows.items(): for curve_name, flow in vlm_chart._flows.items():
if ( if (
curve_name != 'volume' and curve_name not in {'volume', fqsn}
flow.render and ( and flow.render
liv and and (
do_rt_update or do_append liv and do_rt_update
or do_append
) )
# and not flow.is_ohlc
# and curve_name != fqsn
): ):
update_fsp_chart( update_fsp_chart(
vlm_chart, vlm_chart,
@ -836,11 +940,12 @@ async def link_views_with_region(
pen=pg.mkPen(hcolor('gunmetal')), pen=pg.mkPen(hcolor('gunmetal')),
brush=pg.mkBrush(hcolor('default_darkest')), brush=pg.mkBrush(hcolor('default_darkest')),
) )
region.setZValue(10) # put linear region "in front" in layer terms
region.setOpacity(0)
hist_pi.addItem(region, ignoreBounds=True) hist_pi.addItem(region, ignoreBounds=True)
region.setOpacity(6/16)
flow = rt_chart._flows[hist_chart.name] flow = rt_chart._flows[flume.symbol.fqsn]
assert flow assert flow
# XXX: no idea why this doesn't work but it's causing # XXX: no idea why this doesn't work but it's causing
@ -948,6 +1053,32 @@ async def link_views_with_region(
# region.sigRegionChangeFinished.connect(update_pi_from_region) # region.sigRegionChangeFinished.connect(update_pi_from_region)
# force 0 to always be in view
def multi_maxmin(
chart: ChartPlotWidget,
names: list[str],
) -> tuple[float, float]:
'''
Flows "group" maxmin loop; assumes all named flows
are in the same co-domain and thus can be sorted
as one set.
Iterates all the named flows and calls the chart
api to find their range values and return.
TODO: really we should probably have a more built-in API
for this?
'''
mx = 0
for name in names:
ymn, ymx = chart.maxmin(name=name)
mx = max(mx, ymx)
return 0, mx
async def display_symbol_data( async def display_symbol_data(
godwidget: GodWidget, godwidget: GodWidget,
fqsns: list[str], fqsns: list[str],
@ -974,13 +1105,13 @@ async def display_symbol_data(
# ) # )
for fqsn in fqsns: for fqsn in fqsns:
loading_sym_key = sbar.open_status( loading_sym_key = sbar.open_status(
f'loading {fqsn} ->', f'loading {fqsn} ->',
group_key=True group_key=True
) )
feed: Feed feed: Feed
# assert len(fqsns) == 2
async with open_feed( async with open_feed(
fqsns, fqsns,
loglevel=loglevel, loglevel=loglevel,
@ -991,34 +1122,51 @@ async def display_symbol_data(
) as feed: ) as feed:
# TODO: right now we only show one symbol on charts, but # use expanded contract symbols passed back from feed layer.
# overlays are coming muy pronto guey.. fqsns = list(feed.flumes.keys())
assert len(feed.flumes) == 1
flume = list(feed.flumes.values())[0] # step_size_s = 1
# tf_key = tf_in_1s[step_size_s]
godwidget.window.setWindowTitle(
f'{fqsns} '
# f'tick:{symbol.tick_size} '
# f'step:{tf_key} '
)
# generate order mode side-pane UI
# A ``FieldsForm`` form to configure order entry
# and add as next-to-y-axis singleton pane
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
godwidget.pp_pane = pp_pane
# create top history view chart above the "main rt chart".
rt_linked = godwidget.rt_linked
hist_linked = godwidget.hist_linked
# NOTE: here we insert the slow-history chart set into
# the fast chart's splitter -> so it's a splitter of charts
# inside the first widget slot of a splitter of charts XD
rt_linked.splitter.insertWidget(0, hist_linked)
rt_chart: None | ChartPlotWidget = None
hist_chart: None | ChartPlotWidget = None
bg_chart_color = 'grayest'
bg_last_bar_color = 'grayer'
pis: dict[str, list[pgo.PlotItem, pgo.PlotItem]] = {}
# load in ohlc data to a common linked but split chart set.
for fqsn, flume in feed.flumes.items():
rt_linked._symbol = flume.symbol
hist_linked._symbol = flume.symbol
ohlcv: ShmArray = flume.rt_shm ohlcv: ShmArray = flume.rt_shm
hist_ohlcv: ShmArray = flume.hist_shm hist_ohlcv: ShmArray = flume.hist_shm
symbol = flume.symbol symbol = flume.symbol
fqsn = symbol.fqsn fqsn = symbol.fqsn
brokername = symbol.brokers[0]
step_size_s = 1 if hist_chart is None:
tf_key = tf_in_1s[step_size_s]
# load in symbol's ohlc data
godwidget.window.setWindowTitle(
f'{fqsn} '
f'tick:{symbol.tick_size} '
f'step:{tf_key} '
)
rt_linked = godwidget.rt_linked
rt_linked._symbol = symbol
# create top history view chart above the "main rt chart".
hist_linked = godwidget.hist_linked
hist_linked._symbol = symbol
hist_chart = hist_linked.plot_ohlc_main( hist_chart = hist_linked.plot_ohlc_main(
symbol, symbol,
hist_ohlcv, hist_ohlcv,
@ -1027,26 +1175,103 @@ async def display_symbol_data(
# sidepane=False, # sidepane=False,
sidepane=godwidget.search, sidepane=godwidget.search,
) )
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
else:
hist_pi = hist_chart.overlay_plotitem(
name=fqsn,
axis_title=fqsn,
)
hist_pi.hideAxis('left')
hist_pi.hideAxis('bottom')
curve, _ = hist_chart.draw_curve(
name=fqsn,
shm=hist_ohlcv,
array_key=fqsn,
overlay=hist_pi,
pi=hist_pi,
is_ohlc=True,
color=bg_chart_color,
last_bar_color=bg_last_bar_color,
)
hist_pi.vb.maxmin = partial(
hist_chart.maxmin,
name=fqsn,
)
# TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
flow = hist_chart._flows[fqsn]
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
# don't show when not focussed # don't show when not focussed
hist_linked.cursor.always_show_xlabel = False hist_linked.cursor.always_show_xlabel = False
# generate order mode side-pane UI
# A ``FieldsForm`` form to configure order entry
# and add as next-to-y-axis singleton pane
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
godwidget.pp_pane = pp_pane
# create main OHLC chart # create main OHLC chart
ohlc_chart = rt_linked.plot_ohlc_main( if rt_chart is None:
rt_chart = rt_linked.plot_ohlc_main(
symbol, symbol,
ohlcv, ohlcv,
# in the case of history chart we explicitly set `False` # in the case of history chart we explicitly set `False`
# to avoid internal pane creation. # to avoid internal pane creation.
sidepane=pp_pane, sidepane=pp_pane,
) )
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
else:
rt_pi = rt_chart.overlay_plotitem(
name=fqsn,
axis_title=fqsn,
)
ohlc_chart._feeds[symbol.key] = feed rt_pi.hideAxis('left')
ohlc_chart.setFocus() rt_pi.hideAxis('bottom')
curve, _ = rt_chart.draw_curve(
name=fqsn,
shm=ohlcv,
array_key=fqsn,
overlay=rt_pi,
pi=rt_pi,
is_ohlc=True,
color=bg_chart_color,
last_bar_color=bg_last_bar_color,
)
rt_pi.vb.maxmin = partial(
rt_chart.maxmin,
name=fqsn,
)
# multi_maxmin,
# names=fqsn
# TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop
# since only a placeholder of `None` is entered in
# ``.draw_curve()``.
flow = rt_chart._flows[fqsn]
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
rt_chart._feeds[symbol.key] = feed
rt_chart.setFocus()
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?! # XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
# plot historical vwap if available # plot historical vwap if available
@ -1056,7 +1281,7 @@ async def display_symbol_data(
# and 'bar_wap' in bars.dtype.fields # and 'bar_wap' in bars.dtype.fields
# ): # ):
# wap_in_history = True # wap_in_history = True
# ohlc_chart.draw_curve( # rt_chart.draw_curve(
# name='bar_wap', # name='bar_wap',
# shm=ohlcv, # shm=ohlcv,
# color='default_light', # color='default_light',
@ -1070,28 +1295,31 @@ async def display_symbol_data(
rt_linked.focus() rt_linked.focus()
await trio.sleep(0) await trio.sleep(0)
# NOTE: here we insert the slow-history chart set into
# the fast chart's splitter -> so it's a splitter of charts
# inside the first widget slot of a splitter of charts XD
rt_linked.splitter.insertWidget(0, hist_linked)
# XXX: if we wanted it at the bottom? # XXX: if we wanted it at the bottom?
# rt_linked.splitter.addWidget(hist_linked) # rt_linked.splitter.addWidget(hist_linked)
rt_linked.focus() rt_linked.focus()
godwidget.resize_all() godwidget.resize_all()
vlm_chart: Optional[ChartPlotWidget] = None vlms: dict[str, ChartPlotWidget] = {}
async with trio.open_nursery() as ln: async with trio.open_nursery() as ln:
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 available load volume related built-in display(s)
if ( if (
not symbol.broker_info[brokername].get('no_vlm', False) not symbol.broker_info[brokername].get('no_vlm', False)
and has_vlm(ohlcv) and has_vlm(ohlcv)
): ):
vlm_chart = await ln.start( vlms[fqsn] = vlm_chart = await ln.start(
open_vlm_displays, open_vlm_displays,
rt_linked, rt_linked,
ohlcv, flume,
) )
# load (user's) FSP set (otherwise known as "indicators") # load (user's) FSP set (otherwise known as "indicators")
@ -1099,25 +1327,13 @@ async def display_symbol_data(
ln.start_soon( ln.start_soon(
start_fsp_displays, start_fsp_displays,
rt_linked, rt_linked,
ohlcv, flume,
loading_sym_key, loading_sym_key,
loglevel, loglevel,
) )
# start graphics update loop after receiving first live quote
ln.start_soon(
graphics_update_loop,
ln,
godwidget,
flume,
wap_in_history,
vlm_chart,
)
await trio.sleep(0)
# size view to data prior to order mode init # size view to data prior to order mode init
ohlc_chart.default_view() rt_chart.default_view()
rt_linked.graphics_cycle() rt_linked.graphics_cycle()
await trio.sleep(0) await trio.sleep(0)
@ -1130,26 +1346,9 @@ async def display_symbol_data(
godwidget.resize_all() godwidget.resize_all()
await link_views_with_region(
ohlc_chart,
hist_chart,
flume,
)
mode: OrderMode
async with (
open_order_mode(
feed,
godwidget,
fqsn,
order_mode_started
) as mode
):
if not vlm_chart: if not vlm_chart:
# trigger another view reset if no sub-chart # trigger another view reset if no sub-chart
ohlc_chart.default_view() rt_chart.default_view()
rt_linked.mode = mode
# 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.
@ -1168,7 +1367,7 @@ async def display_symbol_data(
# TODO: make this not so shit XD # TODO: make this not so shit XD
# close group status # close group status
sbar._status_groups[loading_sym_key][1]() # sbar._status_groups[loading_sym_key][1]()
hist_linked.graphics_cycle() hist_linked.graphics_cycle()
await trio.sleep(0) await trio.sleep(0)
@ -1181,5 +1380,35 @@ async def display_symbol_data(
) )
godwidget.resize_all() godwidget.resize_all()
await link_views_with_region(
rt_chart,
hist_chart,
flume,
)
# start graphics update loop after receiving first live quote
ln.start_soon(
graphics_update_loop,
ln,
godwidget,
feed,
pis,
wap_in_history,
vlms,
)
await trio.sleep(0)
mode: OrderMode
async with (
open_order_mode(
feed,
godwidget,
fqsns[-1],
order_mode_started
) as mode
):
rt_linked.mode = mode
# let the app run.. bby # let the app run.. bby
await trio.sleep_forever() await trio.sleep_forever()