Fix EOD DB sweep bug and add regression test

Bug: pending candidate DB records were only swept inside the "has open
positions" branch of run_eod_exit, so a server restart mid-day (ORB
detection ran, no breakouts, no positions) left candidates as "pending"
forever.

Fix: move the in-memory and DB pending sweep to run unconditionally before
the positions check.

Test: TestEodDbSweep verifies both code paths (no-position + in-memory).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent a0732a9589
commit ba491b2a0d

@ -846,6 +846,22 @@ class ORBTradingEngine:
self._session.session_id, date_str, phase="eod_exit" self._session.session_id, date_str, phase="eod_exit"
) )
# Cancel any unfilled breakout candidates — must run unconditionally so
# stale pending records are cleaned up even when there are no open positions
# (e.g., server restarted after ORB detection but before any breakout).
for cand in self._pending_cands:
self._state.update_candidate_status(
self._session.session_id, date_str, cand["ticker"], "timeout"
)
self._pending_cands = []
db_cands = self._state.list_candidates(self._session.session_id, date_str)
for c in db_cands:
if c["status"] == "pending":
self._state.update_candidate_status(
self._session.session_id, date_str, c["ticker"], "timeout"
)
positions = self._state.get_open_positions(self._session.session_id, date_str) positions = self._state.get_open_positions(self._session.session_id, date_str)
if not positions: if not positions:
self._log("EOD: no open positions") self._log("EOD: no open positions")
@ -880,21 +896,6 @@ class ORBTradingEngine:
self._session.session_id, date_str, pos.ticker self._session.session_id, date_str, pos.ticker
) )
# Cancel any unfilled breakout candidates (in-memory and DB)
for cand in self._pending_cands:
self._state.update_candidate_status(
self._session.session_id, date_str, cand["ticker"], "timeout"
)
self._pending_cands = []
# Also sweep DB for any pending records not in in-memory list (e.g. after restart)
db_cands = self._state.list_candidates(self._session.session_id, date_str)
for c in db_cands:
if c["status"] == "pending":
self._state.update_candidate_status(
self._session.session_id, date_str, c["ticker"], "timeout"
)
return {"closed": closed} return {"closed": closed}
# ── Phase 6: Post-close ─────────────────────────────────────────────────── # ── Phase 6: Post-close ───────────────────────────────────────────────────

@ -246,3 +246,47 @@ class TestMaxSimultaneousEntries:
mock_snaps.assert_called_once() mock_snaps.assert_called_once()
assert result["remaining"] == 1 assert result["remaining"] == 1
# ── EOD DB sweep ──────────────────────────────────────────────────────────────
class TestEodDbSweep:
def test_sweep_runs_with_no_open_positions(self):
# Regression: before fix, DB sweep was inside the "has positions" branch;
# after a server restart with no open positions, stale pending records leaked.
eng = _make_engine()
eng._state.get_open_positions.return_value = []
eng._state.list_candidates.return_value = [
{"ticker": "AAPL", "status": "pending"},
{"ticker": "NVDA", "status": "pending"},
{"ticker": "TSLA", "status": "filled"}, # already filled, must not be touched
]
result = eng.run_eod_exit("2026-01-05")
assert result["closed"] == 0
# Both pending records should be timed out
calls = [
call.args for call in eng._state.update_candidate_status.call_args_list
]
assert ("test-session", "2026-01-05", "AAPL", "timeout") in calls
assert ("test-session", "2026-01-05", "NVDA", "timeout") in calls
# Filled record must not be touched
assert ("test-session", "2026-01-05", "TSLA", "timeout") not in calls
def test_in_memory_pending_swept_with_no_open_positions(self):
# In-memory pending candidates must also be swept unconditionally
eng = _make_engine()
eng._state.get_open_positions.return_value = []
eng._state.list_candidates.return_value = []
eng._pending_cands = [
{"ticker": "MSFT", "direction": "bullish", "orb_bar": {}, "atr": 1.0,
"score": 0.5, "rvol": 1.5, "gap_pct": 0.02},
]
eng.run_eod_exit("2026-01-05")
assert eng._pending_cands == [] # cleared
eng._state.update_candidate_status.assert_called_once_with(
"test-session", "2026-01-05", "MSFT", "timeout"
)

Loading…
Cancel
Save