From a16a8cd29d81d9032d1897fe5c8650ffa8a7586e Mon Sep 17 00:00:00 2001 From: momentum dev Date: Tue, 16 Jun 2026 07:22:55 -0400 Subject: [PATCH] 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 --- momentum_cli.py | 75 +++++++++++++++++++++++--- runtime/watcher.py | 8 +++ strategy/evaluation/setup_evaluator.py | 3 ++ strategy/models.py | 4 ++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/momentum_cli.py b/momentum_cli.py index 4acb314..40885bb 100644 --- a/momentum_cli.py +++ b/momentum_cli.py @@ -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() diff --git a/runtime/watcher.py b/runtime/watcher.py index 957b84a..8b2cbe7 100644 --- a/runtime/watcher.py +++ b/runtime/watcher.py @@ -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, }, ) ) diff --git a/strategy/evaluation/setup_evaluator.py b/strategy/evaluation/setup_evaluator.py index 6e24604..776ed68 100644 --- a/strategy/evaluation/setup_evaluator.py +++ b/strategy/evaluation/setup_evaluator.py @@ -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, ) diff --git a/strategy/models.py b/strategy/models.py index dbfb959..634a250 100644 --- a/strategy/models.py +++ b/strategy/models.py @@ -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}", + )