fix(execution): fill integration keys cash reasons and reservation release off OrderRole, not fill side (ALP-944) - #351
Conversation
…lease off OrderRole, not fill side (ALP-944) Fill integration treated buy as entry and sell as exit — correct for longs by coincidence, inverted for every leg of a short. Short entry fills (sells) logged CASH_CREDITED reason=EXIT_FILL, and the fill-time capital-reservation release only fired on buy-side fills, stranding short-entry stakes in cash_ledger.reserved_capital_usd forever (and arming the inverse: a short's protective buy legs would release capital they never staked). - Widen the reason vocabularies: CashCreditReason gains ENTRY_FILL, CashDebitReason gains EXIT_FILL (detail_json payload — no migration). - Key the cash reason off the order's role via the ENTRY/ADD_ENTRY partition the strategy dispatch and cancel path already use; event_type stays keyed off cash direction (every buy debits, every sell credits). - Gate the reserved_capital_usd decrement and the CAPITAL_RELEASED emission on entry-role, mirroring the role-scoped staking sites. - Tests: short entry sell fill credits as ENTRY_FILL and releases its stake; short cover buy fill debits as EXIT_FILL without touching the reservation pool; long-path tests now assert the reason payload instead of event presence; cyclic OPEN-cluster seeding deduped into one helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Use _is_entry_role at the strategy-dispatch site so the entry-role partition has one spelling in fill_collection. - Fold the new OPEN-cluster seed helper into _seed_position_order_thesis_bracket via an extra_orders param; the take-profit-leg test adopts it too (fourth inline copy removed). - Assert reason=EXIT_FILL on the take-profit leg fill's cash credit — the protective-role quadrant of the reason matrix was unasserted. - Trim the _is_entry_role docstring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ALP-944 Fill integration keys role semantics off fill side — short cash reasons inverted, short entry reservations stranded
SummaryCorrected from the original report: the reported mechanism is confirmed, but "no cash impact / only the reason label is wrong" is refuted. The same side-for-role conflation also gates the fill-time capital-reservation release, so the two short entries stranded $4,561.38 in Fill integration treats "buy" as "entry" and "sell" as "exit" in two places. Correct for longs by coincidence; inverted for every leg of a short:
Cash arithmetic ( Evidence (prod DB immutable snapshot, 2026-06-10 — POS-TXN / POS-BAC)Aggregate over all genesis-account The 5 mislabeled rows, all on FILLED sell-limit ENTRY-role orders: Reservation strand — the activity log and ledger reconcile exactly, and the residue is precisely the two short stakes: Every long entry in account history (GS, BKR, TER, MRVL, TSM, SLB, COP) shows reserve-then-full-release; neither short entry has any Root cause (confirmed)
Why it escaped testsThe write-path integration tests assert cash-event presence only — Reading
Scope — key role semantics off OrderRole at the fill seam (the cancel-path pattern)(A) Widen the reason vocabularies. Add (B) Key the cash reason off the order's role. At (C) Gate the fill-time reservation release on role, not side. In (D) Tests. In Out of scope: the CANCEL-path release ( Acceptance criteria
VerificationScoped runs while implementing: One-time prod data cleanup (manual, Windows prod box; source .env)Run after the fix deploys — until then the running code keeps minting mislabeled rows and stranding new short stakes, and a backfilled Step 1 — relabel the mislabeled rows (role-driven, idempotent). As of the 2026-06-10 snapshot this hits exactly 5 rows (TXN ×3, BAC ×2); the predicate also catches any further short legs that land before deploy: UPDATE activity_log
SET detail_json = json_set(detail_json, '$.reason', 'ENTRY_FILL')
WHERE event_type = 'CASH_CREDITED'
AND json_extract(detail_json, '$.reason') = 'EXIT_FILL'
AND order_id IN (SELECT order_id FROM orders
WHERE order_role IN ('ENTRY','ADD_ENTRY'));
UPDATE activity_log
SET detail_json = json_set(detail_json, '$.reason', 'EXIT_FILL')
WHERE event_type = 'CASH_DEBITED'
AND json_extract(detail_json, '$.reason') = 'ENTRY_FILL'
AND order_id IN (SELECT order_id FROM orders
WHERE order_role IN ('TAKE_PROFIT','PRICE_STOP','TIME_STOP','CLOSE'));Step 2 — recompute reserved_capital_usd from the staking invariant (sum of live entry-order reserved notionals — do not hardcode 1,494.72; pre-deploy fills can change the composition). Derive the target: SELECT COALESCE(SUM(
CAST(COALESCE(json_extract(price_parameters_json,'$.limit_price'),
json_extract(price_parameters_json,'$.stop_trigger_price')) AS REAL)
* remaining_quantity), 0)
FROM orders
WHERE order_role IN ('ENTRY','ADD_ENTRY')
AND status IN ('PENDING_SUBMIT','PENDING','PARTIALLY_FILLED')
AND COALESCE(json_extract(price_parameters_json,'$.limit_price'),
json_extract(price_parameters_json,'$.stop_trigger_price')) IS NOT NULL;Then write the result as a literal in the column's DecimalText format (three decimals, e.g. '1494.720'): UPDATE cash_ledger SET reserved_capital_usd = '<derived value>' WHERE id = 'current';Cash hazard: Step 3 — verify. The role-consistency sweep returns 0; SELECT COUNT(*) FROM activity_log a JOIN orders o ON o.order_id = a.order_id
WHERE (a.event_type='CASH_CREDITED'
AND json_extract(a.detail_json,'$.reason')='EXIT_FILL'
AND o.order_role IN ('ENTRY','ADD_ENTRY'))
OR (a.event_type='CASH_DEBITED'
AND json_extract(a.detail_json,'$.reason')='ENTRY_FILL'
AND o.order_role IN ('TAKE_PROFIT','PRICE_STOP','TIME_STOP','CLOSE'));Related
|
Summary
CASH_CREDITEDwithreason=EXIT_FILL, and the fill-time capital-reservation release only fired on buy-side fills, stranding short-entry stakes incash_ledger.reserved_capital_usdforever ($4,561.38 across TXN + BAC in prod, distorting the PM risk budget).CashCreditReasongainsENTRY_FILL,CashDebitReasongainsEXIT_FILL(JSON payload — no migration).event_typestays keyed off cash direction;reasonnow keys off the order's role (ENTRY/ADD_ENTRY vs the rest — the same partition the strategy dispatch and cancel path use).reserved_capital_usddecrement andCAPITAL_RELEASEDemission are gated on entry-role instead of buy-side, mirroring the role-scoped staking sites. This also defuses the latent inverse: a short's protective buy legs would have released capital they never staked.ENTRY_FILL, releases its stake) and short cover buy fill (debits asEXIT_FILL, leaves the pool untouched); long-path and take-profit tests now assert thereasonpayload instead of event presence; cyclic seed boilerplate consolidated into one helper.Follow-up candidate (out of scope per the issue): the ENTRY/ADD_ENTRY partition is still spelled independently in
command_execution/cancel.py:139,risk_guardrails/library_snapshot.py:123, andcommand_center/alerts/conditions.py:702— a shared constant next toPROTECTIVE_LEG_ROLE_VALUESinrecords/orders.pywould converge them.Closes https://linear.app/alphamind-jatassi/issue/ALP-944/fill-integration-keys-role-semantics-off-fill-side-short-cash-reasons
Test plan
.github/workflows/ci.yml) green on the PR — lint on Linux + full pytest on Windowsruff check,ruff format --check,mypy,lint-imports)tests/execution/state_persistence/+tests/portfolio_state/(1,515 tests)🤖 Generated with Claude Code