From ebfada24cb041dab691109d54f538b8f6fb100d2 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 14 Apr 2026 08:54:02 -0700 Subject: [PATCH] Fix: don't permanently reject events when parking sell fails transiently - When _parking_liquidate_for_event returns False but parking had shares (sell failed due to transient Alpaca error), skip without recording so event retries on next run_next_open instead of being permanently blocked - Improve parking_sell_for_event_failed log: include exc_type, symbol, qty so we can see the actual exception (was silently swallowed with empty msg) - Also deleted AVGO from processed_events so it retries tomorrow Co-Authored-By: Claude Sonnet 4.6 --- apps/paper_trader/engine.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/paper_trader/engine.py b/apps/paper_trader/engine.py index d9e3098..6eb59e4 100644 --- a/apps/paper_trader/engine.py +++ b/apps/paper_trader/engine.py @@ -1596,7 +1596,7 @@ class PaperTradingEngine: ) return True except Exception as exc: - logger.warning("parking_sell_for_event_failed", error=str(exc)) + logger.warning("parking_sell_for_event_failed", error=str(exc) or repr(exc), exc_type=type(exc).__name__, symbol=sym, qty=shares_to_sell) return False def _parking_position_value( @@ -2502,7 +2502,11 @@ class PaperTradingEngine: engine_daily_new_risk_used=engine_risk_used if engine_cfg else 0.0, ) freed = False + parking_had_qty = False if plan.skip_reason == "insufficient_cash": + # Check if parking has shares before attempting liquidation + _pst = self._state.get_parking_state(session_id) + parking_had_qty = _pst is not None and (_pst.get("qty") or 0) > 0 # 1) Attempt to free parking cash before giving up needed = plan.shares * float(candidate.entry_price_est) if plan.shares else float(candidate.entry_price_est) freed = self._parking_liquidate_for_event(session_id, today, needed) @@ -2537,9 +2541,7 @@ class PaperTradingEngine: ) if plan.skip_reason: - # If we just freed parking cash but plan still shows insufficient_cash, - # Alpaca may not have settled the SGOV sell yet → skip without recording - # so the event retries on the next run_next_open. + # Case 1: parking freed but cash not yet reflected in account (Alpaca latency) if freed and plan.skip_reason == "insufficient_cash": logger.warning( "paper_engine_parking_freed_cash_not_settled", @@ -2547,6 +2549,15 @@ class PaperTradingEngine: hint="SGOV sold but cash not yet reflected in account — will retry on next run", ) continue + # Case 2: parking existed but sell failed (transient Alpaca error) + # Don't permanently reject — retry next run when sell may succeed. + if not freed and parking_had_qty and plan.skip_reason == "insufficient_cash": + logger.warning( + "paper_engine_parking_sell_failed_skip", + symbol=candidate.symbol, + hint="Parking sell failed (transient?) — skipping without recording to allow retry", + ) + continue self._state.record_processed_event( session_id, candidate.event_id, today.isoformat(), "rejected", skip_reason=plan.skip_reason,