Skip to content

fix(coinbase): guard the adapter's preview Decimal conversions against empty fields (#799) - #800

Merged
eaitbrahim merged 2 commits into
mainfrom
fix/799-adapter-decimal-guard
Sep 15, 2026
Merged

eaitbrahim merged 2 commits into
mainfrom
fix/799-adapter-decimal-guard

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes the code half of #799.

The bug

_field(response, key, "0") supplies a default for a missing key. It does nothing for a key
that is present and empty, which is what Coinbase sent. Decimal("") raises InvalidOperation.

-  est_base_size=Decimal(_field(response, "base_size", "0")),
+  est_base_size=Decimal(_field(response, "base_size", "0") or "0"),

…and the same for quote_size and commission_total.

Why it mattered

2026-08-25, live, cycle e917fb3d21544dc2:

13:20:20  executor.order_placed    PAXG-USD BUY  order_id=4  status=filled
13:20:21  executor.preview_failed  PAXG-USD SELL
          decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]

The entry filled. The protective SELL's preview then threw, and the exception propagated out
of executor._run_order before agent.py's repo.open_position(...) ran. Three weeks later that
holding is still stranded: no positions row, so no channel exit and no 2xATR stop can ever fire;
its computed stop of 4521.76 exists only in a log line (PAXG is now ~4312, so the stop was
breached and nothing acted); and $61.71 still counts against the exposure caps, because
_open_exposure_by_asset reads orders, not positions.

agent.tranche_not_recorded — the warning that exists for "entry filled but not recorded" — did
not fire. It only covers a missing actual_fill/qty. This path throws earlier and logs a
preview failure, which reads like a harmless pre-trade hiccup.

Why it was still live

The retired keel/data/cb_client.py already carries or "0". The adapter that replaced it does
not — #524 flipped _build_broker to the registry-resolved adapter, so the fix landed on the
path being retired and not on the path that replaced it.
The August failure would recur today.

Test

Parametrized over the three fields. Each case asserts the emptied field reads 0 and that the
other two still parse from the fixture
, so a blanket try/except zeroing the whole Preview
does not pass it. Verified by mutation: restoring the bare conversion for one field fails that
field's case alone, leaving the other two green.

ruff format --check and ruff check clean; mypy clean apart from a pre-existing PyYAML stub
note in scripts/release_notes.py; tests/broker_coinbase tests/broker_api tests/execution →
602 passed.

Not in this PR

#799's other three items are deliberately left open — they are design changes, not a one-line
guard:

  • a filled entry must survive a later failure (record the position, or downgrade to an explicit
    UNBRACKETED state the codebase already models)
  • preview_failed on a protective leg after order_placed needs its own event and a doctor
    check for filled BUYs with no position row
  • reconciling live order id=4 is an operational decision, not a code one

🤖 Generated with Claude Code

https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD

eaitbrahim and others added 2 commits September 15, 2026 07:50
…t empty fields

The `"0"` in `_field(response, key, "0")` is a MISSING-KEY default and does nothing
when the key is present and empty. Coinbase sends `""`, and `Decimal("")` raises
`InvalidOperation`.

On 2026-08-25 that crash cost a live position. A PAXG entry FILLED, the protective
SELL's preview then raised out of `executor._run_order`, and the exception propagated
before `agent.open_position` ran -- so the fill was never recorded. There is no
`positions` row, so no channel exit and no 2xATR stop can fire; the computed stop
(4521.76) survives only in a log line; and the BUY notional still counts against the
exposure caps, because `_open_exposure_by_asset` reads `orders`, not `positions`.

The retired `keel/data/cb_client.py` was given `or "0"` for exactly this. The adapter
that replaced it never was -- #524 flipped `_build_broker` to the registry-resolved
adapter, so the fix landed on the path being retired and not on the one now in use,
and the same failure would recur today.

Test is parametrized per field and asserts the OTHER two still parse from the fixture,
so a blanket try/except that zeroed the whole Preview does not satisfy it. Verified by
mutation: restoring the bare conversion for one field fails that field's case alone.

Refs #799.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD
@eaitbrahim
eaitbrahim merged commit 43bedda into main Sep 15, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the fix/799-adapter-decimal-guard branch September 15, 2026 21:22
eaitbrahim added a commit that referenced this pull request Sep 16, 2026
…path (#801)

Patch release carrying #800. 0.15.0's wheels predate it, so every deployment is
still running the crash #799 documents: `Decimal("")` out of the Coinbase adapter's
preview, which on 2026-08-25 aborted `executor._run_order` after a PAXG entry had
already FILLED and before `agent.open_position` could record it.

Eight workspace `pyproject.toml` versions and the fourteen inter-package `==` pins
move together -- the pins are what a `keel versions` PARTIAL INSTALL comes from when
they are left behind -- plus the refreshed `uv.lock` the workflow checks first.


Claude-Session: https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant