fix(coinbase): guard the adapter's preview Decimal conversions against empty fields (#799) - #800
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the code half of #799.
The bug
_field(response, key, "0")supplies a default for a missing key. It does nothing for a keythat is present and empty, which is what Coinbase sent.
Decimal("")raisesInvalidOperation.…and the same for
quote_sizeandcommission_total.Why it mattered
2026-08-25, live, cycle
e917fb3d21544dc2:The entry filled. The protective SELL's preview then threw, and the exception propagated out
of
executor._run_orderbeforeagent.py'srepo.open_position(...)ran. Three weeks later thatholding is still stranded: no
positionsrow, so no channel exit and no 2xATR stop can ever fire;its computed stop of
4521.76exists only in a log line (PAXG is now ~4312, so the stop wasbreached and nothing acted); and
$61.71still counts against the exposure caps, because_open_exposure_by_assetreadsorders, notpositions.agent.tranche_not_recorded— the warning that exists for "entry filled but not recorded" — didnot fire. It only covers a missing
actual_fill/qty. This path throws earlier and logs apreview failure, which reads like a harmless pre-trade hiccup.
Why it was still live
The retired
keel/data/cb_client.pyalready carriesor "0". The adapter that replaced it doesnot — #524 flipped
_build_brokerto the registry-resolved adapter, so the fix landed on thepath 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
0and that theother two still parse from the fixture, so a blanket
try/exceptzeroing the wholePreviewdoes 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 --checkandruff checkclean;mypyclean apart from a pre-existing PyYAML stubnote 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:
UNBRACKETED state the codebase already models)
preview_failedon a protective leg afterorder_placedneeds its own event and adoctorcheck for filled BUYs with no position row
id=4is an operational decision, not a code one🤖 Generated with Claude Code
https://claude.ai/code/session_01AoGRoExgHVCsWDHT3ej8mD