Skip to content

feat(coinbase): refuse a derivative-capable account, and say what that does not prove (#666) - #692

Merged
eaitbrahim merged 2 commits into
mainfrom
feat-666-coinbase-posture
Sep 2, 2026
Merged

feat(coinbase): refuse a derivative-capable account, and say what that does not prove (#666)#692
eaitbrahim merged 2 commits into
mainfrom
feat-666-coinbase-posture

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #666 (Stage 1). Stage 2 — the operator attestation — is filed as #691.

Written against what a probe of the live account actually returned, not against a guess.

The probe

get_transaction_summary → margin_rate present: True, value: null
get_portfolios          → DEFAULT 'Default', CONSUMER 'My portfolio'

margin_rate is present-and-null. It's in the response schema for every account, so its presence signals nothing — a check keyed on presence would refuse every deployment, which is the exact failure this issue was filed to avoid.

A non-null value is plausibly a signal, and could not be verified — there's no margin-enabled account to compare against, so that refusal branch would ship untested. A compliance gate whose refusal path has never fired against a true positive is the gate that eventually fires on the compliant case and gets disabled in anger. It's surfaced as a warning on get_fee_summary, which already fetches that response, so visibility costs no extra request.

What is unambiguous

The portfolio list. DEFAULT and CONSUMER are the spot portfolios; INTX is the international/perpetuals one, and its presence is derivative capability on the account. verify_cash_account refuses on it, called from _build_broker beside Alpaca's — one get_portfolios read per build.

Passing means "no contradiction found", never proof

The docstring says so, and a test asserts it does, because a reader taking a pass as evidence has the guarantee backwards. Coinbase exposes no cash-versus-margin field for spot — every margin, borrow, leverage and liquidation field in its SDK lives in the futures, perpetuals or derivative-order types. Nothing here can issue a cash posture. #691 carries the attestation that can, on rail 17's pattern.

⚠️ It passes on an unreadable response — the opposite of Alpaca

A property of the venues, not an inconsistency. Alpaca fails closed because multiplier is the classification: a readable answer is definitive, so silence is a distinct third state worth refusing on.

Here the check can only refute, so silence proves nothing a readable response would also have failed to prove — and failing closed would refuse a compliant deployment on a network blip while establishing nothing. A mutation making it fail closed is one of the six killed.

The pin that fired

docs/fiqh-basis.md's open question is rewritten, and test_the_venue_boundary_premise_is_stated_not_hidden failed when this landed — which is what #672 wrote it for. It's now been re-pointed twice, and its docstring records both moves: first from a proxy that stopped tracking what it stood for, now from a premise that genuinely changed.

Verification

Tests: tests/broker_coinbase/test_adapter.py (+8), tests/commands/test_coinbase_posture_wiring.py (2, new — the call site, because three PRs this week shipped a helper whose call site was unpinned and whose removal left every other test green).

6 mutants, 6 killed: INTX no longer refusing, case-sensitive matching, the unreadable case failing closed, the margin_rate warning removed, the warning firing on null, and the build seam skipping the check.

Full suite 5268 passed / 3 skipped; ruff and mypy clean. Written test-first.

…t does not prove (#666)

Stage 1, written against what a probe of the live account actually returned
rather than against a guess.

THE PROBE, 2026-09-02, two read-only calls:

  get_transaction_summary -> margin_rate present: True, value: NULL
  get_portfolios          -> DEFAULT 'Default', CONSUMER 'My portfolio'

`margin_rate` is present-and-NULL. It is in the response SCHEMA for every account,
so its presence signals nothing and a check keyed on presence would refuse every
deployment -- the exact failure this issue was filed to avoid. A non-null value is
plausibly a signal, and could not be verified: there is no margin-enabled account
to compare against, so that refusal branch would ship untested. A compliance gate
whose refusal path has never fired against a true positive is the gate that
eventually fires on the compliant case and gets disabled in anger. It is surfaced
as a WARNING on `get_fee_summary`, which already fetches that response, so the
visibility costs no extra request.

WHAT IS UNAMBIGUOUS is the portfolio list. DEFAULT and CONSUMER are the spot
portfolios; INTX is the international/perpetuals one, and its presence is
derivative capability on the account. `verify_cash_account` refuses on it and is
called from `_build_broker` beside Alpaca's, so every command that builds a
broker inherits it. One `get_portfolios` read per build.

PASSING MEANS NO CONTRADICTION FOUND, NEVER PROOF, and the docstring says so
because a future reader taking a pass as evidence has the guarantee backwards.
Coinbase exposes no cash-versus-margin field for spot -- every margin, borrow,
leverage and liquidation field in its SDK lives in the futures, perpetuals or
derivative-order types -- so nothing here can issue a cash posture. #691 carries
the operator attestation that can, on rail 17's pattern.

⚠️ IT PASSES ON AN UNREADABLE RESPONSE, THE OPPOSITE OF THE ALPACA CHECK, and the
asymmetry is a property of the venues rather than an inconsistency. Alpaca fails
CLOSED because `multiplier` IS the classification: a readable answer is definitive
and silence is a distinct third state worth refusing on. Here the check can only
refute, so silence proves nothing a readable response would not also have failed
to prove -- and failing closed would refuse a compliant deployment on a network
blip while establishing nothing.

`docs/fiqh-basis.md`'s open question is rewritten to the residual that remains,
and `test_the_venue_boundary_premise_is_stated_not_hidden` FAILED when this
landed, which is what it was written for. It has now been re-pointed twice and its
docstring records both moves: first from a proxy that stopped tracking what it
stood for, now from a premise that genuinely changed.

Tests: `tests/broker_coinbase/test_adapter.py` (+8) and
`tests/commands/test_coinbase_posture_wiring.py` (2, new -- the call site, because
three PRs this week shipped a helper whose call site was unpinned).
6 mutants, 6 killed, including one that made the unreadable case fail closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
@eaitbrahim eaitbrahim self-assigned this Sep 2, 2026
CI caught what a narrower local check could not. `Transport` is the adapter's
STATEMENT of what it needs from a client, and `verify_cash_account` reached
`get_portfolios` without declaring it there -- so mypy saw a call to an attribute
the protocol does not have, and the test fakes were under no obligation to
provide it.

MY VERIFICATION WAS THE BUG. `[tool.mypy]` sets `files = ["packages", "keel",
"tests"]`, so bare `mypy` checks 389 source files; I had been running `mypy keel/`,
which checks 122 and skips every broker package -- including the one this change
is entirely inside. Bare `mypy` now passes on all 389.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
@eaitbrahim
eaitbrahim merged commit eeec344 into main Sep 2, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-666-coinbase-posture branch September 2, 2026 12:02
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.

Coinbase has no cash-account posture check — a margin-enabled account can turn a spot SELL into a short

1 participant