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
On the live deployment, 2026-08-25 13:20 UTC, cycle e917fb3d21544dc2:
13:20:19 INFO engine.setup_detected turtle_breakout PAXG-USD cts_score=6 entry=4673.23 stop=4521.76
13:20:19 INFO executor.order_previewed PAXG-USD BUY notional=61.70661377875054234072303553
13:20:20 INFO executor.order_placed PAXG-USD BUY order_id=4 status=filled
13:20:21 ERROR executor.preview_failed PAXG-USD SELL
Traceback (most recent call last):
File ".../keel/execution/executor.py", line 625, in _run_order
preview = broker.preview_order(intent.product_id, intent.side, order_configuration)
File ".../keel/data/cb_client.py", line 255, in preview_order
result[key] = Decimal(value)
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
The entry filled. The protective SELL preview then threw, and the exception aborted the
flow before agent.py:380's repo.open_position(...) ran.
Resulting state, still true today
orders: id=4 rule_id=3 PAXG-USD BUY filled qty=0.0132 actual_fill=4673.23 fee=0.73
positions: (no PAXG row -- only the two BTC/dca tranches)
Consequences, all live right now:
No exit can ever fire. The 20-day channel exit and the 2xATR stop both key off a position
row. There isn't one, so a planned swing trade became permanent unmanaged spot.
$61.71 of cap is consumed forever._open_exposure_by_asset (guards.py:339) reads orders, not positions, so guard 6 counts the BUY notional. It is the exact gap between the
reported total exposure $163.51 and BTC's $101.81.
initial_stop was computed and discarded -- 4521.76 is in the log and nowhere else.
Note the silence: agent.tranche_not_recorded (the WARNING that exists for "entry filled but
could not be recorded") never fired, because that guard only covers a missing actual_fill/qty.
This path throws earlier and logs only a preview failure, which reads like a harmless
pre-trade hiccup rather than a stranded position.
Root cause, and why it is NOT fixed
The venue returned a value that is not a valid Decimal -- an empty string is the likely shape --
for a field the client converted unguarded.
The legacy client has since been fixed. In the deployed 0.15.0, keel/data/cb_client.py reads:
The or "0" is what handles ""; the bare "0" default only covers a MISSING key.
But _build_broker was migrated to the registry-resolved adapter by #524, and that adapter never
got the guard. Deployed 0.15.0, keel_broker_coinbase/adapter.py:332-334:
Same in the repo at packages/keel-broker-coinbase/keel_broker_coinbase/adapter.py. The fix
landed on the path that was being retired and not on the path that replaced it, so the August
incident would recur today.
Proposed
Guard the conversions in the adapter, matching the legacy client (or "0"), and add a test
feeding "" for each of base_size/quote_size/commission_total. A cross-implementation
test asserting the two parsers agree on a degenerate response would stop the next divergence.
A filled entry must never be lost to a later failure. Record the position first, or wrap
the bracket leg so its failure downgrades to "position open, UNBRACKETED" -- a state the
codebase already models (bracket: NO bracket renders on the two BTC tranches) -- rather than
unwinding the bookkeeping for an entry that really did fill.
Escalate the log line. A preview_failed on a protective leg after order_placed is a
stranded-position event, not a preview hiccup. It needs its own event and a doctor check for
filled BUY orders with no corresponding position row.
Reconcile order id=4 -- decide whether to write the missing position (recovering the exit
lifecycle and the recorded initial_stop of 4521.76) or to treat the holding as closed.
Severity
Silent, and it strands real money in an unmanaged state while tightening every cap against
inventory nothing can act on. Found while diagnosing why the live loop had placed no order since
2026-08-21. Related: #797, #798.
What happened
On the live deployment, 2026-08-25 13:20 UTC, cycle
e917fb3d21544dc2:The entry filled. The protective SELL preview then threw, and the exception aborted the
flow before
agent.py:380'srepo.open_position(...)ran.Resulting state, still true today
Consequences, all live right now:
row. There isn't one, so a planned swing trade became permanent unmanaged spot.
_open_exposure_by_asset(guards.py:339) readsorders, notpositions, so guard 6 counts the BUY notional. It is the exact gap between thereported total exposure
$163.51and BTC's$101.81.keel positionscannot see it, so no operator action can address it either (see Reconcile: out-of-band venue executions leave phantom exposure; nokeel positions close#798).initial_stopwas computed and discarded --4521.76is in the log and nowhere else.Note the silence:
agent.tranche_not_recorded(the WARNING that exists for "entry filled butcould not be recorded") never fired, because that guard only covers a missing
actual_fill/qty.This path throws earlier and logs only a preview failure, which reads like a harmless
pre-trade hiccup rather than a stranded position.
Root cause, and why it is NOT fixed
The venue returned a value that is not a valid
Decimal-- an empty string is the likely shape --for a field the client converted unguarded.
The legacy client has since been fixed. In the deployed 0.15.0,
keel/data/cb_client.pyreads:The
or "0"is what handles""; the bare"0"default only covers a MISSING key.But
_build_brokerwas migrated to the registry-resolved adapter by #524, and that adapter nevergot the guard. Deployed 0.15.0,
keel_broker_coinbase/adapter.py:332-334:Same in the repo at
packages/keel-broker-coinbase/keel_broker_coinbase/adapter.py. The fixlanded on the path that was being retired and not on the path that replaced it, so the August
incident would recur today.
Proposed
or "0"), and add a testfeeding
""for each ofbase_size/quote_size/commission_total. A cross-implementationtest asserting the two parsers agree on a degenerate response would stop the next divergence.
the bracket leg so its failure downgrades to "position open, UNBRACKETED" -- a state the
codebase already models (
bracket: NO bracketrenders on the two BTC tranches) -- rather thanunwinding the bookkeeping for an entry that really did fill.
preview_failedon a protective leg afterorder_placedis astranded-position event, not a preview hiccup. It needs its own event and a
doctorcheck forfilled BUY orders with no corresponding position row.
lifecycle and the recorded
initial_stopof 4521.76) or to treat the holding as closed.Severity
Silent, and it strands real money in an unmanaged state while tightening every cap against
inventory nothing can act on. Found while diagnosing why the live loop had placed no order since
2026-08-21. Related: #797, #798.