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
+1
View File
@@ -130,6 +130,7 @@
<tr><td><code>/scan</code></td><td>Run an immediate analysis of the primary timeframe.</td></tr>
<tr><td><code>/autoscan</code></td><td>Toggle automatic scanning (M5 / M15 / H1 / H4 at each candle close).</td></tr>
<tr><td><code>/status</code></td><td>Uptime, auto-scan state, WebSocket price-monitor state.</td></tr>
<tr><td><code>/news</code></td><td>Net XAU/USD news sentiment score (informational only — never gates signals).</td></tr>
<tr><td><code>/cache</code> · <code>/clearcache</code></td><td>Inspect / reset the market-data cache.</td></tr>
<tr><td><code>/help</code></td><td>Command reference.</td></tr>
</table>
+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))
+26 -1
View File
@@ -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
# ══════════════════════════════════════════════════════════════════════