From 4aba24347212fd68636d34c2e4dda1babc34b07f Mon Sep 17 00:00:00 2001 From: Harwinder Date: Mon, 4 May 2026 17:16:00 -0400 Subject: [PATCH] Rebuild Telegram bot with trade alert template and grade-filtered scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace /scan+/pair commands with /signal (single pair) and /scan (all pairs, B+ only) - Implement exact visual template: grade dot (🟢/🟡/🔴), TRADE ALERT header, Entry/TP/SL/R:R block, full indicator list with value + condition - Grade dot enforced: A/A+=🟢, B=🟡, C/D/F=🔴 - /scan filters to minimum Grade B confidence before sending alerts - HTML parse mode with html.escape() on all dynamic content - Per-chat default timeframe state preserved Co-Authored-By: Claude Sonnet 4.6 --- telegram_bot.py | 518 +++++++++++++++++++++++++----------------------- 1 file changed, 269 insertions(+), 249 deletions(-) diff --git a/telegram_bot.py b/telegram_bot.py index d270f8d..9b32d25 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 """ Forex Signal Intelligence Platform - Telegram Bot -=================================================== -Mirrors main.py output exactly via Telegram. +================================================== +Trade alert frontend for the signal engine. Setup: - export TELEGRAM_BOT_TOKEN="your_token_here" + set TELEGRAM_BOT_TOKEN=your_token_here python telegram_bot.py Commands: - /scan [TF] — Full signal scan (all major pairs) - /pair EURUSD [TF] — Detailed analysis for one pair - /tf H1 — Set default timeframe - /help — Commands + timeframe guide + /signal EURUSD [TF] — Signal card for one pair + /scan [TF] — Scan all pairs, show Grade B+ alerts + /tf H1 — Set default timeframe + /help — Command guide """ import asyncio +import html import logging import os from concurrent.futures import ThreadPoolExecutor @@ -34,7 +35,7 @@ from signal_aggregator import SignalAggregator, SignalReport logger = logging.getLogger(__name__) -# ── Timeframe config ─────────────────────────────────────────────── +# ── Timeframe maps ────────────────────────────────────────────────── TF_MAP = { "M5": Timeframe.M5, @@ -54,193 +55,196 @@ TF_CONFIRMATIONS = { "D1": [Timeframe.H4, Timeframe.W1], } -# ── Formatting (mirrors main.py, no ANSI colours) ────────────────── +# ── Formatting helpers ────────────────────────────────────────────── -WIDTH = 80 +def _grade_dot(quality: str) -> str: + """Colored dot matching confidence grade per spec.""" + if quality in ("A+", "A"): + return "🟢" + if quality == "B": + return "🟡" + return "🔴" -def _score_bar(score: float, width: int = 30) -> str: - normalized = (score + 100) / 200 - filled = int(normalized * width) - center = width // 2 - bar = "" - for i in range(width): - if i == center: - bar += "│" - elif (score >= 0 and center < i <= filled) or \ - (score < 0 and filled <= i < center): - bar += "█" - else: - bar += "░" - return bar +def _signal_action(signal: SignalStrength) -> str: + return { + SignalStrength.STRONG_BUY: "STRONG BUY", + SignalStrength.BUY: "BUY", + SignalStrength.STRONG_SELL: "STRONG SELL", + SignalStrength.SELL: "SELL", + SignalStrength.NEUTRAL: "NEUTRAL", + }.get(signal, "NEUTRAL") -def _format_header(timeframe: str) -> str: - now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - lines = [ - "", - "═" * WIDTH, - " ╔═══════════════════════════════════════════════════════╗", - " ║ FOREX SIGNAL INTELLIGENCE PLATFORM ║", - " ║ Multi-Layer Signal Analysis Engine ║", - " ╚═══════════════════════════════════════════════════════╝", - f" Scan Time: {now} TF: {timeframe}", - "═" * WIDTH, - "", - ] - return "\n".join(lines) - - -def _format_table(reports: List[SignalReport]) -> str: - lines = [ - " ┌─────────────┬───────────┬──────────────────────────────────┬────────┬────────────┐", - " │ Pair │ Signal │ Score │ Grade │ Confidence │", - " ├─────────────┼───────────┼──────────────────────────────────┼────────┼────────────┤", - ] - for r in reports: - sig = r.final_signal.name.replace("_", " ") - bar = _score_bar(r.final_score, 30) - pair = f"{r.display_name:<11}" - signal_str = f"{sig:<9}" - grade_str = f"{r.signal_quality:<6}" - conf_str = f"{r.final_confidence:5.1f}%" - warn = " ⚠" if r.event_risk == "danger" else (" △" if r.event_risk == "caution" else " ") - lines.append(f" │ {pair} │ {signal_str} │ {bar} │ {grade_str} │ {conf_str:>8}{warn} │") - lines.append( - " └─────────────┴───────────┴──────────────────────────────────┴────────┴────────────┘" - ) - lines += [ - "", - " Legend: ██ Bullish | ██ Bearish | │ Center (0) | ⚠ Event Risk | △ Caution", - ] - return "\n".join(lines) - - -def _format_calendar(reports: List[SignalReport]) -> str: - events_shown = set() - lines = [] - for r in reports: - if r.event_risk != "clear" and r.event_description not in events_shown: - events_shown.add(r.event_description) - lines.append(f" ● {r.event_description} → affects {r.display_name}") - if lines: - return "\n 📅 ECONOMIC CALENDAR ALERTS\n" + "\n".join(lines) - return "\n 📅 No high-impact events in the near term" - - -def _format_detail(r: SignalReport) -> List[str]: +def _indicator_condition(ind) -> str: """ - Returns a list of message strings for one pair's detail report. - Split into two messages so each stays under Telegram's 4096-char limit. + Extract a short readable condition from the indicator description. + Prefer text after ' — ' (e.g. 'RSI 68.2 — Overbought' → 'Overbought'). + Fall back to the signal enum name. """ - sig_name = r.final_signal.name.replace("_", " ") + desc = ind.description + if " — " in desc: + return desc.split(" — ", 1)[1].strip() + if " | " in desc: + # e.g. 'MACD bullish crossover | Hist: 0.00008' — take first clause + return desc.split(" | ", 1)[0].strip() + return ind.signal.name.replace("_", " ").title() - # ── Part 1: header, signal, MTF, indicators ──────────────────── - p1 = ["─" * WIDTH] - p1.append( - f" {r.display_name} │ Price: {r.current_price:.5f}" - f" │ Regime: {r.market_regime} │ Vol: {r.volatility_regime}" - ) - p1.append( - f"\n SIGNAL: {sig_name} │ Score: {r.final_score:+.1f}" - f" │ Grade: {r.signal_quality} │ Confidence: {r.final_confidence:.0f}%" - ) - p1.append(f" Action: {r.action_recommendation}") - if r.mtf_signals: - agree = "✓ ALIGNED" if r.mtf_agreement else "✗ DIVERGENT" - p1.append(f"\n Multi-Timeframe: {agree}") - for tf, sig in r.mtf_signals.items(): - score = r.mtf_scores.get(tf, 0) - p1.append(f" {tf:>5}: {sig.name.replace('_', ' '):<12} ({score:+.1f})") +def _fmt_float(val: float) -> str: + """Human-readable float with appropriate precision.""" + if val == 0.0: + return "0" + av = abs(val) + if av >= 1000: + return f"{val:.1f}" + if av >= 10: + return f"{val:.2f}" + if av >= 0.01: + return f"{val:.4f}" + return f"{val:.6f}" - if r.primary_analysis: - p1.append(f"\n Indicators ({r.primary_analysis.timeframe}):") - for name, ind in r.primary_analysis.indicators.items(): + +def _h(text: str) -> str: + """HTML-escape a string for safe use inside HTML parse mode.""" + return html.escape(str(text)) + + +def _build_alert(report: SignalReport, timeframe: str) -> str: + """ + Build a single trade alert card using the required visual template. + Returns an HTML-formatted string ready for Telegram. + + Template: + {Dot} **TRADE ALERT: {ACTION} {PAIR}** + Grade / confidence / timeframe subtitle + + **Entry:** {price} + **Take Profit:** {price} (+pips) + **Stop Loss:** {price} (-pips) + **Risk:Reward:** 1:2.0 + + **Technical Indicators:** + • {Name}: {Value} ({Condition}) + ... + + Multi-Timeframe block + Warnings / events / news + Market context line + Disclaimer + """ + dot = _grade_dot(report.signal_quality) + action = _signal_action(report.final_signal) + pair = _h(report.display_name) + grade = _h(report.signal_quality) + conf = report.final_confidence + + lines: List[str] = [] + + # ── Header ─────────────────────────────────────────────────────── + lines.append(f"{dot} TRADE ALERT: {action} {pair}") + lines.append(f"Grade: {grade} | Confidence: {conf:.0f}% | {timeframe}") + lines.append("") + + # ── Trade setup (only when grade B or above) ───────────────────── + if report.trade_setup: + ts = report.trade_setup + lines.append(f"Entry: {ts.entry_price:.5f}") + lines.append( + f"Take Profit: {ts.take_profit_1:.5f}" + f" (+{ts.reward_pips:.1f} pips)" + ) + if abs(ts.take_profit_2 - ts.take_profit_1) > 1e-8: + lines.append(f"Take Profit 2: {ts.take_profit_2:.5f}") + lines.append( + f"Stop Loss: {ts.stop_loss:.5f}" + f" (-{ts.risk_pips:.1f} pips)" + ) + lines.append(f"Risk:Reward: 1:{ts.risk_reward:.1f}") + else: + # No actionable setup — show price and recommendation + lines.append(f"Current Price: {report.current_price:.5f}") + lines.append(f"Analysis: {_h(report.action_recommendation)}") + + lines.append("") + + # ── Technical Indicators ───────────────────────────────────────── + if report.primary_analysis and report.primary_analysis.indicators: + lines.append("Technical Indicators:") + for name, ind in report.primary_analysis.indicators.items(): if name.startswith("_") or ind.weight == 0: continue - ind_name = f"{ind.name:<18}" - sig_str = f"{ind.signal.name.replace('_', ' '):<12}" - desc = ind.description[:60] - p1.append(f" {ind_name} {sig_str} ({ind.confidence:.0f}%) │ {desc}") + val_str = _fmt_float(ind.value) + condition = _h(_indicator_condition(ind)) + ind_name = _h(ind.name) + lines.append(f"• {ind_name}: {val_str} ({condition})") - # ── Part 2: reasons, warnings, trade setup, key levels ───────── - p2 = [] + # ── Multi-Timeframe ─────────────────────────────────────────────── + if report.mtf_signals: + lines.append("") + agree_str = "✅ Aligned" if report.mtf_agreement else "⚠️ Divergent" + lines.append(f"Multi-Timeframe ({agree_str}):") + for tf, sig in report.mtf_signals.items(): + score = report.mtf_scores.get(tf, 0) + sig_name = _h(sig.name.replace("_", " ")) + lines.append(f" {_h(tf)}: {sig_name} ({score:+.0f})") - if r.bullish_reasons: - p2.append(" ▲ Bullish Factors:") - for reason in r.bullish_reasons[:5]: - p2.append(f" + {reason}") + # ── Warnings ───────────────────────────────────────────────────── + if report.warnings: + lines.append("") + for w in report.warnings: + lines.append(f"⚠️ {_h(w)}") - if r.bearish_reasons: - p2.append("\n ▼ Bearish Factors:") - for reason in r.bearish_reasons[:5]: - p2.append(f" - {reason}") + # ── Calendar / news ─────────────────────────────────────────────── + if report.event_risk != "clear" and report.event_description: + lines.append(f"📅 Event Risk: {_h(report.event_description)}") - if r.warnings: - p2.append("\n ⚠ Warnings:") - for w in r.warnings: - p2.append(f" ! {w}") + if report.news_description and report.news_description != "Neutral news sentiment": + lines.append(f"📰 {_h(report.news_description)}") - if r.news_description and r.news_description != "Neutral news sentiment": - p2.append(f"\n 📰 News: {r.news_description}") - if r.event_description and r.event_risk != "clear": - p2.append(f" 📅 Event: {r.event_description}") + # ── Context footer ──────────────────────────────────────────────── + lines.append("") + lines.append( + f"Regime: {_h(report.market_regime)} | " + f"Vol: {_h(report.volatility_regime)} | " + f"Score: {report.final_score:+.1f}" + ) + lines.append("⚠️ Educational use only. Not financial advice.") - if r.trade_setup: - ts = r.trade_setup - p2 += [ - "\n Trade Setup:", - f" Direction: {ts.direction}", - f" Entry: {ts.entry_price:.5f}", - f" Stop Loss: {ts.stop_loss:.5f} ({ts.risk_pips:.1f} pips)", - f" Take Profit 1: {ts.take_profit_1:.5f} ({ts.reward_pips:.1f} pips)", - f" Take Profit 2: {ts.take_profit_2:.5f}", - f" Risk:Reward: 1:{ts.risk_reward:.1f}", - ] - - if r.primary_analysis and r.primary_analysis.key_levels: - lvls = r.primary_analysis.key_levels - p2.append("\n Key Levels:") - for k in ("r3", "r2", "r1", "pivot", "s1", "s2", "s3"): - if k in lvls: - p2.append(f" {k.upper():>6}: {lvls[k]:.5f}") - - result = ["\n".join(p1)] - if p2: - result.append("\n".join(p2)) - return result + return "\n".join(lines) -# ── Per-chat state ───────────────────────────────────────────────── +# ── Per-chat state ────────────────────────────────────────────────── class ChatState: def __init__(self): self.timeframe: str = "H1" -# ══════════════════════════════════════════════════════════════════ -# BOT -# ══════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════ +# Bot +# ══════════════════════════════════════════════════════════════════════ class ForexTelegramBot: def __init__(self): self._states: Dict[int, ChatState] = {} self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="forex") + # ── Helpers ─────────────────────────────────────────────────────── + def _state(self, chat_id: int) -> ChatState: if chat_id not in self._states: self._states[chat_id] = ChatState() return self._states[chat_id] - def _build_config(self, tf: str, pairs: list = None) -> PlatformConfig: - config = PlatformConfig() - config.primary_timeframe = TF_MAP[tf] - config.confirmation_timeframes = TF_CONFIRMATIONS.get(tf, [Timeframe.M15, Timeframe.H4]) - config.watch_pairs = list(pairs or MAJOR_PAIRS) - return config + def _build_config(self, tf: str, pairs: Optional[List[str]] = None) -> PlatformConfig: + cfg = PlatformConfig() + cfg.primary_timeframe = TF_MAP[tf] + cfg.confirmation_timeframes = TF_CONFIRMATIONS.get(tf, [Timeframe.M15, Timeframe.H4]) + cfg.watch_pairs = list(pairs or MAJOR_PAIRS) + return cfg - def _run_scan(self, tf: str, pairs: list) -> List[SignalReport]: + def _run_scan(self, tf: str, pairs: List[str]) -> List[SignalReport]: return SignalAggregator(self._build_config(tf, pairs)).analyze_all_pairs() async def _analyze(self, func, *args): @@ -248,33 +252,30 @@ class ForexTelegramBot: return await loop.run_in_executor(self._executor, func, *args) @staticmethod - def _parse_tf(args: list) -> Optional[str]: + def _parse_tf(args: List[str]) -> Optional[str]: for a in args: if a.upper() in TF_MAP: return a.upper() return None @staticmethod - async def _send_pre(update: Update, text: str): - """Send text as a monospace pre block, splitting if over 4096 chars.""" - LIMIT = 3800 # leave room for
 tags
