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
{{ message }}
Repository navigation
Commit e5b08b4
Browse filesBrowse the repository at this point in the historyBrowse files
feat(broker-api): a product-catalogue read, the first of #524's two blockers (#561)
#524 names two reasons the live path cannot move onto the port. This closes the
first and touches no live behaviour: nothing calls `get_instrument` yet.
── THE GAP ────────────────────────────────────────────────────────────────────
`executor._base_increment_for` (#516) needs the finest `base_size` a venue will
accept, and gets it today by calling `broker.list_products()` and picking through
raw dicts for `product_id` and `base_increment`. That is the pre-port
CoinbaseClient's shape. The port had no catalogue read at all, so there was
nothing for the executor to move ONTO.
── ONE PRODUCT, NOT THE CATALOGUE ─────────────────────────────────────────────
`get_instrument(product_id) -> Instrument | None`, and the shape is the caller's
own argument: `list_products` returns ~900 rows on Coinbase and
`_base_increment_for` caches exactly ONE per miss, because `Repository.set_state`
commits per call and caching all of them would mean ~900 fsyncs inside the
order-placement path. Coinbase's transport has carried `get_product` all along.
`None` means "this venue does not list that product" and is an ANSWER, not a
failure -- which is why this differs from `get_order`. An order id was handed to
the caller by this venue; a product id comes from an operator's allowlist and may
simply not be listed here.
`Instrument` carries `product_id` and `base_increment` and nothing else. Quote
granularity and minimum sizes are the same class of fact and would sit here
naturally, but nothing reads them, and a field no caller reads is a field no test
meaningfully checks.
`__post_init__` refuses a non-positive increment. It is what a caller quantizes
against, so a zero crossing the port is a division error or a silent zero size on
the exit path.
── FIVE ADAPTERS, THREE DIFFERENT TRUTHS ──────────────────────────────────────
* coinbase -- `get_product`, unwrapping either envelope; `None` for missing,
unparseable, zero or negative.
* robinhood -- `get_trading_pairs(symbol)`; the venue's `min_order_size` IS
this fact, and the port keeps keel's name for it rather than the venue's.
* fake -- a fixed increment, and `None` for a reserved `NOT-LISTED` id so the
conformance suite actually exercises the absent case.
* alpaca -- `NotImplementedError`. Alpaca HAS `/v2/assets/{symbol}`; this
adapter's Transport protocol does not declare it. Deliberately not `None`:
`None` would tell the executor a symbol is unlisted when the truth is that
nobody wrote the read, which on the live path means silently skipping
quantization for every equity.
* kraken -- the stub's standard refusal.
── CONFORMANCE ────────────────────────────────────────────────────────────────
Two suite tests, run against all four registered adapters. No capability flag
gates them, deliberately: a product catalogue is not an optional venue FEATURE,
it is something every venue has and some adapters have not been taught to read.
`NotImplementedError` says which of those it is.
Mutation-checked: returning a raw venue dict fails the type assertion, and a zero
increment fails before it can be returned at all.
Gates: 4292 passed / 3 skipped, ruff clean, mypy clean across 347 files.
Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments