diff --git a/dashboard_publisher.py b/dashboard_publisher.py index 1bce95d..d6f7541 100644 --- a/dashboard_publisher.py +++ b/dashboard_publisher.py @@ -49,6 +49,7 @@ from datetime import datetime, timezone from logging.handlers import TimedRotatingFileHandler from typing import Dict, List, Optional +from agent_panel import attach_panel from config import PlatformConfig, Timeframe, XAUUSD from main import report_to_dict from news_engine import NewsEngine @@ -357,6 +358,13 @@ class DashboardPublisher: try: report = produce_signal(XAUUSD, tf, self._provider(), CONFIRMATIONS[tf]) + # Advisory AI panel, post-hoc and display-only. Off unless + # config.json agent_panel.dashboard is true. The reuse-TTL + # cache + max_panels_per_day budget bound the spend against + # this process's own DeepSeek cap (a persisting M5 signal + # re-serves its cached verdict instead of re-running every + # bar); a fresh panel adds โ‰ค~25 s before this TF publishes. + attach_panel(report, surface="dashboard") self.snapshots[tf.value] = { "status": "ok", "scanned_at": datetime.now(timezone.utc).isoformat(), diff --git a/main.py b/main.py index c49a7e1..27be14c 100755 --- a/main.py +++ b/main.py @@ -20,6 +20,7 @@ if hasattr(sys.stdout, "reconfigure"): if hasattr(sys.stderr, "reconfigure"): sys.stderr.reconfigure(encoding="utf-8") +from agent_panel import attach_panel from config import PlatformConfig, Timeframe, SignalStrength, XAUUSD from pair_signals import PAIR_PRODUCERS from signal_aggregator import SignalAggregator, SignalReport @@ -163,6 +164,13 @@ def print_signal_report(report: SignalReport, detail: bool = False): print(f" {Colors.MAGENTA}๐Ÿ“ฐ{Colors.RESET} {report.news_description}") if report.news_summary: print(f" {Colors.MAGENTA}๐Ÿง {Colors.RESET} {report.news_summary}") + if report.panel_verdict: + print(f" {Colors.CYAN}๐Ÿค–{Colors.RESET} AI Panel: " + f"{Colors.BOLD}{report.panel_verdict}{Colors.RESET} " + f"({report.panel_confidence:.0f}%) โ€” {report.panel_rationale}") + for t in report.panel_takes: + print(f" {Colors.GRAY}{t['persona']:<11} {t['stance']:<8} " + f"{t['note']}{Colors.RESET}") # โ”€โ”€ 6. DETAIL (only with --detail) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if detail and report.primary_analysis: @@ -209,6 +217,10 @@ def report_to_dict(report: SignalReport) -> dict: "news_sentiment": round(report.news_sentiment, 3), "news_description": report.news_description, "news_summary": report.news_summary, + "panel_verdict": report.panel_verdict, + "panel_confidence": round(report.panel_confidence, 1), + "panel_rationale": report.panel_rationale, + "panel_takes": report.panel_takes, "bullish_reasons": report.bullish_reasons, "bearish_reasons": report.bearish_reasons, "warnings": report.warnings, @@ -268,6 +280,9 @@ Examples: help="Primary analysis timeframe (default: H1)") parser.add_argument("--detail", action="store_true", help="Show full indicator breakdown") + parser.add_argument("--panel", action="store_true", + help="Run the LLM agent panel on the result " + "(advisory only; needs a DeepSeek key)") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--verbose", "-v", action="store_true", @@ -323,6 +338,11 @@ def run_live(args, config: PlatformConfig): aggregator = SignalAggregator(config) report = aggregator.analyze_pair(symbol=config.symbol) + # Explicit user intent: force bypasses the enabled/surface/quality + # toggles, but the daily panel budget and LLM cap still apply. + if getattr(args, "panel", False): + attach_panel(report, surface="cli", force=True) + if args.json: print(json.dumps(report_to_dict(report), indent=2)) return diff --git a/static/dashboard.html b/static/dashboard.html index 7d4e4d9..e479673 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -981,6 +981,22 @@ function detailHTML(tf) { ${esc(r.news_summary || "")}`); } + /* AI agent panel (advisory) */ + if (r.panel_verdict) { + const vcls = r.panel_verdict === "AGREE" ? "buy" + : r.panel_verdict === "OPPOSE" ? "sell" : "neutral"; + const takes = (r.panel_takes || []).map(t => + `${esc(t.persona)} + ${esc(t.stance)} + ${t.confidence != null ? Math.round(t.confidence) : "โ€”"} + ${esc(t.note || "")}`).join(""); + out += sec("AI panel (advisory)", `
+ ${esc(r.panel_verdict)} ยท + conf ${Math.round(r.panel_confidence || 0)}%
+ ${esc(r.panel_rationale || "")}
+ ${takes ? `${takes}
` : ""}`); + } + /* provenance */ out += sec("Scan detail", ` diff --git a/test_all.py b/test_all.py index 0cb4713..4f3db3a 100644 --- a/test_all.py +++ b/test_all.py @@ -7282,6 +7282,25 @@ def _(): ap.reset_panel_state() +@test("report_to_dict carries panel keys โ†’ dashboard snapshot propagation") +def _(): + from main import report_to_dict as _rtd + from dashboard_publisher import report_to_webapp_dict as _rtw + report = _panel_report() + d = _rtd(report) + assert d["panel_verdict"] == "" and d["panel_takes"] == [] + report.panel_verdict = "AGREE" + report.panel_confidence = 77.5 + report.panel_rationale = "solid confluence" + report.panel_takes = [{"persona": "risk", "stance": "support", + "confidence": 66.0, "note": "rr fine"}] + wd = _rtw(report) + assert wd["panel_verdict"] == "AGREE" + assert wd["panel_confidence"] == 77.5 + assert wd["panel_takes"][0]["persona"] == "risk" + json.dumps(wd, allow_nan=False) + + @test("agent_panel: attach never raises (LLM explosion, garbage settings)") def _(): class _Bomb:
Instrument${esc(r.display_name || r.symbol || "โ€”")}