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:
1 parent
9bfd458956
commit
2e39b756ba
4 files changed
+63
-1
No files matched your search
@@ -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,
|
||||
|
||||
@@ -484,6 +484,7 @@
|
||||
|
||||
<div id="pause-banner" class="pause-banner">⏸ ENTRIES PAUSED — no new positions (exits & 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 : '?') + ' → 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 || "");
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user