From 0fc90009341f9ad43a36f60c558237b94eff8734 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Fri, 24 Jul 2026 10:58:22 -0400 Subject: [PATCH] Telegram alert redesign: verdict-first layout + informational 100-pip TP format_signal_message restyled (display-only, zero signal-logic impact): verdict header first (signal - pair - TF - quality), trade setup moved above the detail, per-indicator breakdown condensed to one arrow line (_indicator_summary), heavy dividers dropped. New TP 100p line in every setup block: entry +/- 100 pips ($10.00 on XAU) with its R-multiple vs the stop (_INFO_TP_PIPS, formatter-only - not stamped on TradeSetup, not in CLI/JSON/backtester). Trailing-plan logic unchanged (stamped per-TF params, pullback fallback). Removed dead _indicator_line and _SIGNAL_TAG. Tests 210 -> 212 (100p TP math BUY/SELL/no-setup, one-line indicator summary). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BoZKi67Ct6QTeiFJmybXSw --- CLAUDE.md | 2 +- telegram_bot.py | 126 ++++++++++++++++++++++++++++-------------------- test_all.py | 57 ++++++++++++++++++++-- 3 files changed, 127 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 27d0d3a..3b9dd73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -273,7 +273,7 @@ Run: `python telegram_bot.py` (credentials resolve env `TELEGRAM_TOKEN`/`TELEGRA - **WebSocket monitor**: subscribes every `scan_pairs` symbol to TD quotes WS; 10 s heartbeat; exponential reconnect (≤60 s). On tick crossing any cached key level (`swing_high/low_1..3`, `ema_trend`, `bb_upper/lower`, `kc_upper/lower`; cache TTL 1 h) → immediate M15 rescan → "LEVEL BREAK" alert if actionable+changed (same gates as auto-scan), 5-min per-pair cooldown. - **Commands**: `/start` (menu), `/scan` (buttons: per pair — M5/M15/H1/H4 + Scan All TFs), `/autoscan` (toggle DM copy), `/status` (loops, WS, live prices, level cache, cooldowns), `/cache` (age per pair/TF), `/clearcache`, `/help`. - **Tiingo spread monitor: REMOVED 2026-07-23** (shipped 2026-07-22, removed next day). Tiingo's free `/tiingo/fx/top` publishes a two-tier *indicative* spread for gold — exactly $0.10 in liquid hours and exactly $0.70 in the 21:00–00:00 UTC rollover window / news moments (verified: 273-sample production log + live probes; only those two values ever occur). Quotes are mid ± a posted increment, not executable depth, so the feature could never validate the $0.30 backtest cost assumption. Do not re-add a spread display from Tiingo's free FX feed. -- Signal messages include the full indicator breakdown, warnings, trade setup, **trailing-stop management plan** (arm price + trail distance, read from the `trail_arm_r`/`trail_dist_r` stamped on the setup by the producing per-TF config), and a risk disclaimer on every outbound message. +- Signal messages (redesigned 2026-07-24, `format_signal_message`): verdict header first, then trade setup, then a condensed one-line indicator summary (`EMA↑ RSI→ …`, `_indicator_summary`) + warnings. The setup block includes an **informational 100-pip TP line** (`_INFO_TP_PIPS`, display-only — entry ± 100 pips, ≈R vs stop; NOT part of the backtested exit model) and the **trailing-stop management plan** (arm price + trail distance, read from the `trail_arm_r`/`trail_dist_r` stamped on the setup by the producing per-TF config). Risk disclaimer on every outbound message. - Blocking scan work runs in `ThreadPoolExecutor(max_workers=4)`; one shared `ForexDataProvider` across all scans (cache reuse). - **Logging (hardened 2026-07-22)**: root-logger handlers (`force=True`) → console + `logs/telegram.log` (3-hour rotation, 240 backups = 30 days), full-date timestamps; every module logger (data_provider, news_engine, websockets, telegram.ext) lands in the same file. Failure surfaces all covered: candle loops catch-and-continue per cycle (one bad scan can't kill a loop), breach tasks log exceptions via done-callback, PTB `add_error_handler` registered, and `sys.excepthook`/`threading.excepthook` (installed in `main()`) route uncaught crashes to the log. WS monitor keeps its exponential-backoff reconnect + `logger.exception`. diff --git a/telegram_bot.py b/telegram_bot.py index 9fa97e8..8306bf2 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -223,15 +223,6 @@ _QUALITY_EMOJI = { "A+": "🏆", "A": "🥇", "B": "🥈", "C": "🥉", "D": "⚠️", "F": "❌", } -_SIGNAL_TAG = { - SignalStrength.STRONG_BUY: "Strong Buy", - SignalStrength.BUY: "Bullish", - SignalStrength.NEUTRAL: "Neutral", - SignalStrength.SELL: "Bearish", - SignalStrength.STRONG_SELL: "Strong Sell", -} - - def _signal_label(sig: SignalStrength) -> str: return sig.name.replace("_", " ") @@ -246,10 +237,36 @@ def _mtf_line(report: SignalReport) -> str: return " | ".join(parts) -def _indicator_line(name: str, ind) -> str: - tag = _SIGNAL_TAG.get(ind.signal, "Neutral") - short = ind.description.split(" | ")[0] if " | " in ind.description else ind.description - return f"[{tag}] *{_escape_md(ind.name)}*: {_escape_md(short)}" +# Informational fixed-pip target shown on every trade setup (display-only, +# not part of the backtested exit model). +_INFO_TP_PIPS = 100 + +_IND_SHORT = { + "ema_trend": "EMA", "rsi": "RSI", "macd": "MACD", "bollinger": "BB", + "adx": "ADX", "roc": "ROC", "keltner": "KC", "divergence": "Div", + "candlestick": "Cndl", +} + +_IND_ARROW = { + SignalStrength.STRONG_BUY: "⇈", + SignalStrength.BUY: "↑", + SignalStrength.NEUTRAL: "→", + SignalStrength.SELL: "↓", + SignalStrength.STRONG_SELL: "⇊", +} + + +def _indicator_summary(report: SignalReport) -> str: + """One-line condensed indicator readout: 'EMA↑ RSI↑ MACD→ …'.""" + if not report.primary_analysis: + return "" + parts = [] + for name, ind in report.primary_analysis.indicators.items(): + if name.startswith("_") or ind.weight == 0: + continue + label = _IND_SHORT.get(name, name) + parts.append(f"{label}{_IND_ARROW.get(ind.signal, '→')}") + return " ".join(parts) def _live_price_line(symbol: str) -> Optional[str]: @@ -276,46 +293,26 @@ def format_signal_message(report: SignalReport) -> str: emoji = _SIGNAL_EMOJI.get(sig, "⬜") qe = _QUALITY_EMOJI.get(report.signal_quality, "") label = _signal_label(sig) - mtf_tag = "✅ MTF aligned" if report.mtf_agreement else "❌ MTF diverged" - - lines: list[str] = [] + mtf_tag = "MTF ✅" if report.mtf_agreement else "MTF ❌" + tf = report.primary_analysis.timeframe if report.primary_analysis else "" now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") regime_str = _escape_md(report.market_regime.replace("_", " ").title()) vol_str = _escape_md(str(report.volatility_regime)) - lines += [ - f"🔍 *{report.display_name} Signal Analysis* ({report.primary_analysis.timeframe if report.primary_analysis else ''})", - f"🕐 {now}", - "", - f"💰 Close: *{_fmt_price(report.current_price)}*", + + lines: list[str] = [ + f"{emoji} *{label} · {report.display_name} · {tf}* {qe}{report.signal_quality}", + f"🕐 {now} · 💰 {_fmt_price(report.current_price)}", ] live = _live_price_line(report.symbol) if live: lines.append(live) lines += [ - f"📊 Regime: {regime_str} | Vol: {vol_str}", - "", - "━━━━━━━━━━━━━━━━━━━━", - f"{emoji} *{label}* {qe}{report.signal_quality} | Score: {report.final_score:+.1f} | {report.final_confidence:.0f}% conf", - f"_{_escape_md(report.action_recommendation)}_", - f"{mtf_tag}", + f"Score *{report.final_score:+.1f}* · Conf *{report.final_confidence:.0f}%* · {mtf_tag}", + f"📊 Regime: {regime_str} · Vol: {vol_str}", ] - - mtf = _mtf_line(report) - if mtf: - lines += ["", f"📈 *MTF*: {mtf}"] - - if report.primary_analysis: - lines += ["", "━━━━━━━━━━━━━━━━━━━━", "📋 *Indicators*"] - for name, ind in report.primary_analysis.indicators.items(): - if name.startswith("_") or ind.weight == 0: - continue - lines.append(_indicator_line(name, ind)) - - if report.warnings: - lines.append("") - for w in report.warnings: - lines.append(f"⚠️ {_escape_md(w)}") + if report.action_recommendation: + lines.append(f"_{_escape_md(report.action_recommendation)}_") if report.trade_setup and sig not in (SignalStrength.NEUTRAL,): ts = report.trade_setup @@ -324,16 +321,28 @@ def format_signal_message(report: SignalReport) -> str: reward_usd = ts.reward_pips * ts.pip_value lines += [ "", - "━━━━━━━━━━━━━━━━━━━━", - f"🚀 *TRADE SETUP*", + "🚀 *TRADE SETUP*", f"{dir_emoji} Direction: *{ts.direction}*", - f"▶ Entry: `{_fmt_price(ts.entry_price)}`", - f"🛑 SL: `{_fmt_price(ts.stop_loss)}` (${risk_usd:.2f} / {ts.risk_pips:.0f} pips)", - f"🎯 TP1: `{_fmt_price(ts.take_profit_1)}` (${reward_usd:.2f} / {ts.reward_pips:.0f} pips)", + f"▶ Entry `{_fmt_price(ts.entry_price)}`", + f"🛑 SL `{_fmt_price(ts.stop_loss)}` (${risk_usd:.2f} / {ts.risk_pips:.0f}p)", + f"🎯 TP1 `{_fmt_price(ts.take_profit_1)}` (${reward_usd:.2f} / {ts.reward_pips:.0f}p)", ] if ts.take_profit_2: - lines.append(f"🎯 TP2: `{_fmt_price(ts.take_profit_2)}`") - lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}") + lines.append(f"🎯 TP2 `{_fmt_price(ts.take_profit_2)}`") + + # Informational fixed-pip target: where a 100-pip TP would sit for + # this setup (display-only — not part of the backtested exit model). + info_dist = _INFO_TP_PIPS * ts.pip_value + if ts.direction == "BUY": + info_price = ts.entry_price + info_dist + else: + info_price = ts.entry_price - info_dist + risk_dist = abs(ts.entry_price - ts.stop_loss) + info_r = f" · ≈{info_dist / risk_dist:.1f}R" if risk_dist > 0 else "" + lines.append( + f"🎯 TP {_INFO_TP_PIPS}p: `{_fmt_price(info_price)}` (${info_dist:.2f}{info_r})" + ) + lines.append(f"⚖️ RR 1:{ts.risk_reward:.1f}") # Trailing-stop management plan (matches backtested exit model: # TP1 is advisory, the trail is the exit). Params are stamped on @@ -348,7 +357,6 @@ def format_signal_message(report: SignalReport) -> str: arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r if arm_r > 0: - risk_dist = abs(ts.entry_price - ts.stop_loss) if ts.direction == "BUY": arm_price = ts.entry_price + arm_r * risk_dist else: @@ -356,12 +364,24 @@ def format_signal_message(report: SignalReport) -> str: trail_dist = dist_r * risk_dist tag = " (pullback)" if is_pb else "" lines.append( - f"🔄 Trail{tag}: once price hits `{_fmt_price(arm_price)}` " + f"🔄 Trail{tag}: arm `{_fmt_price(arm_price)}` " f"(+{arm_r:.1f}R), trail SL ${trail_dist:.2f} " f"behind best price — TP1 is a guide, let it run" ) - lines += ["", "━━━━━━━━━━━━━━━━━━━━", _DISCLAIMER] + extra: list[str] = [] + mtf = _mtf_line(report) + if mtf: + extra.append(f"📈 MTF: {mtf}") + ind_summary = _indicator_summary(report) + if ind_summary: + extra.append(f"📋 {ind_summary}") + for w in report.warnings: + extra.append(f"⚠️ {_escape_md(w)}") + if extra: + lines += [""] + extra + + lines += ["", _DISCLAIMER] return "\n".join(lines) diff --git a/test_all.py b/test_all.py index a364a97..b651b8d 100644 --- a/test_all.py +++ b/test_all.py @@ -1555,7 +1555,7 @@ def _(): print("\n═══ Module 7: telegram_bot.py ═══") from telegram_bot import ( - _escape_md, _signal_label, _mtf_line, + _escape_md, _signal_label, _mtf_line, _indicator_summary, format_signal_message, _format_scan_summary, _seconds_until_boundary, _check_level_breach, scan_keyboard, SCAN_BUTTONS, SCAN_LOOKUP, @@ -1615,9 +1615,9 @@ def _(): assert isinstance(msg, str) assert len(msg) > 0 assert "XAU/USD" in msg - assert "Signal Analysis" in msg - assert "Price" in msg - assert "Indicators" in msg + assert "Score" in msg + assert "Regime" in msg + assert "Not financial advice" in msg @test("format_signal_message renders stamped per-TF trail plan") @@ -1644,6 +1644,55 @@ def _(): assert "$7.50" in msg +@test("format_signal_message renders 100-pip informational TP") +def _(): + def _setup(direction="BUY", **extra): + return TradeSetup( + pair="XAU/USD", display_name="XAU/USD", direction=direction, + entry_price=3300.0, stop_loss=3290.0, take_profit_1=3315.0, + take_profit_2=3325.0, risk_reward=1.5, risk_pips=100, + reward_pips=150, pip_value=0.10, **extra, + ) + report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now") + report.final_signal = SignalStrength.BUY + report.signal_quality = "A" + # BUY: 100 pips × $0.10 = $10 above entry; risk $10 → exactly 1.0R. + report.trade_setup = _setup() + msg = format_signal_message(report) + assert "TP 100p" in msg + assert "3310.00" in msg, "BUY 100p TP must be entry + $10.00" + assert "≈1.0R" in msg + # SELL mirror: $10 below entry. + report.final_signal = SignalStrength.SELL + report.trade_setup = _setup(direction="SELL") + msg = format_signal_message(report) + assert "TP 100p" in msg + assert "3290.00" in msg, "SELL 100p TP must be entry - $10.00" + # No setup → no informational TP line. + report.trade_setup = None + msg = format_signal_message(report) + assert "TP 100p" not in msg + + +@test("_indicator_summary condenses indicators to one arrow line") +def _(): + config = PlatformConfig() + agg = SignalAggregator(config) + dfs = { + Timeframe.H1: make_ohlcv(300, seed=42), + Timeframe.M15: make_ohlcv(300, seed=43), + Timeframe.H4: make_ohlcv(300, seed=44), + } + report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False) + summary = _indicator_summary(report) + assert "EMA" in summary + assert "RSI" in summary + assert summary.count("\n") == 0, "summary must be a single line" + msg = format_signal_message(report) + assert summary in msg + assert "[Bullish]" not in msg and "[Bearish]" not in msg and "[Neutral]" not in msg + + @test("_format_scan_summary produces valid text") def _(): config = PlatformConfig()