From 1f54452007c92cebb3d59723a807f9df7a546792 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Mon, 3 Aug 2026 18:01:42 -0400 Subject: [PATCH] Telegram /news command: net XAU/USD news sentiment score on demand Owner-locked /news replies with the existing get_pair_sentiment() net score (XAU mean - USD mean, -1..+1) and its label; a caveat line marks the no-headlines case so a feed outage can't read as neutral. Fetch runs in the shared executor with a placeholder-then-edit reply. Display-only: news still never gates signals. Registered in handler block, command menu, help text, docstring, and user guide; 3 new builder tests (299). Claude-Session: https://claude.ai/code/session_017975c1TufZ3xRYAq8iWTCS --- docs/user_guide.html | 1 + telegram_bot.py | 36 ++++++++++++++++++++++++++++++++++++ test_all.py | 27 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/user_guide.html b/docs/user_guide.html index 2db2087..a4a321a 100644 --- a/docs/user_guide.html +++ b/docs/user_guide.html @@ -130,6 +130,7 @@ /scanRun an immediate analysis of the primary timeframe. /autoscanToggle automatic scanning (M5 / M15 / H1 / H4 at each candle close). /statusUptime, auto-scan state, WebSocket price-monitor state. +/newsNet XAU/USD news sentiment score (informational only — never gates signals). /cache · /clearcacheInspect / reset the market-data cache. /helpCommand reference. diff --git a/telegram_bot.py b/telegram_bot.py index 41813e6..bb3978a 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -19,6 +19,8 @@ WebSocket: Streams live ticks from the streaming provider — config.json EMA200) cached from the last boundary scan, triggers an immediate M15 scan and sends a Level Break Alert. 5-minute cooldown prevents spam. +News: /news replies with the net XAU/USD news sentiment score + (display-only — news never gates signals). Owner lock: All commands and buttons only respond to TELEGRAM_CHAT_ID (the owner). Everyone else gets a polite refusal — the bot @@ -69,6 +71,7 @@ from app_config import ( telegram_channel_id, ) from providers import BaseDataProvider, make_data_provider +from news_engine import FundamentalContext, NewsEngine from pair_signals import PAIR_PRODUCERS from signal_aggregator import SignalAggregator, SignalReport @@ -951,6 +954,7 @@ _HELP_TEXT = ( "/scan — Quick scan menu\n" "/autoscan — Toggle auto-scan on/off\n" "/status — Bot status, WS connection, live prices\n" + "/news — Net news sentiment score for XAU/USD\n" "/cache — Show data cache age per pair/TF\n" "/clearcache — Wipe data cache, force fresh fetch\n" "/help — This message\n\n" @@ -1030,6 +1034,36 @@ async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None await update.message.reply_text(_status_text(), parse_mode=ParseMode.MARKDOWN) +def _news_text(context: FundamentalContext, engine: NewsEngine) -> str: + net, desc = engine.get_pair_sentiment(XAUUSD, context) + lines = [ + "📰 *XAU/USD News Score*\n", + f"*Net score:* `{net:+.2f}` — {desc}", + ] + # A cold/failed feed yields 0.00 "Neutral" — flag it so a data outage + # can't be mistaken for a genuinely neutral read. + if not context.news_sentiment: + lines.append("_no scored headlines yet — score defaults to 0_") + return "\n".join(lines) + + +def _news_report_sync() -> str: + engine = NewsEngine(PlatformConfig().news) + return _news_text(engine.get_fundamental_context(), engine) + + +@_owner_only +async def cmd_news(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + msg = await update.message.reply_text("⏳ Fetching news…") + loop = asyncio.get_running_loop() + try: + text = await loop.run_in_executor(_executor, _news_report_sync) + except Exception as exc: + logger.exception("News fetch failed") + text = f"❌ News fetch failed: {_escape_md(str(exc))}" + await msg.edit_text(text, parse_mode=ParseMode.MARKDOWN) + + @_owner_only async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: dp = _get_shared_dp() @@ -1245,6 +1279,7 @@ async def _post_init(app: Application) -> None: ("scan", "Quick scan buttons"), ("autoscan", "Toggle auto-scan on/off"), ("status", "Bot status, WS connection, live prices"), + ("news", "XAU/USD net news sentiment score"), ("cache", "Show data cache age per pair/TF"), ("clearcache", "Wipe data cache, force fresh fetch"), ("help", "All commands"), @@ -1374,6 +1409,7 @@ def main() -> None: app.add_handler(CommandHandler("scan", cmd_scan)) app.add_handler(CommandHandler("help", cmd_help)) app.add_handler(CommandHandler("status", cmd_status)) + app.add_handler(CommandHandler("news", cmd_news)) app.add_handler(CommandHandler("autoscan", cmd_autoscan)) app.add_handler(CommandHandler("cache", cmd_cache)) app.add_handler(CommandHandler("clearcache", cmd_clearcache)) diff --git a/test_all.py b/test_all.py index f8656ca..ebd2254 100644 --- a/test_all.py +++ b/test_all.py @@ -1591,7 +1591,7 @@ print("\n═══ Module 7: telegram_bot.py ═══") from telegram_bot import ( _escape_md, _signal_label, _mtf_line, _indicator_summary, - format_signal_message, _format_scan_summary, + format_signal_message, _format_scan_summary, _news_text, _seconds_until_boundary, _check_level_breach, scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP, _update_level_cache, _last_prices, _level_cache, _level_cache_ts, @@ -1881,6 +1881,31 @@ def _(): assert ws_to_pair["XAU/USD"] == XAUUSD +@test("_news_text bullish score") +def _(): + ctx = FundamentalContext(news_sentiment={"XAU": 0.5, "USD": -0.1}) + text = _news_text(ctx, NewsEngine(PlatformConfig().news)) + assert "+0.60" in text + assert "Bullish" in text + assert "no scored headlines" not in text + + +@test("_news_text bearish score") +def _(): + ctx = FundamentalContext(news_sentiment={"XAU": -0.4, "USD": 0.1}) + text = _news_text(ctx, NewsEngine(PlatformConfig().news)) + assert "-0.50" in text + assert "Bearish" in text + + +@test("_news_text degrades on empty context") +def _(): + text = _news_text(FundamentalContext(), NewsEngine(PlatformConfig().news)) + assert "+0.00" in text + assert "Neutral" in text + assert "no scored headlines" in text + + # ══════════════════════════════════════════════════════════════════════ # MODULE 8: Integration / Cross-module tests # ══════════════════════════════════════════════════════════════════════