fix(execution): equity CLOSE re-checks the live broker position at dispatch (ALP-943) - #348
Conversation
…spatch (ALP-943) The 2026-06-09 17:00Z run decided to close MRVL long 4 and dispatched that CLOSE 48 minutes later, after the monitor's re-protection stop had already flattened the position. The dispatch resolved qty/side from the frozen positions projection, tolerated the 422 already-filled rejection on the protective-leg cancel by design (ALP-937), and sold 4 shares into a flat position — Alpaca classified the sell sell_to_open and opened an unmanaged -4 MRVL short. Fix — execution-time live-position recheck in the shared equity CLOSE dispatch (covers both the PM-directed and engine-envelope paths): - (A) AccountStateQueries.get_open_position(symbol) -> PositionSnapshot | None, wrapping TradingClient.get_open_position with 404 -> None like get_asset; mirrored on AccountStateQueriesP and the debug-e2e stand-in. - (B) _close_equity resolves the requested quantity (hoisted from submit_equity_close) and consults the live position before any leg-cancel RPC: flat or side-flipped -> PermanentRejectionError carrying PermanentRejection(code="position_state_drift", http_status=0); live qty below requested -> clamp the sell to the live quantity. - (C) _cancel_protective_legs resolves a permanently-rejected leg cancel via get_order_by_id: filled / partially_filled aborts the close with the same drift rejection (the protective exit executed); canceled / expired / 404-unknown proceeds as before. - The PM dispatch branch emits a command_abandoned entry for local guard rejections (http_status == 0) — the command never reached the broker, mirroring the stale-anchor backstop. The engine-envelope path returns a rejected SubmissionResult with the trigger not marked seen, unchanged. - PermanentRejectionCode widened with "position_state_drift"; runbook gains § 8.11 documenting the rejection class (operator posture: no action). Tests: the ALP-937 tolerate-already-terminal test is rewritten as the canceled-proceeds / filled-aborts pair; new drift-guard tests cover flat, side-flip, clamp, and both route-through paths; the broker fake now models live position existence and per-leg terminal states. Closes ALP-943. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Emit the ALP-937 (F) NAKED POSITION alert when the close aborts (drift rejection or unclassifiable error) AFTER an earlier protective leg's cancel was confirmed — previously the abort propagated out of _cancel_protective_legs before the alert path, leaving a surviving remainder silently unprotected (+ regression test). - Compare the live side via PositionSnapshot.side (Alpaca's own PositionSide field) instead of the sign of qty, removing the guard's dependence on the broker's qty sign convention for shorts. - Share recovery.py's fill-bearing status set (promoted to public FILL_BEARING_STATUSES) instead of a second hand-maintained copy. - Single-evaluation status extraction in _resolve_rejected_leg_state; drop the redundant symbol alias in _close_equity. - Document the http_status == 0 local-guard sentinel on PermanentRejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ALP-943 fix(execution): equity CLOSE dispatch executes against the stale positions projection, discarding the broker's filled-leg flat signal — sold 4 MRVL into a flat position → unmanaged −4 short (2026-06-09)
SummaryThe 2026-06-09 17:00Z pipeline run ( Corrected from the original report
Evidence (prod DB + live Alpaca PA3QJFZPNGCH + archived PM submission log — all UTC)
Root cause (confirmed)
Secondary integrity note (not the fix target): the pre-commit writeback Why it escaped tests
Reading
Scope — execution-time live-position recheck in the shared equity CLOSE dispatch(A) Live-position query surface. Add (B) The guard in the equity close dispatch. Thread (C) Abort on the filled-leg signal. In (D) Tests. Rewrite (E) Runbook. Document the Out of scope: equity Acceptance criteria
VerificationScoped runs while implementing: One-time prod cleanup (manual, Windows prod box;
|
Wrap the drift-guard live-position read in submit_with_retry, matching the transient-retry discipline of the path's submit/cancel calls. On window exhaustion the close is not submitted blind — the dispatch returns GatewaySubmissionFailed before any leg-cancel or order-submit RPC, surfacing through the callers' existing gateway-failure handling (+ regression test). Rejected nits with reasons in the PR conversation: the raw-data guard asymmetry in _resolve_rejected_leg_state already converges to the documented fail-open path (production client is typed); the silent clamp is in-spec per Scope (B) and logs a warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
sell_to_open)._close_equity, covering both the PM-directed and engine-envelope paths): re-check the live broker position via the newAccountStateQueries.get_open_positionbefore any leg-cancel RPC — flat or side-flipped → reject with the newposition_state_driftcode (http_status=0, local guard); live qty below requested → clamp the sell to the live quantity.get_order_by_id: filled / partially-filled aborts the close with the same drift rejection (the protective exit executed); canceled / expired / unknown proceeds as before. If the abort lands after an earlier leg's cancel was confirmed, the ALP-937 NAKED POSITION CRITICAL alert fires before the abort propagates.command_abandonedforensic entry for local-guard rejections; engine path returns a rejected SubmissionResult with the trigger not marked seen (the monitor may retry). Runbook § 8.11 documents the new rejection class (operator posture: no action).Closes https://linear.app/alphamind-jatassi/issue/ALP-943/fixexecution-equity-close-dispatch-executes-against-the-stale
Test plan
.github/workflows/ci.yml) — expected to fail with the GitHub usage-limit billing sentinel; perdocs/agents/ci.mdfallback, the full CI chain runs locally (lint chain + full pytest) before mergeruff check,ruff format --check,mypy,lint-imports)tests/execution/oms/, query-surface tests intests/execution/broker_adapter/)🤖 Generated with Claude Code