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
This commit is contained in:
Harwinder Sekhon
2026-08-03 18:01:42 -04:00
parent be5dd4a25b
commit 1f54452007
3 changed files with 63 additions and 1 deletions
+36
View File
@@ -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))