diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py index 4d197f5a..b6fb9281 100644 --- a/piker/ui/_axes.py +++ b/piker/ui/_axes.py @@ -306,7 +306,10 @@ class DynamicDateAxis(Axis): times = array['time'] i_0, i_l = times[0], times[-1] + # edge cases if ( + not indexes + or (indexes[0] < i_0 and indexes[-1] < i_l) or diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 14921b3d..7f078a47 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -829,8 +829,6 @@ class ChartPlotWidget(pg.PlotWidget): sig_mouse_leave = QtCore.pyqtSignal(object) sig_mouse_enter = QtCore.pyqtSignal(object) - _l1_labels: L1Labels = None - mode_name: str = 'view' # TODO: can take a ``background`` color setting - maybe there's @@ -986,13 +984,15 @@ class ChartPlotWidget(pg.PlotWidget): ''' # TODO: compute some sensible maximum value here # and use a humanized scheme to limit to that length. - l1_len = self._max_l1_line_len + from ._l1 import L1Label + l1_len = abs(L1Label._x_br_offset) ryaxis = self.getAxis('right') r_axis_x = ryaxis.pos().x() up_to_l1_sc = r_axis_x - l1_len marker_right = up_to_l1_sc - (1.375 * 2 * marker_size) - line_end = marker_right - (6/16 * marker_size) + # line_end = marker_right - (6/16 * marker_size) + line_end = marker_right - marker_size # print( # f'r_axis_x: {r_axis_x}\n' @@ -1231,7 +1231,8 @@ class ChartPlotWidget(pg.PlotWidget): # (we need something that avoids clutter on x-axis). axis.add_sticky( pi=pi, - bg_color=color, + fg_color='black', + # bg_color=color, digits=digits, ) diff --git a/piker/ui/_cursor.py b/piker/ui/_cursor.py index 762acf73..3172be1a 100644 --- a/piker/ui/_cursor.py +++ b/piker/ui/_cursor.py @@ -71,7 +71,7 @@ class LineDot(pg.CurvePoint): plot: ChartPlotWidget, # type: ingore # noqa pos=None, - color: str = 'default_light', + color: str = 'bracket', ) -> None: # scale from dpi aware font size @@ -349,7 +349,7 @@ class Cursor(pg.GraphicsObject): # XXX: not sure why these are instance variables? # It's not like we can change them on the fly..? self.pen = pg.mkPen( - color=hcolor('default'), + color=hcolor('bracket'), style=QtCore.Qt.DashLine, ) self.lines_pen = pg.mkPen( @@ -365,7 +365,7 @@ class Cursor(pg.GraphicsObject): self._lw = self.pixelWidth() * self.lines_pen.width() # xhair label's color name - self.label_color: str = 'default' + self.label_color: str = 'bracket' self._y_label_update: bool = True diff --git a/piker/ui/_dataviz.py b/piker/ui/_dataviz.py index 07ead769..d43377b9 100644 --- a/piker/ui/_dataviz.py +++ b/piker/ui/_dataviz.py @@ -217,6 +217,9 @@ def render_baritems( ) +_sample_rates: set[float] = {1, 60} + + class Viz(msgspec.Struct): # , frozen=True): ''' (Data) "Visualization" compound type which wraps a real-time @@ -284,15 +287,33 @@ class Viz(msgspec.Struct): # , frozen=True): reset: bool = False, ) -> float: + + # attempt to dectect the best step size by scanning a sample of + # the source data. if self._index_step is None: + index = self.shm.array[self.index_field] isample = index[:16] - mxdiff = np.diff(isample).max() + + mxdiff: None | float = None + for step in np.diff(isample): + if step in _sample_rates: + if ( + mxdiff is not None + and step != mxdiff + ): + raise ValueError( + f'Multiple step sizes detected? {mxdiff}, {step}' + ) + mxdiff = step + self._index_step = max(mxdiff, 1) if ( mxdiff < 1 or 1 < mxdiff < 60 ): + # TODO: remove this once we're sure the above scan loop + # is rock solid. breakpoint() return self._index_step diff --git a/piker/ui/_display.py b/piker/ui/_display.py index e544b64d..482cdfb6 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -328,10 +328,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 @@ -416,11 +412,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) @@ -449,16 +445,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 @@ -471,10 +472,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 @@ -554,48 +552,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][[ @@ -610,10 +594,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, @@ -629,40 +610,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 @@ -670,11 +675,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] ( _, @@ -691,7 +695,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 @@ -767,10 +771,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 @@ -1047,6 +1049,10 @@ async def display_symbol_data( group_key=True ) + # (TODO: make this not so shit XD) + # close group status once a symbol feed fully loads to view. + # sbar._status_groups[loading_sym_key][1]() + # TODO: ctl over update loop's maximum frequency. # - load this from a config.toml! # - allow dyanmic configuration from chart UI? @@ -1129,6 +1135,8 @@ async def display_symbol_data( # and sub-charts for FSPs fqsn, flume = fitems[0] + # TODO NOTE: THIS CONTROLS WHAT SYMBOL IS USED FOR ORDER MODE + # SUBMISSIONS, we need to make this switch based on selection. rt_linked._symbol = flume.symbol hist_linked._symbol = flume.symbol @@ -1219,9 +1227,6 @@ async def display_symbol_data( # 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 @@ -1232,9 +1237,14 @@ async def display_symbol_data( name=fqsn, axis_title=fqsn, ) - hist_pi.hideAxis('left') + # only show a singleton bottom-bottom axis by default. hist_pi.hideAxis('bottom') + # XXX: TODO: THIS WILL CAUSE A GAP ON OVERLAYS, + # i think it needs to be "removed" instead when there + # are none? + hist_pi.hideAxis('left') + viz = hist_chart.draw_curve( fqsn, hist_ohlcv, @@ -1311,36 +1321,14 @@ async def display_symbol_data( # XXX: if we wanted it at the bottom? # rt_linked.splitter.addWidget(hist_linked) - rt_linked.focus() - godwidget.resize_all() - - # add all additional symbols as overlays + # greedily do a view range default and pane resizing + # on startup before loading the order-mode machinery. for fqsn, flume in feed.flumes.items(): # size view to data prior to order mode init rt_chart.default_view() rt_linked.graphics_cycle() - await trio.sleep(0) - - hist_chart.default_view( - bars_from_y=int(len(hist_ohlcv.array)), # size to data - y_offset=6116*2, # push it a little away from the y-axis - ) - hist_linked.graphics_cycle() - await trio.sleep(0) - - godwidget.resize_all() - - # trigger another view reset if no sub-chart - hist_chart.default_view() - rt_chart.default_view() - # let qt run to render all widgets and make sure the - # sidepanes line up vertically. - await trio.sleep(0) - - # dynamic resize steps - godwidget.resize_all() # TODO: look into this because not sure why it was # commented out / we ever needed it XD @@ -1350,21 +1338,11 @@ async def display_symbol_data( # determine if auto-range adjustements should be made. # rt_linked.subplots.pop('volume', None) - # TODO: make this not so shit XD - # close group status - # sbar._status_groups[loading_sym_key][1]() - + hist_chart.default_view() hist_linked.graphics_cycle() - rt_chart.default_view() - await trio.sleep(0) - bars_in_mem = int(len(hist_ohlcv.array)) - hist_chart.default_view( - bars_from_y=bars_in_mem, # size to data - # push it 1/16th away from the y-axis - y_offset=round(bars_in_mem / 16), - ) godwidget.resize_all() + await trio.sleep(0) await link_views_with_region( rt_chart, @@ -1372,7 +1350,7 @@ async def display_symbol_data( flume, ) - # start graphics update loop after receiving first live quote + # start update loop task ln.start_soon( graphics_update_loop, ln, @@ -1383,20 +1361,31 @@ async def display_symbol_data( vlm_charts, ) + # boot order-mode + order_ctl_symbol: str = fqsns[0] mode: OrderMode async with ( open_order_mode( feed, godwidget, - fqsns[-1], + fqsns[0], order_mode_started ) as mode ): + rt_linked.mode = mode + viz = rt_chart.get_viz(order_ctl_symbol) + viz.plot.setFocus() + + # default view adjuments and sidepane alignment + # as final default UX touch. rt_chart.default_view() rt_chart.view.enable_auto_yrange() + hist_chart.default_view() hist_chart.view.enable_auto_yrange() + godwidget.resize_all() + await trio.sleep_forever() # let the app run.. bby diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index 5dee7b88..c91436da 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -661,6 +661,12 @@ async def open_vlm_displays( # str(period_param.default) # ) + # use slightly less light (then bracket) gray + # for volume from "main exchange" and a more "bluey" + # gray for "dark" vlm. + vlm_color = 'i3' + dark_vlm_color = 'charcoal' + # built-in vlm which we plot ASAP since it's # usually data provided directly with OHLC history. shm = ohlcv @@ -755,7 +761,7 @@ async def open_vlm_displays( { # fsp engine conf 'func_name': 'dolla_vlm', - 'zero_on_step': True, + 'zero_on_step': False, 'params': { 'price_func': { 'default_value': 'chl3', @@ -769,7 +775,7 @@ async def open_vlm_displays( # FIXME: we should error on starting the same fsp right # since it might collide with existing shm.. or wait we # had this before?? - # dolla_vlm, + # dolla_vlm tasks_ready.append(started) # profiler(f'created shm for fsp actor: {display_name}') @@ -786,19 +792,24 @@ async def open_vlm_displays( dvlm_pi = vlm_chart.overlay_plotitem( 'dolla_vlm', index=0, # place axis on inside (nearest to chart) + axis_title=' $vlm', - axis_side='right', + axis_side='left', + axis_kwargs={ 'typical_max_str': ' 100.0 M ', 'formatter': partial( humanize, digits=2, ), + 'text_color': vlm_color, }, ) - dvlm_pi.hideAxis('left') + # TODO: should this maybe be implicit based on input args to + # `.overlay_plotitem()` above? dvlm_pi.hideAxis('bottom') + # all to be overlayed curve names fields = [ 'dolla_vlm', @@ -823,12 +834,6 @@ async def open_vlm_displays( # add custom auto range handler dvlm_pi.vb._maxmin = group_mxmn - # use slightly less light (then bracket) gray - # for volume from "main exchange" and a more "bluey" - # gray for "dark" vlm. - vlm_color = 'i3' - dark_vlm_color = 'charcoal' - # add dvlm (step) curves to common view def chart_curves( names: list[str], @@ -879,7 +884,7 @@ async def open_vlm_displays( flow_rates, { # fsp engine conf 'func_name': 'flow_rates', - 'zero_on_step': False, + 'zero_on_step': True, }, # loglevel, ) @@ -913,8 +918,8 @@ async def open_vlm_displays( # TODO: dynamically update period (and thus this axis?) # title from user input. axis_title='clears', - axis_side='left', + axis_kwargs={ 'typical_max_str': ' 10.0 M ', 'formatter': partial( diff --git a/piker/ui/_l1.py b/piker/ui/_l1.py index 0c6e4e1a..23162c70 100644 --- a/piker/ui/_l1.py +++ b/piker/ui/_l1.py @@ -30,19 +30,20 @@ from ._pg_overrides import PlotItem class LevelLabel(YAxisLabel): - """Y-axis (vertically) oriented, horizontal label that sticks to + ''' + Y-axis (vertically) oriented, horizontal label that sticks to where it's placed despite chart resizing and supports displaying multiple fields. TODO: replace the rectangle-text part with our new ``Label`` type. - """ - _x_margin = 0 - _y_margin = 0 + ''' + _x_br_offset: float = -16 + _y_txt_h_scaling: float = 2 # adjustment "further away from" anchor point - _x_offset = 9 + _x_offset = 0 _y_offset = 0 # fields to be displayed in the label string @@ -58,12 +59,12 @@ class LevelLabel(YAxisLabel): chart, parent, - color: str = 'bracket', + color: str = 'default_light', orient_v: str = 'bottom', - orient_h: str = 'left', + orient_h: str = 'right', - opacity: float = 0, + opacity: float = 1, # makes order line labels offset from their parent axis # such that they don't collide with the L1/L2 lines/prices @@ -99,13 +100,15 @@ class LevelLabel(YAxisLabel): self._h_shift = { 'left': -1., - 'right': 0. + 'right': 0., }[orient_h] self.fields = self._fields.copy() # ensure default format fields are in correct self.set_fmt_str(self._fmt_str, self.fields) + self.setZValue(10) + @property def color(self): return self._hcolor @@ -113,7 +116,10 @@ class LevelLabel(YAxisLabel): @color.setter def color(self, color: str) -> None: self._hcolor = color - self._pen = self.pen = pg.mkPen(hcolor(color)) + self._pen = self.pen = pg.mkPen( + hcolor(color), + width=3, + ) def update_on_resize(self, vr, r): """Tiis is a ``.sigRangeChanged()`` handler. @@ -125,10 +131,11 @@ class LevelLabel(YAxisLabel): self, fields: dict = None, ) -> None: - """Update the label's text contents **and** position from + ''' + Update the label's text contents **and** position from a view box coordinate datum. - """ + ''' self.fields.update(fields) level = self.fields['level'] @@ -175,7 +182,8 @@ class LevelLabel(YAxisLabel): fields: dict, ): # use space as e3 delim - self.label_str = self._fmt_str.format(**fields).replace(',', ' ') + self.label_str = self._fmt_str.format( + **fields).replace(',', ' ') br = self.boundingRect() h, w = br.height(), br.width() @@ -188,14 +196,14 @@ class LevelLabel(YAxisLabel): self, p: QtGui.QPainter, rect: QtCore.QRectF - ) -> None: - p.setPen(self._pen) + ) -> None: + + p.setPen(self._pen) rect = self.rect if self._orient_v == 'bottom': lp, rp = rect.topLeft(), rect.topRight() - # p.drawLine(rect.topLeft(), rect.topRight()) elif self._orient_v == 'top': lp, rp = rect.bottomLeft(), rect.bottomRight() @@ -209,6 +217,11 @@ class LevelLabel(YAxisLabel): ]) ) + p.fillRect( + self.rect, + self.bg_color, + ) + def highlight(self, pen) -> None: self._pen = pen self.update() @@ -247,9 +260,10 @@ class L1Label(LevelLabel): class L1Labels: - """Level 1 bid ask labels for dynamic update on price-axis. + ''' + Level 1 bid ask labels for dynamic update on price-axis. - """ + ''' def __init__( self, plotitem: PlotItem, @@ -265,15 +279,17 @@ class L1Labels: 'chart': plotitem, 'parent': raxis, - 'opacity': 1, + 'opacity': .9, 'font_size': font_size, - 'fg_color': chart.pen_color, - 'bg_color': chart.view_color, + 'fg_color': 'default_light', + 'bg_color': chart.view_color, # normally 'papas_special' } + # TODO: add humanized source-asset + # info format. fmt_str = ( - ' {size:.{size_digits}f} x ' - '{level:,.{level_digits}f} ' + ' {size:.{size_digits}f} u' + # '{level:,.{level_digits}f} ' ) fields = { 'level': 0, @@ -286,12 +302,17 @@ class L1Labels: orient_v='bottom', **kwargs, ) - bid.set_fmt_str(fmt_str=fmt_str, fields=fields) + bid.set_fmt_str( + fmt_str='\n' + fmt_str, + fields=fields, + ) bid.show() ask = self.ask_label = L1Label( orient_v='top', **kwargs, ) - ask.set_fmt_str(fmt_str=fmt_str, fields=fields) + ask.set_fmt_str( + fmt_str=fmt_str, + fields=fields) ask.show() diff --git a/piker/ui/_label.py b/piker/ui/_label.py index 699a81ae..247b4cc0 100644 --- a/piker/ui/_label.py +++ b/piker/ui/_label.py @@ -233,6 +233,36 @@ class Label: def delete(self) -> None: self.vb.scene().removeItem(self.txt) + # NOTE: pulled out from ``ChartPlotWidget`` from way way old code. + # def _label_h(self, yhigh: float, ylow: float) -> float: + # # compute contents label "height" in view terms + # # to avoid having data "contents" overlap with them + # if self._labels: + # label = self._labels[self.name][0] + + # rect = label.itemRect() + # tl, br = rect.topLeft(), rect.bottomRight() + # vb = self.plotItem.vb + + # try: + # # on startup labels might not yet be rendered + # top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y()) + + # # XXX: magic hack, how do we compute exactly? + # label_h = (top - bottom) * 0.42 + + # except np.linalg.LinAlgError: + # label_h = 0 + # else: + # label_h = 0 + + # # print(f'label height {self.name}: {label_h}') + + # if label_h > yhigh - ylow: + # label_h = 0 + + # print(f"bounds (ylow, yhigh): {(ylow, yhigh)}") + class FormatLabel(QLabel): ''' diff --git a/piker/ui/_overlay.py b/piker/ui/_overlay.py index ac15a9dc..0fb747e7 100644 --- a/piker/ui/_overlay.py +++ b/piker/ui/_overlay.py @@ -361,8 +361,8 @@ class PlotItemOverlay: if not sub_handlers: src_handler = getattr( - root.vb, - ev_name, + root.vb, + ev_name, ) def broadcast( diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 4ac2f457..e339739e 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -349,7 +349,7 @@ class OrderMode: ''' if not order: - staged = self._staged_order + staged: Order = self._staged_order # apply order fields for ems oid = str(uuid.uuid4()) order = staged.copy() @@ -703,7 +703,6 @@ async def open_order_mode( # symbol id symbol = chart.linked.symbol - symkey = symbol.front_fqsn() # map of per-provider account keys to position tracker instances trackers: dict[str, PositionTracker] = {} @@ -864,7 +863,7 @@ async def open_order_mode( # the expected symbol key in its positions msg. for (broker, acctid), msgs in position_msgs.items(): for msg in msgs: - log.info(f'Loading pp for {symkey}:\n{pformat(msg)}') + log.info(f'Loading pp for {acctid}@{broker}:\n{pformat(msg)}') await process_trade_msg( mode, book,