-        chunks = []
+    async def _send_html(update: Update, text: str):
+        """Send HTML message, splitting at newlines if over Telegram's 4096-char limit."""
+        LIMIT = 4000
+        chunks: List[str] = []
         while text:
             if len(text) <= LIMIT:
                 chunks.append(text)
                 break
-            # Split at last newline before limit
             split_at = text.rfind("\n", 0, LIMIT)
             if split_at == -1:
                 split_at = LIMIT
             chunks.append(text[:split_at])
             text = text[split_at:].lstrip("\n")
         for chunk in chunks:
-            await update.message.reply_text(
-                f"
{chunk}
", parse_mode=ParseMode.HTML - ) + await update.message.reply_text(chunk, parse_mode=ParseMode.HTML) - # ── Commands ─────────────────────────────────────────────────── + # ── Commands ────────────────────────────────────────────────────── async def cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): await self.cmd_help(update, context) @@ -282,22 +283,26 @@ class ForexTelegramBot: async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE): state = self._state(update.effective_chat.id) text = ( - "🌐 Forex Signal Intelligence Platform\n\n" + "🌐 Forex Signal Intelligence Platform\n\n" "Commands:\n" - " /scan — Scan all major pairs\n" - " /scan H4 — Scan with specific timeframe\n" - " /pair EURUSD — Detailed analysis\n" - " /pair EURUSD H4 — With specific timeframe\n" - " /tf H1 — Set default timeframe\n" + " /signal EURUSD — Signal card for a pair\n" + " /signal EURUSD H4 — With specific timeframe\n" + " /scan — Scan all pairs (Grade B+ alerts only)\n" + " /scan H4 — Scan at a specific timeframe\n" + " /tf H1 — Set your default timeframe\n" " /help — This message\n\n" "Timeframes:\n" " M5 — 5 minutes\n" " M15 — 15 minutes\n" " M30 — 30 minutes\n" - " H1 — 1 hour ← recommended for intraday\n" - " H4 — 4 hours ← recommended for swing trading\n" - " D1 — Daily ← recommended for position trading\n\n" - f"Current default: {state.timeframe}\n\n" + " H1 — 1 hour (default, intraday)\n" + " H4 — 4 hours (swing trading)\n" + " D1 — Daily (position trading)\n\n" + "Signal Grades:\n" + " 🟢 A / A+ — High confidence, consider entry\n" + " 🟡 B — Decent setup, wait for confirmation\n" + " 🔴 C / D — Low confidence, watch only\n\n" + f"Your default timeframe: {state.timeframe}\n\n" "⚠️ Educational use only. Not financial advice." ) await update.message.reply_text(text, parse_mode=ParseMode.HTML) @@ -307,65 +312,29 @@ class ForexTelegramBot: if not tf: opts = " ".join(TF_MAP.keys()) await update.message.reply_text( - f"Usage: /tf <timeframe>\n" - f"Options: {opts}", + f"Usage: /tf <timeframe>\nOptions: {opts}", parse_mode=ParseMode.HTML, ) return self._state(update.effective_chat.id).timeframe = tf await update.message.reply_text( - f"✅ Default timeframe set to {tf}", parse_mode=ParseMode.HTML + f"✅ Default timeframe set to {tf}", + parse_mode=ParseMode.HTML, ) - async def cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + async def cmd_signal(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + /signal EURUSD [TF] + Analyze a single pair and return the full trade alert card. + """ chat_id = update.effective_chat.id - state = self._state(chat_id) - tf = self._parse_tf(context.args or []) or state.timeframe - - msg = await update.message.reply_text(f"🔄 Scanning markets ({tf})…") - try: - reports = await self._analyze(self._run_scan, tf, list(MAJOR_PAIRS)) - - # ── summary block (header + table + calendar) ────────── - summary = ( - _format_header(tf) - + _format_table(reports) - + _format_calendar(reports) - ) - await msg.edit_text(f"
{summary}
", parse_mode=ParseMode.HTML) - - # ── detailed block for each actionable signal ────────── - actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")] - if actionable: - header_line = "\n ═══ ACTIONABLE SIGNALS (Grade B or above) ═══\n" - first = True - for r in actionable: - parts = _format_detail(r) - if first: - parts[0] = header_line + parts[0] - first = False - for part in parts: - await self._send_pre(update, part) - else: - await self._send_pre( - update, - "\n No high-grade signals detected.\n" - " Use /scan with a different timeframe to see more.\n", - ) - - except Exception as e: - logger.error(f"Scan error: {e}") - await msg.edit_text(f"❌ Scan failed: {e}") - - async def cmd_pair(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = update.effective_chat.id - state = self._state(chat_id) - args = context.args or [] + state = self._state(chat_id) + args = context.args or [] if not args: await update.message.reply_text( - "Usage: /pair <symbol> [timeframe]\n" - "Example: /pair EURUSD H4", + "Usage: /signal <pair> [timeframe]\n" + "Example: /signal EURUSD H4", parse_mode=ParseMode.HTML, ) return @@ -373,28 +342,78 @@ class ForexTelegramBot: raw = args[0].upper().replace("/", "").replace("_", "") if not raw.endswith("=X"): raw += "=X" - tf = self._parse_tf(args[1:]) or state.timeframe - display = SYMBOL_DISPLAY.get(raw, raw) + tf = self._parse_tf(args[1:]) or state.timeframe + display = SYMBOL_DISPLAY.get(raw, raw.replace("=X", "")) - msg = await update.message.reply_text(f"🔄 Analyzing {display} ({tf})…") + msg = await update.message.reply_text(f"🔄 Analyzing {_h(display)} ({tf})…") try: reports = await self._analyze(self._run_scan, tf, [raw]) if not reports: - await msg.edit_text(f"❌ No data returned for {raw}") + await msg.edit_text(f"❌ No data returned for {_h(display)}") return - r = reports[0] - summary = _format_header(tf) + _format_table([r]) - await msg.edit_text(f"
{summary}
", parse_mode=ParseMode.HTML) - - for part in _format_detail(r): - await self._send_pre(update, part) + r = reports[0] + alert = _build_alert(r, tf) + await msg.delete() + await self._send_html(update, alert) except Exception as e: - logger.error(f"Pair error for {raw}: {e}") - await msg.edit_text(f"❌ Analysis failed: {e}") + logger.error(f"Signal error for {raw}: {e}", exc_info=True) + await msg.edit_text(f"❌ Analysis failed: {_h(str(e))}") - # ── Application builder ──────────────────────────────────────── + async def cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + /scan [TF] + Analyze all major pairs and send a trade alert card for every + signal rated Grade B or above. + """ + chat_id = update.effective_chat.id + state = self._state(chat_id) + tf = self._parse_tf(context.args or []) or state.timeframe + + now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + msg = await update.message.reply_text(f"🔄 Scanning all pairs ({tf})…") + + try: + reports = await self._analyze(self._run_scan, tf, list(MAJOR_PAIRS)) + + actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")] + + if not actionable: + await msg.edit_text( + f"🔍 Scan complete — {now_str}\n\n" + "No Grade B+ signals found at this time.\n" + "Try a different timeframe with /scan H4.", + parse_mode=ParseMode.HTML, + ) + return + + # Summary header + grades = {"A+": 0, "A": 0, "B": 0} + for r in actionable: + grades[r.signal_quality] = grades.get(r.signal_quality, 0) + 1 + + grade_str = " ".join( + f"{g}: {count}" for g, count in grades.items() if count + ) + summary = ( + f"📊 Scan complete — {now_str}\n" + f"Timeframe: {tf}\n" + f"Grade B+ signals: {len(actionable)} ({grade_str})\n\n" + "Trade alerts follow ↓" + ) + await msg.edit_text(summary, parse_mode=ParseMode.HTML) + + # One card per actionable signal + for r in actionable: + alert = _build_alert(r, tf) + await self._send_html(update, alert) + + except Exception as e: + logger.error(f"Scan error: {e}", exc_info=True) + await msg.edit_text(f"❌ Scan failed: {_h(str(e))}") + + # ── Application builder ─────────────────────────────────────────── def build_application(self) -> Application: token = os.environ.get("TELEGRAM_BOT_TOKEN") @@ -402,15 +421,16 @@ class ForexTelegramBot: raise ValueError( "TELEGRAM_BOT_TOKEN environment variable is not set.\n" "Get a token from @BotFather on Telegram, then:\n" - " export TELEGRAM_BOT_TOKEN='your_token_here'" + " Windows: set TELEGRAM_BOT_TOKEN=your_token\n" + " Linux/Mac: export TELEGRAM_BOT_TOKEN=your_token" ) async def _register_commands(app: Application) -> None: await app.bot.set_my_commands([ - BotCommand("scan", "Scan all major pairs"), - BotCommand("pair", "Detailed pair analysis — /pair EURUSD"), - BotCommand("tf", "Set default timeframe — /tf H4"), - BotCommand("help", "Commands and timeframe guide"), + BotCommand("signal", "Trade alert for one pair — /signal EURUSD"), + BotCommand("scan", "Scan all pairs, Grade B+ alerts — /scan H4"), + BotCommand("tf", "Set default timeframe — /tf H4"), + BotCommand("help", "Commands and grade guide"), ]) app = ( @@ -420,16 +440,16 @@ class ForexTelegramBot: .build() ) - app.add_handler(CommandHandler("start", self.cmd_start)) - app.add_handler(CommandHandler("help", self.cmd_help)) - app.add_handler(CommandHandler("tf", self.cmd_tf)) - app.add_handler(CommandHandler("scan", self.cmd_scan)) - app.add_handler(CommandHandler("pair", self.cmd_pair)) + app.add_handler(CommandHandler("start", self.cmd_start)) + app.add_handler(CommandHandler("help", self.cmd_help)) + app.add_handler(CommandHandler("tf", self.cmd_tf)) + app.add_handler(CommandHandler("signal", self.cmd_signal)) + app.add_handler(CommandHandler("scan", self.cmd_scan)) return app -# ══════════════════════════════════════════════════════════════════ +# ══════════════════════════════════════════════════════════════════════ def main(): logging.basicConfig(