WS3: surface evaluation internals (gap/rvol/per-criterion reasons)

- SetupEvaluationResult.criteria_detail: per-criterion {name,passed,reason}.
- watcher CriteriaEvaluatedEvent now carries gap_pct, relative_volume, status,
  reason, and the per-criterion detail (was only passed/failed names).
- CLI: 'inspect criteria SYMBOL' shows status + gap/rvol + each criterion reason;
  new 'inspect signals' = ready board + per-symbol gap/rvol/status.

Verified on real 2026-06-15 bars: RGNT shows 9/9 with reasons (rvol 26.2x),
'late' explained by the 10:30 ET cutoff. 67/67 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-16 07:22:55 -04:00
1 parent 9e79c50751
commit a16a8cd29d
4 files changed
+84 -6

No files matched your search

+69 -6
View File
@@ -286,18 +286,81 @@ def inspect_criteria(symbol: str):
return
ts, payload = rows[0]
p = json.loads(payload)
console.print(f"[bold]{symbol.upper()}[/bold] score={p.get('success_score_pct')}% "
f"passed={p.get('passed_criteria')}/{p.get('total_criteria')} [dim]{ts}[/dim]")
cr = p.get("criteria_results", {})
status = cr.get("status", "?")
sc = {"ready": "green", "blocked": "red", "late": "yellow"}.get(status, "white")
gap = cr.get("gap_pct")
rvol = cr.get("relative_volume")
gap_s = f"{gap:+.1%}" if isinstance(gap, (int, float)) else str(gap)
rvol_s = f"{rvol:.1f}x" if isinstance(rvol, (int, float)) else str(rvol)
console.print(
f"[bold]{symbol.upper()}[/bold] [{sc}]{status.upper()}[/{sc}] "
f"score={p.get('success_score_pct')}% passed={p.get('passed_criteria')}/{p.get('total_criteria')} "
f"gap={gap_s} rvol={rvol_s} [dim]{ts}[/dim]"
)
if cr.get("reason"):
console.print(f"[dim]reason:[/dim] {cr['reason']}")
table = Table(box=box.SIMPLE)
table.add_column("criterion", style="bold")
table.add_column("result")
if isinstance(cr, dict):
for name, val in cr.items():
ok = val if isinstance(val, bool) else bool(val)
table.add_row(name, "[green]pass[/green]" if ok else "[red]fail[/red]")
table.add_column("reason", overflow="fold")
detail = cr.get("detail", [])
if detail:
for d in detail:
ok = d.get("passed")
table.add_row(d.get("name", "?"),
"[green]pass[/green]" if ok else "[red]fail[/red]",
d.get("reason") or "")
else: # older events without detail
for n in cr.get("passed", []):
table.add_row(n, "[green]pass[/green]", "")
for n in cr.get("failed", []):
table.add_row(n, "[red]fail[/red]", "")
console.print(table)
@inspect_app.command("signals")
def inspect_signals(limit: int = 25):
"""Ready signals + the latest per-symbol evaluation board (gap/rvol/status)."""
con = _con()
console.print("[bold]READY signals[/bold]")
rows = con.execute(
"SELECT timestamp, message FROM events WHERE event_type='signal_ready' "
"ORDER BY timestamp DESC LIMIT ?", [limit]).fetchall()
if rows:
t = Table(box=box.SIMPLE)
t.add_column("time", style="cyan"); t.add_column("signal")
for ts, msg in rows:
t.add_row(str(ts), msg or "")
console.print(t)
else:
console.print("[yellow]no ready signals yet (none, or after the 10:30 ET cutoff)[/yellow]")
console.print("\n[bold]latest evaluations[/bold] (most recent per symbol)")
rows = con.execute(
"SELECT payload_json FROM events WHERE event_type='criteria_evaluated' "
"ORDER BY timestamp DESC LIMIT 400").fetchall()
seen: dict = {}
for (pj,) in rows:
p = json.loads(pj)
sym = p.get("symbol")
if sym and sym not in seen:
cr = p.get("criteria_results", {})
seen[sym] = (p.get("success_score_pct") or 0, cr.get("status"),
cr.get("gap_pct"), cr.get("relative_volume"))
if not seen:
console.print("[yellow]no evaluations yet (needs minute bars + a watcher pass)[/yellow]")
return
t = Table(box=box.SIMPLE)
for c in ("symbol", "status", "score", "gap", "rvol"):
t.add_column(c, justify="left" if c == "symbol" else "right")
for sym, (score, status, gap, rvol) in sorted(seen.items(), key=lambda x: -(x[1][0] or 0)):
color = {"ready": "green", "late": "yellow", "blocked": "red"}.get(status, "white")
t.add_row(sym, f"[{color}]{status}[/{color}]", f"{score}%",
f"{gap:+.1%}" if isinstance(gap, (int, float)) else str(gap),
f"{rvol:.1f}x" if isinstance(rvol, (int, float)) else str(rvol))
console.print(t)
if __name__ == "__main__":
app()
+8
View File
@@ -204,6 +204,11 @@ class Watcher:
criteria_results={
"passed": evaluation.criteria_names_passed,
"failed": evaluation.criteria_names_failed,
"detail": evaluation.criteria_detail,
"gap_pct": evaluation.gap_pct,
"relative_volume": evaluation.relative_volume,
"status": evaluation.status,
"reason": evaluation.reason,
},
total_criteria=evaluation.criteria_total,
passed_criteria=evaluation.criteria_passed,
@@ -212,6 +217,9 @@ class Watcher:
"symbol": symbol,
"success_score_pct": evaluation.success_score_pct,
"status": evaluation.status,
"reason": evaluation.reason,
"gap_pct": evaluation.gap_pct,
"relative_volume": evaluation.relative_volume,
},
)
)
+3
View File
@@ -166,6 +166,9 @@ def evaluate_setup(
success_score_pct=round(score_pct, 2),
criteria_names_passed=passed_names,
criteria_names_failed=failed_names,
criteria_detail=[
{"name": r.name, "passed": r.passed, "reason": r.reason} for r in results
],
setups=setups,
)
+4
View File
@@ -71,3 +71,7 @@ class SetupEvaluationResult(BaseModel):
default_factory=list, description="Names of failed criteria"
)
setups: list[dict] = Field(default_factory=list, description="Detected setups")
criteria_detail: list[dict] = Field(
default_factory=list,
description="Per-criterion breakdown: {name, passed, reason}",
)