From adeb9698102c25cf952c40c921ba815c6eaaac09 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 2 Jan 2023 14:59:44 -0500 Subject: [PATCH] Repair auto-y-ranging to always include L1 spread Goes back to always adjusting the y-axis range to include the L1 spread and clearing label in view whenever the last datum is also in view, previously this was broken after reworking the display loop for multi-feeds. Drops a bunch of old commented tick looping cruft from before we started using tick-type framing. Also adds more stringent guards for ignoring but error logging quote values that are more then 25% out of range; it seems particularly our `ib` feed has some issues with strange `price` values that are way off here and there? --- piker/ui/_display.py | 128 ++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 3b4b173c..2d873cf9 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -322,10 +322,6 @@ async def graphics_update_loop( digits=symbol.tick_size_digits, size_digits=symbol.lot_size_digits, ) - # 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: # - in theory we should be able to read buffer data faster @@ -410,11 +406,11 @@ async def graphics_update_loop( last_quote_s = time.time() - for sym, quote in quotes.items(): - ds = dss[sym] + for fqsn, quote in quotes.items(): + ds = dss[fqsn] ds.quotes = quote - rt_pi, hist_pi = pis[sym] + rt_pi, hist_pi = pis[fqsn] # chart isn't active/shown so skip render cycle and # pause feed(s) @@ -443,16 +439,21 @@ async def graphics_update_loop( def graphics_update_cycle( ds: DisplayState, quote: dict, + wap_in_history: bool = False, trigger_all: bool = False, # flag used by prepend history updates prepend_update_index: Optional[int] = None, ) -> None: - # TODO: eventually optimize this whole graphics stack with ``numba`` - # hopefully XD + + # TODO: SPEEDing this all up.. + # - optimize this whole graphics stack with ``numba`` hopefully + # or at least a little `mypyc` B) + # - pass more direct refs as input to avoid so many attr accesses? + # - use a streaming minmax algo and drop the use of the + # state-tracking ``chart_maxmin()`` routine from above? chart = ds.chart - # TODO: just pass this as a direct ref to avoid so many attr accesses? hist_chart = ds.godwidget.hist_linked.chart flume = ds.flume @@ -465,10 +466,7 @@ def graphics_update_cycle( msg=f'Graphics loop cycle for: `{chart.name}`', delayed=True, disabled=not pg_profile_enabled(), - # disabled=True, ms_threshold=ms_slower_then, - - # ms_threshold=1/12 * 1e3, ) # unpack multi-referenced components @@ -548,48 +546,34 @@ def graphics_update_cycle( profiler('view incremented') - # from pprint import pformat - # frame_counts = { - # typ: len(frame) for typ, frame in frames_by_type.items() - # } - # print( - # f'{pformat(frame_counts)}\n' - # f'framed: {pformat(frames_by_type)}\n' - # f'lasts: {pformat(lasts)}\n' - # ) - # for typ, tick in lasts.items(): - # ticks_frame = quote.get('ticks', ()) + # iterate frames of ticks-by-type such that we only update graphics + # using the last update per type where possible. ticks_by_type = quote.get('tbt', {}) - - # for tick in ticks_frame: for typ, ticks in ticks_by_type.items(): # NOTE: ticks are `.append()`-ed to the `ticks_by_type: dict` by the # `._sampling.uniform_rate_send()` loop - tick = ticks[-1] - # typ = tick.get('type') + tick = ticks[-1] # get most recent value + price = tick.get('price') size = tick.get('size') # compute max and min prices (including bid/ask) from # tick frames to determine the y-range for chart # auto-scaling. - # TODO: we need a streaming minmax algo here, see def above. - if liv: + if ( + liv + + # TODO: make sure IB doesn't send ``-1``! + and price > 0 + ): mx = max(price + tick_margin, mx) mn = min(price - tick_margin, mn) + # clearing price update: + # generally, we only want to update grahpics from the *last* + # tick event once - thus showing the most recent state. if typ in clear_types: - # XXX: if we only wanted to update graphics from the - # "current"/"latest received" clearing price tick - # once (see alt iteration order above). - # if last_clear_updated: - # continue - - # last_clear_updated = True - # we only want to update grahpics from the *last* - # tick event that falls under the "clearing price" - # set. # update price sticky(s) end_ic = array[-1][[ @@ -604,10 +588,7 @@ def graphics_update_cycle( chart.update_graphics_from_flow('bar_wap') # L1 book label-line updates - # XXX: is this correct for ib? - # if ticktype in ('trade', 'last'): - # if ticktype in ('last',): # 'size'): - if typ in ('last',): # 'size'): + if typ in ('last',): label = { l1.ask_label.fields['level']: l1.ask_label, @@ -623,40 +604,64 @@ def graphics_update_cycle( ) # TODO: on trades should we be knocking down - # the relevant L1 queue? + # the relevant L1 queue manually ourselves? # label.size -= size + # NOTE: right now we always update the y-axis labels + # despite the last datum not being in view. Ideally + # we have a guard for this when we detect that the range + # of those values is not in view and then we disable these + # blocks. elif ( typ in _tick_groups['asks'] - # TODO: instead we could check if the price is in the - # y-view-range? - and liv ): l1.ask_label.update_fields({'level': price, 'size': size}) elif ( typ in _tick_groups['bids'] - # TODO: instead we could check if the price is in the - # y-view-range? - and liv ): l1.bid_label.update_fields({'level': price, 'size': size}) - # check for y-range re-size - if (mx > varz['last_mx']) or (mn < varz['last_mn']): + # check for y-autorange re-size + lmx = varz['last_mx'] + lmn = varz['last_mn'] + mx_diff = mx - lmx + mn_diff = mn - lmn - # fast chart resize case + if ( + mx_diff + or mn_diff + ): if ( + abs(mx_diff) > .25 * lmx + or + abs(mn_diff) > .25 * lmn + ): + log.error( + f'WTF MN/MX IS WAY OFF:\n' + f'lmn: {lmn}\n' + f'mn: {mn}\n' + f'lmx: {lmx}\n' + f'mx: {mx}\n' + f'mx_diff: {mx_diff}\n' + f'mn_diff: {mn_diff}\n' + ) + # fast chart resize case + elif ( liv and not chart._static_yrange == 'axis' ): - # main_vb = chart.view main_vb = chart._vizs[fqsn].plot.vb if ( main_vb._ic is None or not main_vb._ic.is_set() ): - # print(f'updating range due to mxmn') + yr = (mn, mx) + # print( + # f'updating y-range due to mxmn\n' + # f'{fqsn}: {yr}' + # ) + main_vb._set_yrange( # TODO: we should probably scale # the view margin based on the size @@ -664,11 +669,10 @@ def graphics_update_cycle( # slap in orders outside the current # L1 (only) book range. # range_margin=0.1, - # yrange=(mn, mx), + yrange=yr ) # check if slow chart needs a resize - hist_viz = hist_chart._vizs[fqsn] ( _, @@ -685,7 +689,7 @@ def graphics_update_cycle( if hist_liv: hist_viz.plot.vb._set_yrange() - # XXX: update this every draw cycle to make L1-always-in-view work. + # XXX: update this every draw cycle to make varz['last_mx'], varz['last_mn'] = mx, mn # run synchronous update on all linked viz @@ -761,10 +765,8 @@ def graphics_update_cycle( if ( mx_vlm_in_view != varz['last_mx_vlm'] ): - yrange = (0, mx_vlm_in_view * 1.375) - vlm_chart.view._set_yrange( - yrange=yrange, - ) + vlm_yr = (0, mx_vlm_in_view * 1.375) + vlm_chart.view._set_yrange(yrange=vlm_yr) profiler('`vlm_chart.view._set_yrange()`') # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') varz['last_mx_vlm'] = mx_vlm_in_view