Skip to content

Finish the broker-port migration: _build_broker still constructs CoinbaseClient directly #524

Description

@eaitbrahim

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

Established during the #502 design pass:

  1. No product-catalog port method. executor._base_increment_for (executor: SELL base_size is still serialised at full Decimal precision (follow-up to #513) #516) needs base_increment from list_products, which the port does not expose.
  2. Balances shape mismatch. _fetch_available_quote probes a dict-shaped get_accounts response; the port returns list[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:

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.

Notes

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions