Skip to content

feat(executor): every order is an OrderSpec, and the second Coinbase renderer is gone (#524) - #569

Merged
eaitbrahim merged 1 commit into
mainfrom
feat-524-flip
Aug 27, 2026
Merged

feat(executor): every order is an OrderSpec, and the second Coinbase renderer is gone (#524)#569
eaitbrahim merged 1 commit into
mainfrom
feat-524-flip

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

The flip. The executor placed orders by handing hand-built Coinbase dicts to a pre-port
client; it now builds OrderSpec values and reads Preview/PlaceResult.

The bytes on the wire are unchanged, and that is verified rather than asserted. Ten order
configurations rendered by the new path were compared against the pre-flip renderers loaded out
of git
— the bracket across four size/price/increment combinations, plus every market leg — and
are byte-identical.

One renderer, not two

CoinbaseClient.preview_order/place_order take a spec and render it through
keel_broker_coinbase.translate.to_order_configuration — the adapter's own function.
executor._bracket_order_configuration is deleted, and with it the test #502 stage 1 shipped to
pin the two byte-identical while both existed. That test's own words: "The test imports both;
production code does not."
There is one now, so there is nothing left to hold in agreement.

_order_configuration becomes _order_spec: BUY is MarketIOCByQuote, SELL is
MarketIOCByBase, the bracket is BracketGTC. #516's quantization is untouched and still runs
before the spec is built, including its deliberate BUY/SELL asymmetry.

The trap I nearly walked into

Every OrderSpec carries an initial_status ClassVar, and using it for the order row's status is
the obvious move and wrong. The port's vocabulary is the venue's (filled_or_rejected,
open); this column is keel's (filled, pending).

reconcile sweeps for pending. Writing open would leave every resting order invisible to the
sweep that exists to observe its fill
— a bracket recorded as open is a protective order keel
would never look at again.

So _initial_status stays, mapping spec.kind to keel's words. What went is the dict inspection
(next(iter(order_configuration), "")), not the vocabulary. Caught by a test asserting 'filled',
which is exactly what that test was for.

Smaller things the types made obvious

Tests

~13 fakes across five files moved to the port's signatures. The dict-shaped preview payloads are
deliberately kept as dicts at the call sites and converted by one helper: dozens of tests build
a bespoke preview to exercise one degraded field (a missing best_bid, a non-numeric best_ask),
and rewriting each into a constructor would have been a larger diff than the change it accompanies,
with more chances to alter a case by accident.

Verification

4301 passed / 3 skipped, ruff clean, mypy clean across 347 files, one paper cycle run against real
venue data, and the byte-identity check above.

What's left of #524

_build_broker still constructs CoinbaseClient rather than resolving through load_broker. That
is now a small change — the client and every adapter speak the same interface — but it is gated
on two consumers that are not port methods: assets discover's list_products, and assets holdings' get_accounts. Both want a decision (a port catalogue read? holdings onto
get_balances?) rather than a mechanical move, so they belong in their own change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2

…renderer is gone (#524)

The flip. The executor placed orders by handing hand-built Coinbase dicts to a
pre-port client; it now builds `OrderSpec` values and reads `Preview`/
`PlaceResult`. **The bytes on the wire are unchanged, and that is verified rather
than asserted** -- ten configurations rendered by the new path were compared
against the PRE-FLIP renderers loaded out of git, and are byte-identical.

── ONE RENDERER, NOT TWO ──────────────────────────────────────────────────────

`CoinbaseClient.preview_order`/`place_order` take a spec and render it through
`keel_broker_coinbase.translate.to_order_configuration` -- the adapter's own
function. `executor._bracket_order_configuration` is deleted, and with it the
test #502 stage 1 shipped to pin the two byte-identical while both existed. That
test's own words: "The test imports both; production code does not." There is one
now, so there is nothing left to hold in agreement.

`_order_configuration` becomes `_order_spec`: BUY is `MarketIOCByQuote`, SELL is
`MarketIOCByBase`, the bracket is `BracketGTC`. #516's quantization is untouched
and still runs before the spec is built, including its deliberate BUY/SELL
asymmetry.

── THE TRAP I NEARLY WALKED INTO ──────────────────────────────────────────────

Every `OrderSpec` carries an `initial_status` ClassVar, and using it for the
order row's status is the obvious move and WRONG. The port's vocabulary is the
venue's (`filled_or_rejected`, `open`); this column is keel's (`filled`,
`pending`). `reconcile` sweeps for `pending`, so writing `open` would leave every
resting order invisible to the sweep that exists to observe its fill -- a bracket
recorded as `open` is a protective order keel would never look at again.

`_initial_status` therefore stays, mapping `spec.kind` to KEEL's words. What went
is the dict inspection (`next(iter(order_configuration), "")`), not the
vocabulary. Caught by a test asserting `'filled'`, which is exactly what that
test was for.

── SMALLER THINGS THE TYPES MADE OBVIOUS ──────────────────────────────────────

`raw_response` stored the whole placement response as JSON so that
`_native_order_id` could dig the id out later to cancel with. It stores
`{"order_id": ...}` now, from `PlaceResult.broker_order_id`. No migration: both
shapes answer the same `data.get("order_id")`, so old rows read unchanged.

`_preview_book` already accepted `Preview | dict` and the Coinbase adapter already
carried the book in `detail` -- the executor was written anticipating this -- so
#350's spread gate and #332's override warning came through untouched.

── TESTS ──────────────────────────────────────────────────────────────────────

~13 fakes across five files moved to the port's signatures. The dict-shaped
preview payloads are kept AS dicts at the call sites and converted by one helper:
dozens of tests build a bespoke preview to exercise one degraded field, and
rewriting each into a constructor would have been a bigger diff than the change
it accompanies, with more chances to alter a case by accident.

Gates: 4301 passed / 3 skipped, ruff clean, mypy clean across 347 files, one
paper cycle run against real venue data.

── WHAT IS LEFT OF #524 ───────────────────────────────────────────────────────

`_build_broker` still constructs `CoinbaseClient` rather than resolving through
`load_broker`. That is now a SMALL change -- the client and every adapter speak
the same interface -- gated on two consumers that are not port methods:
`assets discover`'s `list_products`, and `assets holdings`' `get_accounts`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant