From 3fbe45e01316b20fdd0b218ffaa282c58219dfd7 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Fri, 7 Aug 2026 22:32:57 -0400 Subject: [PATCH] telegram: attach AI panel to alert-quality sends (advisory block) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-scan and level-breach alert paths attach the panel AFTER the quality gate and dedup decision (no LLM spend on suppressed alerts), via run_in_executor so the event loop stays free. Alert message gains a two-line ๐Ÿค– AI Panel block (verdict + confidence + rationale, persona votes); /status shows the daily panel budget when the feature is on. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15 --- telegram_bot.py | 27 +++++++++++++++++++++++++++ test_all.py | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/telegram_bot.py b/telegram_bot.py index a4d8971..31a06ab 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -71,6 +71,7 @@ from app_config import ( telegram_channel_id, ) from providers import BaseDataProvider, make_data_provider +from agent_panel import attach_panel, panel_health from llm_client import get_llm_client from news_engine import FundamentalContext, NewsEngine from pair_signals import PAIR_PRODUCERS @@ -407,6 +408,17 @@ def format_signal_message(report: SignalReport) -> str: extra.append(f"โš ๏ธ {_escape_md(w)}") if report.news_summary: extra.append(f"๐Ÿ“ฐ {_escape_md(report.news_summary)}") + if report.panel_verdict: + mark = {"AGREE": "โœ…", "OPPOSE": "โ›”", "NEUTRAL": "โž–"}.get( + report.panel_verdict, "") + extra.append( + f"๐Ÿค– AI Panel {mark} *{_escape_md(report.panel_verdict)}* " + f"({report.panel_confidence:.0f}%) โ€” " + f"{_escape_md(report.panel_rationale)}") + if report.panel_takes: + votes = " ยท ".join( + f"{t['persona']}:{t['stance'][:3]}" for t in report.panel_takes) + extra.append(f" _{_escape_md(votes)}_") if extra: lines += [""] + extra @@ -720,6 +732,13 @@ async def _scan_timeframe( async with _alert_lock(key): if not _should_alert(key, report): continue + # Panel AFTER the quality gate + dedup decision so no LLM + # spend on suppressed alerts. Runs on the scan executor to + # keep the event loop free; this key's alert lock is held + # for the panel's wall time (โ‰ค~25 s), which only delays a + # same-key duplicate that _should_alert would drop anyway. + await asyncio.get_running_loop().run_in_executor( + _executor, attach_panel, report) detail = format_signal_message(report) delivered = False for chat_id in _signal_targets(): @@ -855,6 +874,10 @@ async def _handle_breach(bot, pair_symbol: str, price: float, level_name: str) - if not _should_alert(key, report): return + # Same post-gate panel attach as the auto-scan path (see + # _scan_timeframe for the spend/lock rationale). + await asyncio.get_running_loop().run_in_executor( + _executor, attach_panel, report) header = ( f"โšก *LEVEL BREAK* โ€” {_escape_md(display)} crossed " f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n" @@ -1193,6 +1216,10 @@ def _news_health_lines() -> List[str]: else: lines.append(f" โœ… DeepSeek: {used}/{cap} calls today," f" {scored} headlines scored") + if get_app_config().agent_panel_settings.get("enabled", False): + ph = panel_health() + lines.append(f" ๐Ÿค– AI panel: {ph['used_today']}/{ph['cap']}" + " panels today") return lines diff --git a/test_all.py b/test_all.py index 361c17d..0cb4713 100644 --- a/test_all.py +++ b/test_all.py @@ -7235,6 +7235,26 @@ def _(): ap.reset_panel_state() +@test("panel verdict flows to Telegram message; absent verdict silent") +def _(): + import telegram_bot as tb + report = _panel_report() + assert "AI Panel" not in tb.format_signal_message(report) + report.panel_verdict = "OPPOSE" + report.panel_confidence = 64.0 + report.panel_rationale = "counter-trend against aligned H4" + report.panel_takes = [ + {"persona": "macro", "stance": "support", "confidence": 60, + "note": "n"}, + {"persona": "technical", "stance": "oppose", "confidence": 80, + "note": "n"}, + ] + msg = tb.format_signal_message(report) + assert "AI Panel" in msg and "OPPOSE" in msg and "(64%)" in msg + assert "counter-trend against aligned H4" in msg + assert "macro:sup" in msg and "technical:opp" in msg + + @test("backtest parity: analyze_from_dataframes never populates panel") def _(): try: