dashboard: stale-session alarm banner + rvol_tod in armed triggers

The 07-21 morning outage (loop booted post-EOD on the 20th, survived
midnight, silently blocked every entry while signals went READY) had zero
UI surface. New query_loop_session projection parses the live session id's
embedded date; a red banner fires when it isn't today's — the exact day-2
poison signature — telling the operator to restart the loop. Armed
Triggers rows now also show rvol_tod (the arming signal since 5597250)
next to legacy rvol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-21 20:09:56 -05:00
1 parent 9bfd458956
commit 2e39b756ba
4 files changed
+63 -1

No files matched your search

+1
View File
@@ -53,6 +53,7 @@ from storage.projections import (
query_fills_feed,
query_gate_receipts,
query_armed_triggers,
query_loop_session,
query_equity_curve,
query_order_lifecycle_snapshot,
query_ready_signals_snapshot,
+11 -1
View File
@@ -484,6 +484,7 @@
<div id="pause-banner" class="pause-banner">&#9208; ENTRIES PAUSED — no new positions (exits &amp; management still run). Click PAUSE ENTRIES to resume.</div>
<div id="naked-banner" class="naked-banner"></div>
<div id="stale-banner" class="naked-banner" role="alert"></div>
<div class="risk-strip" id="risk-strip">
<div><span class="label">Risk</span><span id="risk-status" class="rdot">—</span></div>
<div><span class="label">Day vs limit</span><b id="risk-day">—</b></div>
@@ -1181,7 +1182,7 @@ function renderTriggers(tw, catMap) {
return '<div class="trig-row st-' + esc(t.state) + near + '">'
+ '<div class="trig-sym">' + (t.catalyst ? '📰 ' : '') + esc(t.symbol) + '</div>'
+ '<div class="trig-state">' + esc(t.state) + '</div>'
+ '<div class="trig-meta">' + (t.gap != null ? 'gap ' + t.gap.toFixed(0) + '% ' : '') + (t.rvol != null ? t.rvol.toFixed(1) + 'x' : '') + '</div>'
+ '<div class="trig-meta">' + (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' : '') + '</div>'
+ '<div class="trig-levels">entry <b>' + fmt(trig) + '</b> stop <b>' + fmt(stop) + '</b> now ' + fmt(price) + '</div>'
+ '<div class="trig-bar" title="' + (price != null ? '$' + price : '?') + ' &rarr; trigger $' + (trig != null ? trig : '?') + ' (stop $' + (stop != null ? stop : '?') + ')">'
+ (hasBar ? '<div class="trig-fill" style="width:' + fill.toFixed(1) + '%"></div><div class="trig-trigline" style="left:' + trigPos.toFixed(1) + '%"></div>' : '')
@@ -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 || "");
+26
View File
@@ -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
@@ -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