Nest nursery scope inside EMS ctx in order mode

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
nested_order_mode_block
Gud Boi 2026-04-05 14:00:15 -04:00
parent 5387538ba9
commit e05c258914
1 changed files with 138 additions and 110 deletions

View File

@ -19,7 +19,7 @@ Chart trading, the only way to scalp.
""" """
from __future__ import annotations from __future__ import annotations
from contextlib import asynccontextmanager from contextlib import asynccontextmanager as acm
from dataclasses import dataclass, field from dataclasses import dataclass, field
from decimal import Decimal from decimal import Decimal
from functools import partial from functools import partial
@ -779,7 +779,7 @@ class OrderMode:
return maybe_dialog return maybe_dialog
@asynccontextmanager @acm
async def open_order_mode( async def open_order_mode(
feed: Feed, feed: Feed,
godw: GodWidget, godw: GodWidget,
@ -814,6 +814,7 @@ async def open_order_mode(
# spawn EMS actor-service # spawn EMS actor-service
async with ( async with (
# tractor.trionics.collapse_eg(),
open_ems( open_ems(
fqme, fqme,
loglevel=loglevel, loglevel=loglevel,
@ -824,11 +825,10 @@ async def open_order_mode(
brokerd_accounts, brokerd_accounts,
ems_dialog_msgs, 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 # annotations editors
lines = LineEditor(godw=godw) lines = LineEditor(godw=godw)
@ -841,21 +841,34 @@ async def open_order_mode(
trackers: dict[str, PositionTracker] = {} trackers: dict[str, PositionTracker] = {}
# load account names from ``brokers.toml`` # load account names from ``brokers.toml``
broker_name: str = mkt.broker
accounts_def: bidict[str, str | None] = config.load_accounts( 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 # XXX: ``brokerd`` delivers a set of account names that it
# allows use of but the user also can define the accounts they'd # allows use of but the user also can define the accounts they'd
# like to use, in order, in their `brokers.toml` file. # like to use, in order, in their `brokers.toml` file.
accounts: dict[str, str] = {} accounts: dict[str, str] = {
for name in brokerd_accounts: 'paper': 'paper',
# ensure name is in ``brokers.toml`` }
accounts[name] = accounts_def[name] 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. # 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 # first account listed is the one we select at startup
# (aka order based selection). # (aka order based selection).
@ -940,110 +953,124 @@ async def open_order_mode(
for name, tracker in trackers.items(): for name, tracker in trackers.items():
order_pane.update_account_icons({name: tracker.live_pp}) 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 ( async with (
tractor.trionics.collapse_eg(),
# pp pane kb inputs trio.open_nursery() as tn,
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 # top level abstraction which wraps all this crazyness into
# to handle input since the ems connection is ready # a namespace..
started.set() 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 # select a pp to track
# techincally the call below expects a ``Status``. TODO: tracker: PositionTracker = trackers[pp_account]
# parse into proper ``Status`` equivalents ems-side? mode.current_pp = tracker
# msg.setdefault('resp', msg['broker_details']['resp']) tracker.nav.show()
# msg.setdefault('oid', msg['broker_details']['oid']) tracker.nav.hide_info()
msg['brokerd_msg'] = msg
await process_trade_msg( # XXX: would love to not have to do this separate from edit
mode, # fields (which are done in an async loop - see below)
client, # connect selection signals (from drop down widgets)
msg, # 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( # make fill bar and positioning snapshot
process_trades_and_update_ui, order_pane.update_status_ui(tracker)
trades_stream,
mode,
client,
)
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( async def process_trades_and_update_ui(
@ -1281,7 +1308,8 @@ async def process_trade_msg(
# that should never happen tho? # that should never happen tho?
action: str = ( action: str = (
getattr(order, 'action', None) getattr(order, 'action', None)
or order['action'] or
order['action']
) )
details: dict = msg.brokerd_msg details: dict = msg.brokerd_msg