You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Raised in docs/presentations/se.md §9.1, and split out of the #502 design pass as the stage that makes "port-clean" literally true.
The gap
keel has a ports-and-adapters design: keel-broker-api is the port, and four adapters implement it (coinbase, alpaca, robinhood, kraken) plus fake. All are conformance-tested in CI.
None of them is on the live path.keel/commands/_common.py::_build_broker constructs keel/data/cb_client.py::CoinbaseClient directly, and load_broker/discover_brokers — the keel.brokers entry-point registry — are not called anywhere in the application, only in tests.
The consequence is understated in most descriptions of the architecture: the executor does not bypass the port for the bracket, it bypasses it for every order. CoinbaseClient's whole surface is pre-port — preview_order(product_id, side, order_configuration) and place_order(...) take raw dicts, and executor._order_configuration builds market-order dicts the same way _bracket_order_configuration builds the bracket dict.
This is why keel-broker-robinhood is a dev-only dependency rather than a runtime one: nothing constructs it, so shipping an Ed25519 stack (pynacl) to every install would buy nothing.
Known blockers — the port is missing methods the executor needs
the dual-shape probes — ConfirmFn, _preview_book's dict arm, place_result.get("success")
the capabilities() grandfather clause for the pre-port client
the reason Robinhood and Alpaca cannot be selected at runtime
Acceptance criteria
_build_broker resolves the venue through load_broker/discover_brokers rather than constructing a client directly.
The port exposes what the executor needs (product catalog / increments, and a balances read the executor can use) — or the executor is adapted to the port's existing shapes.
The live path places every order through OrderSpec values, not raw dicts.
The legacy renderer, dual-shape probes and grandfather clause are deleted, not left beside the new path.
A deployment can select a non-Coinbase venue and have it either work or refuse explicitly at arm time.
Raised in
docs/presentations/se.md§9.1, and split out of the #502 design pass as the stage that makes "port-clean" literally true.The gap
keel has a ports-and-adapters design:
keel-broker-apiis the port, and four adapters implement it (coinbase,alpaca,robinhood,kraken) plusfake. All are conformance-tested in CI.None of them is on the live path.
keel/commands/_common.py::_build_brokerconstructskeel/data/cb_client.py::CoinbaseClientdirectly, andload_broker/discover_brokers— thekeel.brokersentry-point registry — are not called anywhere in the application, only in tests.The consequence is understated in most descriptions of the architecture: the executor does not bypass the port for the bracket, it bypasses it for every order.
CoinbaseClient's whole surface is pre-port —preview_order(product_id, side, order_configuration)andplace_order(...)take raw dicts, andexecutor._order_configurationbuilds market-order dicts the same way_bracket_order_configurationbuilds the bracket dict.This is why
keel-broker-robinhoodis a dev-only dependency rather than a runtime one: nothing constructs it, so shipping an Ed25519 stack (pynacl) to every install would buy nothing.Known blockers — the port is missing methods the executor needs
Established during the #502 design pass:
executor._base_increment_for(executor: SELL base_size is still serialised at full Decimal precision (follow-up to #513) #516) needsbase_incrementfromlist_products, which the port does not expose._fetch_available_quoteprobes a dict-shapedget_accountsresponse; the port returnslist[Balance].Both are small, but they are why the flip is not a one-line change.
What the flip deletes
Worth stating, because it is the payoff:
executor._bracket_order_configuration's legacy renderer and its parity test (introduced as the bridge in Broker port has no bracket/OCO order kind — live trailing stops, BE-rolls and scale_out are blocked on it #502 stage 1/2)ConfirmFn,_preview_book's dict arm,place_result.get("success")capabilities()grandfather clause for the pre-port clientAcceptance criteria
_build_brokerresolves the venue throughload_broker/discover_brokersrather than constructing a client directly.OrderSpecvalues, not raw dicts.Notes