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). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017975c1TufZ3xRYAq8iWTCS
This commit is contained in:
@@ -130,6 +130,7 @@
|
|||||||
<tr><td><code>/scan</code></td><td>Run an immediate analysis of the primary timeframe.</td></tr>
|
<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>/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>/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>/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>
|
<tr><td><code>/help</code></td><td>Command reference.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ WebSocket: Streams live ticks from the streaming provider — config.json
|
|||||||
EMA200) cached from the last boundary scan, triggers an
|
EMA200) cached from the last boundary scan, triggers an
|
||||||
immediate M15 scan and sends a Level Break Alert.
|
immediate M15 scan and sends a Level Break Alert.
|
||||||
5-minute cooldown prevents spam.
|
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
|
Owner lock: All commands and buttons only respond to TELEGRAM_CHAT_ID
|
||||||
(the owner). Everyone else gets a polite refusal — the bot
|
(the owner). Everyone else gets a polite refusal — the bot
|
||||||
@@ -69,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 news_engine import FundamentalContext, NewsEngine
|
||||||
from pair_signals import PAIR_PRODUCERS
|
from pair_signals import PAIR_PRODUCERS
|
||||||
from signal_aggregator import SignalAggregator, SignalReport
|
from signal_aggregator import SignalAggregator, SignalReport
|
||||||
|
|
||||||
@@ -951,6 +954,7 @@ _HELP_TEXT = (
|
|||||||
"/scan — Quick scan menu\n"
|
"/scan — Quick scan menu\n"
|
||||||
"/autoscan — Toggle auto-scan on/off\n"
|
"/autoscan — Toggle auto-scan on/off\n"
|
||||||
"/status — Bot status, WS connection, live prices\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"
|
"/cache — Show data cache age per pair/TF\n"
|
||||||
"/clearcache — Wipe data cache, force fresh fetch\n"
|
"/clearcache — Wipe data cache, force fresh fetch\n"
|
||||||
"/help — This message\n\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)
|
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
|
@_owner_only
|
||||||
async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
dp = _get_shared_dp()
|
dp = _get_shared_dp()
|
||||||
@@ -1245,6 +1279,7 @@ async def _post_init(app: Application) -> None:
|
|||||||
("scan", "Quick scan buttons"),
|
("scan", "Quick scan buttons"),
|
||||||
("autoscan", "Toggle auto-scan on/off"),
|
("autoscan", "Toggle auto-scan on/off"),
|
||||||
("status", "Bot status, WS connection, live prices"),
|
("status", "Bot status, WS connection, live prices"),
|
||||||
|
("news", "XAU/USD net news sentiment score"),
|
||||||
("cache", "Show data cache age per pair/TF"),
|
("cache", "Show data cache age per pair/TF"),
|
||||||
("clearcache", "Wipe data cache, force fresh fetch"),
|
("clearcache", "Wipe data cache, force fresh fetch"),
|
||||||
("help", "All commands"),
|
("help", "All commands"),
|
||||||
@@ -1374,6 +1409,7 @@ def main() -> None:
|
|||||||
app.add_handler(CommandHandler("scan", cmd_scan))
|
app.add_handler(CommandHandler("scan", cmd_scan))
|
||||||
app.add_handler(CommandHandler("help", cmd_help))
|
app.add_handler(CommandHandler("help", cmd_help))
|
||||||
app.add_handler(CommandHandler("status", cmd_status))
|
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("autoscan", cmd_autoscan))
|
||||||
app.add_handler(CommandHandler("cache", cmd_cache))
|
app.add_handler(CommandHandler("cache", cmd_cache))
|
||||||
app.add_handler(CommandHandler("clearcache", cmd_clearcache))
|
app.add_handler(CommandHandler("clearcache", cmd_clearcache))
|
||||||
|
|||||||
+26
-1
@@ -1591,7 +1591,7 @@ print("\n═══ Module 7: telegram_bot.py ═══")
|
|||||||
|
|
||||||
from telegram_bot import (
|
from telegram_bot import (
|
||||||
_escape_md, _signal_label, _mtf_line, _indicator_summary,
|
_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,
|
_seconds_until_boundary, _check_level_breach,
|
||||||
scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP,
|
scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP,
|
||||||
_update_level_cache, _last_prices, _level_cache, _level_cache_ts,
|
_update_level_cache, _last_prices, _level_cache, _level_cache_ts,
|
||||||
@@ -1881,6 +1881,31 @@ def _():
|
|||||||
assert ws_to_pair["XAU/USD"] == XAUUSD
|
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
|
# MODULE 8: Integration / Cross-module tests
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user