From 3f48098c551d0364d8a29be4ce50dd25d63dbcae Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 27 Sep 2025 11:55:35 -0400 Subject: [PATCH] ui.order_mode: prioritize mkt-match on `.bs_mktid` For backends which opt to set the new `BrokerdPosition.bs_mktid` field, give (matching logic) priority to it such that even if the `.symbol` field doesn't match the mkt currently focussed on chart, it will always match on a provider's own internal asset-mapping-id. The original fallback logic for `.fqme` matching is left as is. As an example with IB, a qqq.nasdaq.ib txn may have been filled on a non-primary venue as qqq.directedea.ib, in this case if the mkt is displayed and focused on chart we want the **entire position info** to be overlayed by the `OrderMode` UX without discrepancy. Other refinements, - improve logging and add a detailed edge-case-comment around the `.on_fill()` handler to clarify where if a benign 'error' msg is relayed from a backend it will cause the UI to operate as though the order **was not-cleared/cancelled** since the `.on_cancel()` handler will have likely been called just before, popping the `.dialogs` entry. Return `bool` to indicate whether the UI removed-lines / added-fill-arrows. - inverse the `return` branching logic in `.on_cancel()` to reduce indent. - add a very loud `log.error()` in `Status(resp='error')` case-block ensuring the console yells about the order being cancelled, also a todo for the weird msg-field recursion nonsense.. --- piker/ui/order_mode.py | 161 ++++++++++++++++++++++++++--------------- 1 file changed, 103 insertions(+), 58 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index ea6d498a..f1f0e62f 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -555,14 +555,13 @@ class OrderMode: def on_fill( self, - uuid: str, price: float, time_s: float, pointing: str | None = None, - ) -> None: + ) -> bool: ''' Fill msg handler. @@ -575,60 +574,83 @@ class OrderMode: - update fill bar size ''' - dialog = self.dialogs[uuid] + # XXX WARNING XXX + # if a `Status(resp='error')` arrives *before* this + # fill-status, the `.dialogs` entry may have already been + # popped and thus the below will skipped. + # + # NOTE, to avoid this confusing scenario ensure that any + # errors delivered thru from the broker-backend are not just + # "noisy reporting" (like is very common from IB..) and are + # instead ONLY errors-causing-order-dialog-cancellation! + if not (dialog := self.dialogs.get(uuid)): + log.warning( + f'Order was already cleared from `.dialogs` ??\n' + f'uuid: {uuid!r}\n' + ) + return False + lines = dialog.lines chart = self.chart - # XXX: seems to fail on certain types of races? - # assert len(lines) == 2 - if lines: - flume: Flume = self.feed.flumes[chart.linked.mkt.fqme] - _, _, ratio = flume.get_ds_info() - - for chart, shm in [ - (self.chart, flume.rt_shm), - (self.hist_chart, flume.hist_shm), - ]: - viz = chart.get_viz(chart.name) - index_field = viz.index_field - arr = shm.array - - # TODO: borked for int index based.. - index = flume.get_index(time_s, arr) - - # get absolute index for arrow placement - arrow_index = arr[index_field][index] - - self.arrows.add( - chart.plotItem, - uuid, - arrow_index, - price, - pointing=pointing, - color=lines[0].color - ) - else: + if not lines: log.warn("No line(s) for order {uuid}!?") + return False + + # update line state(s) + # + # ?XXX this fails on certain types of races? + # assert len(lines) == 2 + flume: Flume = self.feed.flumes[chart.linked.mkt.fqme] + _, _, ratio = flume.get_ds_info() + + for chart, shm in [ + (self.chart, flume.rt_shm), + (self.hist_chart, flume.hist_shm), + ]: + viz = chart.get_viz(chart.name) + index_field = viz.index_field + arr = shm.array + + # TODO: borked for int index based.. + index = flume.get_index(time_s, arr) + + # get absolute index for arrow placement + arrow_index = arr[index_field][index] + + self.arrows.add( + chart.plotItem, + uuid, + arrow_index, + price, + pointing=pointing, + color=lines[0].color + ) def on_cancel( self, - uuid: str + uuid: str, - ) -> None: + ) -> bool: - msg: Order = self.client._sent_orders.pop(uuid, None) - - if msg is not None: - self.lines.remove_line(uuid=uuid) - self.chart.linked.cursor.show_xhair() - - dialog = self.dialogs.pop(uuid, None) - if dialog: - dialog.last_status_close() - else: + msg: Order|None = self.client._sent_orders.pop(uuid, None) + if msg is None: log.warning( f'Received cancel for unsubmitted order {pformat(msg)}' ) + return False + + # remove GUI line, show cursor. + self.lines.remove_line(uuid=uuid) + self.chart.linked.cursor.show_xhair() + + # remove msg dialog (history) + dialog: Dialog|None = self.dialogs.pop(uuid, None) + if dialog: + dialog.last_status_close() + + return True + def cancel_orders_under_cursor(self) -> list[str]: return self.cancel_orders( @@ -1057,13 +1079,23 @@ async def process_trade_msg( if name in ( 'position', ): - sym: MktPair = mode.chart.linked.mkt + mkt: MktPair = mode.chart.linked.mkt pp_msg_symbol = msg['symbol'].lower() - fqme = sym.fqme - broker = sym.broker + pp_msg_bsmktid = msg['bs_mktid'] + fqme = mkt.fqme + broker = mkt.broker if ( + # match on any backed-specific(-unique)-ID first! + ( + pp_msg_bsmktid + and + mkt.bs_mktid == pp_msg_bsmktid + ) + or + # OW try against what's provided as an FQME.. pp_msg_symbol == fqme - or pp_msg_symbol == fqme.removesuffix(f'.{broker}') + or + pp_msg_symbol == fqme.removesuffix(f'.{broker}') ): log.info( f'Loading position for `{fqme}`:\n' @@ -1086,7 +1118,7 @@ async def process_trade_msg( return msg = Status(**msg) - resp = msg.resp + # resp: str = msg.resp oid = msg.oid dialog: Dialog = mode.dialogs.get(oid) @@ -1150,19 +1182,32 @@ async def process_trade_msg( mode.on_submit(oid) case Status(resp='error'): - - # do all the things for a cancel: - # - drop order-msg dialog from client table - # - delete level line from view - mode.on_cancel(oid) - # TODO: parse into broker-side msg, or should we # expect it to just be **that** msg verbatim (since # we'd presumably have only 1 `Error` msg-struct) broker_msg: dict = msg.brokerd_msg + + # XXX NOTE, this presumes the rxed "error" is + # order-dialog-cancel-causing, THUS backends much ONLY + # relay errors of this "severity"!! log.error( - f'Order {oid}->{resp} with:\n{pformat(broker_msg)}' + f'Order errored ??\n' + f'oid: {oid!r}\n' + f'\n' + f'{pformat(broker_msg)}\n' + f'\n' + f'=> CANCELLING ORDER DIALOG <=\n' + + # from tractor.devx.pformat import ppfmt + # !TODO LOL, wtf the msg is causing + # a recursion bug! + # -[ ] get this shit on msgspec stat! + # f'{ppfmt(broker_msg)}' ) + # do all the things for a cancel: + # - drop order-msg dialog from client table + # - delete level line from view + mode.on_cancel(oid) case Status(resp='canceled'): # delete level line from view @@ -1178,10 +1223,10 @@ async def process_trade_msg( # TODO: UX for a "pending" clear/live order log.info(f'Dark order triggered for {fmtmsg}') + # TODO: do the struct-msg version, blah blah.. + # req=Order(exec_mode='live', action='alert') as req, case Status( resp='triggered', - # TODO: do the struct-msg version, blah blah.. - # req=Order(exec_mode='live', action='alert') as req, req={ 'exec_mode': 'live', 'action': 'alert',