telegram: attach AI panel to alert-quality sends (advisory block)

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.

Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15
This commit is contained in:
Harwinder Sekhon
2026-08-07 22:32:57 -04:00
parent 7be2231eb3
commit a7c2bfc7da
2 changed files with 47 additions and 0 deletions
+27
View File
@@ -71,6 +71,7 @@ from app_config import (
telegram_channel_id, telegram_channel_id,
) )
from providers import BaseDataProvider, make_data_provider from providers import BaseDataProvider, make_data_provider
from agent_panel import attach_panel, panel_health
from llm_client import get_llm_client from llm_client import get_llm_client
from news_engine import FundamentalContext, NewsEngine from news_engine import FundamentalContext, NewsEngine
from pair_signals import PAIR_PRODUCERS from pair_signals import PAIR_PRODUCERS
@@ -407,6 +408,17 @@ def format_signal_message(report: SignalReport) -> str:
extra.append(f"⚠️ {_escape_md(w)}") extra.append(f"⚠️ {_escape_md(w)}")
if report.news_summary: if report.news_summary:
extra.append(f"📰 {_escape_md(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: if extra:
lines += [""] + extra lines += [""] + extra
@@ -720,6 +732,13 @@ async def _scan_timeframe(
async with _alert_lock(key): async with _alert_lock(key):
if not _should_alert(key, report): if not _should_alert(key, report):
continue 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) detail = format_signal_message(report)
delivered = False delivered = False
for chat_id in _signal_targets(): 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): if not _should_alert(key, report):
return 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 = ( header = (
f"⚡ *LEVEL BREAK* — {_escape_md(display)} crossed " f"⚡ *LEVEL BREAK* — {_escape_md(display)} crossed "
f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n" f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n"
@@ -1193,6 +1216,10 @@ def _news_health_lines() -> List[str]:
else: else:
lines.append(f" ✅ DeepSeek: {used}/{cap} calls today," lines.append(f" ✅ DeepSeek: {used}/{cap} calls today,"
f" {scored} headlines scored") 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 return lines
+20
View File
@@ -7235,6 +7235,26 @@ def _():
ap.reset_panel_state() 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") @test("backtest parity: analyze_from_dataframes never populates panel")
def _(): def _():
try: try: