' + (t.gap != null ? 'gap ' + t.gap.toFixed(0) + '% ' : '') + (t.rvol != null ? t.rvol.toFixed(1) + 'x' : '') + '
' + (t.gap != null ? 'gap ' + t.gap.toFixed(0) + '% ' : '') + (t.rvol != null ? t.rvol.toFixed(1) + 'x' : '') + (t.rvol_tod != null ? ' \u00B7 tod ' + (t.rvol_tod >= 100 ? t.rvol_tod.toFixed(0) : t.rvol_tod.toFixed(1)) + 'x' : '') + '
'
+ (hasBar ? '
' : '')
@@ -1457,6 +1458,15 @@ function render(d) {
nb.textContent = "⚠ UNPROTECTED: " + rk.unprotected.join(", ") + " — held long with no / short protective stop";
nb.classList.add("show");
} else { nb.classList.remove("show"); }
+ // day-2 stale-session alarm: the loop booted a PRIOR day and never restarted —
+ // its memory-only daily flags block every entry with zero events (2026-07-21).
+ const ls = d.loop_session || {}, sb = $("stale-banner");
+ if (sb) {
+ if (ls.stale && !d.view_date) {
+ sb.textContent = "\u26A0 LOOP SESSION FROM " + ls.session_date + " STILL RUNNING \u2014 daily state is stale (post-EOD boot survived midnight); new entries are silently blocked. Restart the loop container.";
+ sb.classList.add("show");
+ } else { sb.classList.remove("show"); }
+ }
const rsEl = $("risk-status");
rsEl.textContent = (rk.status || "—").toUpperCase();
rsEl.className = "rdot " + (rk.status || "");
diff --git a/storage/projections.py b/storage/projections.py
index 59a05cb..ac64e4d 100644
--- a/storage/projections.py
+++ b/storage/projections.py
@@ -880,6 +880,32 @@ def query_armed_triggers(store) -> dict:
return {"triggers": [], "armed": 0, "timestamp": None}
+def query_loop_session(store) -> dict:
+ """The live loop's CURRENT session identity — and whether it is STALE (booted a
+ prior day and never restarted). The loop's daily flags (_session_closed, entry
+ caps, benches) are memory-only: a session whose embedded date != today means a
+ post-EOD boot survived midnight and entries are silently blocked (hit live
+ 2026-07-21: signals READY + book armed, zero orders all morning). Session ids
+ look like paper-YYYY-MM-DD-hash; the date is parsed from the id, so this is one
+ bounded range scan with no per-session lookup."""
+ from datetime import datetime, timedelta
+ since = (datetime.now() - timedelta(minutes=15)).isoformat()
+ events = store.query_events(since=since, limit=None)
+ sid = None
+ for e in reversed(events):
+ cid = str(e.get("correlation_id") or "")
+ if cid.startswith("paper-"):
+ sid = cid
+ break
+ if not sid:
+ return {"session_id": None, "session_date": None, "stale": False}
+ parts = sid.split("-") # paper-YYYY-MM-DD-hash
+ sess_date = "-".join(parts[1:4]) if len(parts) >= 5 else None
+ today = datetime.now().strftime("%Y-%m-%d")
+ return {"session_id": sid, "session_date": sess_date,
+ "stale": bool(sess_date and sess_date != today)}
+
+
def query_equity_curve(store, points: int = 200, for_date: str | None = None) -> dict:
"""Total-equity series for an intraday P&L-shape sparkline, plus the prior-
session-close baseline (last_equity). Live = last 10h (the trading day); a
diff --git a/tests/unit/storage/test_interactive_projections.py b/tests/unit/storage/test_interactive_projections.py
index 4890b9f..d0f2ac6 100644
--- a/tests/unit/storage/test_interactive_projections.py
+++ b/tests/unit/storage/test_interactive_projections.py
@@ -282,3 +282,28 @@ def test_daily_performance_curve_trading_days_only():
assert days[1]["day_pnl"] == -200.0 and days[1]["cum_pnl"] == -100.0
assert days[1]["trades"] == 2 and days[1]["win_rate"] == 0.5
store.close()
+
+
+def test_loop_session_stale_detection(store):
+ """A paper session whose embedded date != today must flag stale — the day-2
+ silent entry-block signature (loop booted post-EOD, survived midnight)."""
+ from storage.event_schema import ModuleTickEvent
+ from storage.projections import query_loop_session
+
+ def _tick(cid):
+ store.emit(ModuleTickEvent(
+ timestamp=datetime.now(), mode=EventMode.PAPER,
+ correlation_id=cid, message="tick",
+ module="triggers", stage="snapshot", duration_ms=0.0,
+ input_count=0, output_count=0, metrics={}, errors=[]))
+
+ _tick("paper-2020-01-02-abc123")
+ s = query_loop_session(store)
+ assert s["session_id"] == "paper-2020-01-02-abc123"
+ assert s["session_date"] == "2020-01-02"
+ assert s["stale"] is True
+
+ today = datetime.now().strftime("%Y-%m-%d")
+ _tick(f"paper-{today}-def456")
+ s = query_loop_session(store)
+ assert s["session_date"] == today and s["stale"] is False