From e05c258914bd0a5409eba242ba5fe867e85e10e1 Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 5 Apr 2026 14:00:15 -0400 Subject: [PATCH] Nest nursery scope inside EMS ctx in order mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the `trio.open_nursery()` + `collapse_eg()` block to be nested inside `open_ems()` instead of as a sibling — nursery tasks (order handling, trade processing) depend on the EMS connection staying alive. Also, - Default `'paper'` entry upfront in `accounts` dict and raise `ConfigurationError` with an actionable msg when a `brokerd_accounts` fqan isn't found in `accounts_def`. - Use `msg.setdefault('brokerd_msg', msg)` for ems dialog msgs instead of blind assignment; warn if already set. - Alias `asynccontextmanager as acm`. - Use `fqan` var naming for fully-qualified account names, `broker_name` from `mkt.broker`. - Tighten log msg formatting (`fqme!r`). (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/ui/order_mode.py | 248 +++++++++++++++++++++++------------------ 1 file changed, 138 insertions(+), 110 deletions(-) diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 7ce39b86..806edf95 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -19,7 +19,7 @@ Chart trading, the only way to scalp. """ from __future__ import annotations -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager as acm from dataclasses import dataclass, field from decimal import Decimal from functools import partial @@ -779,7 +779,7 @@ class OrderMode: return maybe_dialog -@asynccontextmanager +@acm async def open_order_mode( feed: Feed, godw: GodWidget, @@ -814,6 +814,7 @@ async def open_order_mode( # spawn EMS actor-service async with ( + # tractor.trionics.collapse_eg(), open_ems( fqme, loglevel=loglevel, @@ -824,11 +825,10 @@ async def open_order_mode( brokerd_accounts, ems_dialog_msgs, ), - tractor.trionics.collapse_eg(), - trio.open_nursery() as tn, - ): - log.info(f'Opening order mode for {fqme}') + log.info( + f'Opening order-mode for {fqme!r}' + ) # annotations editors lines = LineEditor(godw=godw) @@ -841,21 +841,34 @@ async def open_order_mode( trackers: dict[str, PositionTracker] = {} # load account names from ``brokers.toml`` + broker_name: str = mkt.broker accounts_def: bidict[str, str | None] = config.load_accounts( - providers=[mkt.broker], + providers=[broker_name], ) # XXX: ``brokerd`` delivers a set of account names that it # allows use of but the user also can define the accounts they'd # like to use, in order, in their `brokers.toml` file. - accounts: dict[str, str] = {} - for name in brokerd_accounts: - # ensure name is in ``brokers.toml`` - accounts[name] = accounts_def[name] + accounts: dict[str, str] = { + 'paper': 'paper', + } + fqan: str # ex. 'kraken.spot' + for fqan in brokerd_accounts: + # ensure fully-qualified-account-name declared in `brokers.toml` - # always add a paper entry so that paper cleared + try: + accounts[fqan] = accounts_def[fqan] + except KeyError as ke: + raise config.ConfigurationError( + f'The {broker_name!r} account {fqan!r} could not be found?\n' + f'\n' + f'Did you forget to define the correct account name in your `brokers.toml`?\n' + f'{ppfmt(accounts_def)}\n' + ) from ke + + # NOTE, always add a paper entry so that paper cleared # order dialogs can be tracked in the order mode UIs. - accounts['paper'] = 'paper' + # accounts['paper'] = 'paper' # first account listed is the one we select at startup # (aka order based selection). @@ -940,110 +953,124 @@ async def open_order_mode( for name, tracker in trackers.items(): order_pane.update_account_icons({name: tracker.live_pp}) - # top level abstraction which wraps all this crazyness into - # a namespace.. - mode = OrderMode( - godw, - feed, - chart, - hist_chart, - tn, - client, - lines, - arrows, - multistatus, - pane=order_pane, - trackers=trackers, - - ) - # XXX: MUST be set - order_pane.order_mode = mode - - # select a pp to track - tracker: PositionTracker = trackers[pp_account] - mode.current_pp = tracker - tracker.nav.show() - tracker.nav.hide_info() - - # XXX: would love to not have to do this separate from edit - # fields (which are done in an async loop - see below) - # connect selection signals (from drop down widgets) - # to order sync pane handler - for key in ('account', 'size_unit',): - w = form.fields[key] - w.currentTextChanged.connect( - partial( - order_pane.on_selection_change, - key=key, - ) - ) - - # make fill bar and positioning snapshot - order_pane.update_status_ui(tracker) - - # TODO: create a mode "manager" of sorts? - # -> probably just call it "UxModes" err sumthin? - # so that view handlers can access it - chart.view.order_mode = mode - hist_chart.view.order_mode = mode - - order_pane.on_ui_settings_change('account', pp_account) - mode.pane.display_pnl(mode.current_pp) - - # Begin order-response streaming - done() - - # Pack position messages by account, should only be one-to-one. - # NOTE: requires the backend exactly specifies - # the expected symbol key in its positions msg. - for ( - (broker, acctid), - pps_by_fqme - ) in position_msgs.items(): - for msg in pps_by_fqme.values(): - await process_trade_msg( - mode, - client, - msg, - ) - async with ( - - # pp pane kb inputs - open_form_input_handling( - form, - focus_next=chart.linked.godwidget, - on_value_change=order_pane.on_ui_settings_change, - ), - + tractor.trionics.collapse_eg(), + trio.open_nursery() as tn, ): - # signal to top level symbol loading task we're ready - # to handle input since the ems connection is ready - started.set() + # top level abstraction which wraps all this crazyness into + # a namespace.. + mode = OrderMode( + godw, + feed, + chart, + hist_chart, + tn, + client, + lines, + arrows, + multistatus, + pane=order_pane, + trackers=trackers, - for oid, msg in ems_dialog_msgs.items(): + ) + # XXX: MUST be set + order_pane.order_mode = mode - # HACK ALERT: ensure a resp field is filled out since - # techincally the call below expects a ``Status``. TODO: - # parse into proper ``Status`` equivalents ems-side? - # msg.setdefault('resp', msg['broker_details']['resp']) - # msg.setdefault('oid', msg['broker_details']['oid']) - msg['brokerd_msg'] = msg + # select a pp to track + tracker: PositionTracker = trackers[pp_account] + mode.current_pp = tracker + tracker.nav.show() + tracker.nav.hide_info() - await process_trade_msg( - mode, - client, - msg, + # XXX: would love to not have to do this separate from edit + # fields (which are done in an async loop - see below) + # connect selection signals (from drop down widgets) + # to order sync pane handler + for key in ('account', 'size_unit',): + w = form.fields[key] + w.currentTextChanged.connect( + partial( + order_pane.on_selection_change, + key=key, + ) ) - tn.start_soon( - process_trades_and_update_ui, - trades_stream, - mode, - client, - ) + # make fill bar and positioning snapshot + order_pane.update_status_ui(tracker) - yield mode + # TODO: create a mode "manager" of sorts? + # -> probably just call it "UxModes" err sumthin? + # so that view handlers can access it + chart.view.order_mode = mode + hist_chart.view.order_mode = mode + + order_pane.on_ui_settings_change('account', pp_account) + mode.pane.display_pnl(mode.current_pp) + + # Begin order-response streaming + done() + + # Pack position messages by account, should only be one-to-one. + # NOTE: requires the backend exactly specifies + # the expected symbol key in its positions msg. + for ( + (broker, acctid), + pps_by_fqme + ) in position_msgs.items(): + for msg in pps_by_fqme.values(): + await process_trade_msg( + mode, + client, + msg, + ) + + async with ( + + # pp pane kb inputs + open_form_input_handling( + form, + focus_next=chart.linked.godwidget, + on_value_change=order_pane.on_ui_settings_change, + ), + + ): + # signal to top level symbol loading task we're ready + # to handle input since the ems connection is ready + started.set() + + for oid, msg in ems_dialog_msgs.items(): + + # HACK ALERT: ensure a resp field is filled out since + # techincally the call below expects a ``Status``. TODO: + # parse into proper ``Status`` equivalents ems-side? + # msg.setdefault('resp', msg['broker_details']['resp']) + # msg.setdefault('oid', msg['broker_details']['oid']) + ya_msg: dict = msg.setdefault( + 'brokerd_msg', + msg, + ) + if msg is not ya_msg: + log.warning( + f'A `.brokerd_msg` was already set for ems-dialog msg?\n' + f'oid: {oid!r}\n' + f'ya_msg: {ya_msg!r}\n' + f'msg: {ya_msg!r}\n' + ) + + await process_trade_msg( + mode, + client, + msg, + ) + + tn.start_soon( + process_trades_and_update_ui, + trades_stream, + mode, + client, + ) + + yield mode async def process_trades_and_update_ui( @@ -1281,7 +1308,8 @@ async def process_trade_msg( # that should never happen tho? action: str = ( getattr(order, 'action', None) - or order['action'] + or + order['action'] ) details: dict = msg.brokerd_msg