feat(executor): every order is an OrderSpec, and the second Coinbase renderer is gone (#524) - #569
Merged
Conversation
…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
This was referenced Aug 28, 2026
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The flip. The executor placed orders by handing hand-built Coinbase dicts to a pre-port
client; it now builds
OrderSpecvalues and readsPreview/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_ordertake a spec and render it throughkeel_broker_coinbase.translate.to_order_configuration— the adapter's own function.executor._bracket_order_configurationis deleted, and with it the test #502 stage 1 shipped topin 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_configurationbecomes_order_spec: BUY isMarketIOCByQuote, SELL isMarketIOCByBase, the bracket isBracketGTC. #516's quantization is untouched and still runsbefore the spec is built, including its deliberate BUY/SELL asymmetry.
The trap I nearly walked into
Every
OrderSpeccarries aninitial_statusClassVar, and using it for the order row's status isthe obvious move and wrong. The port's vocabulary is the venue's (
filled_or_rejected,open); this column is keel's (filled,pending).reconcilesweeps forpending. Writingopenwould leave every resting order invisible to thesweep that exists to observe its fill — a bracket recorded as
openis a protective order keelwould never look at again.
So
_initial_statusstays, mappingspec.kindto 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_responsestored the entire placement response as JSON so_native_order_idcould dig theid back out to cancel with. It stores
{"order_id": ...}now, fromPlaceResult.broker_order_id.No migration needed — both shapes answer the same
data.get("order_id"), so pre-existing rowsread unchanged.
_preview_bookalready acceptedPreview | dictand the Coinbase adapter already carried thebook in
detail, so A routing-time maximum-spread gate — refuse entries when the live book is too wide #350's spread gate and fix(executor): warn loudly when a conditional entry price is overridden by market routing (#260) #332's override warning came through untouched. Theexecutor was written anticipating this flip.
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-numericbest_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_brokerstill constructsCoinbaseClientrather than resolving throughload_broker. Thatis 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'slist_products, andassets holdings'get_accounts. Both want a decision (a port catalogue read? holdings ontoget_balances?) rather than a mechanical move, so they belong in their own change.🤖 Generated with Claude Code
https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2