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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SisdfkxnXNL9iZsXxN3T15
This commit is contained in:
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user