From a596e7cab23f3e3b5b163f415a792f009ce0e2fd Mon Sep 17 00:00:00 2001 From: Harwinder Date: Thu, 7 May 2026 23:05:01 -0400 Subject: [PATCH] =?UTF-8?q?Add=20async=20Telegram=20bot=20=E2=80=94=20live?= =?UTF-8?q?=20scan=20button=20with=20trade=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces telegram_bot.py: python-telegram-bot v20+ async bot that presents a persistent Scan Signal inline button. Each press fetches a live USD/JPY signal via SignalAggregator and formats it with emoji, MTF breakdown, top bull/bear reasons, and warnings. STRONG_BUY / STRONG_SELL signals additionally render the full trade setup (entry, SL, TP1, TP2, RR). Adds python-telegram-bot>=20.0 to requirements.txt. Co-Authored-By: Claude Sonnet 4.6 --- requirements.txt | 1 + telegram_bot.py | 243 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 telegram_bot.py diff --git a/requirements.txt b/requirements.txt index b86e51e..1326ea0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ requests>=2.28 pyarrow>=14.0 tqdm>=4.66 twelvedata>=1.2.0 +python-telegram-bot>=20.0 diff --git a/telegram_bot.py b/telegram_bot.py new file mode 100644 index 0000000..b37bce4 --- /dev/null +++ b/telegram_bot.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +ForexBot2 — Telegram Bot +======================== +Async Telegram interface for USD/JPY signal intelligence. +Presents a persistent Scan button; on press runs a live signal analysis +and formats the result. Trade setup (entry / SL / TP) is shown when the +engine produces a STRONG_BUY or STRONG_SELL signal. + +Usage: + python telegram_bot.py + TELEGRAM_TOKEN= python telegram_bot.py +""" + +import asyncio +import logging +import os +import sys +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timezone + +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.constants import ParseMode +from telegram.ext import ( + Application, + CallbackQueryHandler, + CommandHandler, + ContextTypes, +) + +from config import PlatformConfig, SignalStrength +from signal_aggregator import SignalAggregator, SignalReport + +# ── Force UTF-8 on Windows ──────────────────────────────────────────── +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + +logging.basicConfig( + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + level=logging.INFO, + datefmt="%H:%M:%S", +) +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("httpcore").setLevel(logging.WARNING) +logger = logging.getLogger("telegram_bot") + +TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9-qfRTfNE") + +SCAN_CB = "scan_signal" + +_executor = ThreadPoolExecutor(max_workers=2) + + +# ── Signal formatting ───────────────────────────────────────────────── + +_SIGNAL_EMOJI = { + SignalStrength.STRONG_BUY: "🟢", + SignalStrength.BUY: "🟩", + SignalStrength.NEUTRAL: "⬜", + SignalStrength.SELL: "🟥", + SignalStrength.STRONG_SELL: "🔴", +} + +_QUALITY_EMOJI = { + "A+": "🏆", "A": "🥇", "B": "🥈", "C": "🥉", "D": "⚠️", "F": "❌", +} + + +def _signal_label(sig: SignalStrength) -> str: + return sig.name.replace("_", " ") + + +def _mtf_line(report: SignalReport) -> str: + if not report.mtf_signals: + return "" + parts = [] + for tf, sig in report.mtf_signals.items(): + score = report.mtf_scores.get(tf, 0) + parts.append(f"{tf}: {_signal_label(sig)} {score:+.1f}") + return " | ".join(parts) + + +def _compact_reasons(reasons: list[str], n: int = 3) -> list[str]: + out = [] + for r in reasons[:n]: + s = r.replace("STRONG: ", "") + if " | " in s: + s = s.split(" | ", 1)[0] + out.append(s.strip()) + return out + + +def format_signal_message(report: SignalReport) -> str: + sig = report.final_signal + 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] = [] + + # Header + now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") + lines += [ + f"🔍 *USD/JPY Signal Analysis*", + f"🕐 {now}", + "", + f"💰 Price: *{report.current_price:.3f}*", + f"📊 Regime: {report.market_regime.replace('_', ' ').title()} | Vol: {report.volatility_regime}", + "", + "━━━━━━━━━━━━━━━━━━━━", + f"{emoji} *{label}* {qe}{report.signal_quality} | Score: {report.final_score:+.1f} | {report.final_confidence:.0f}% conf", + f"_{report.action_recommendation}_", + f"{mtf_tag}", + ] + + # MTF breakdown + mtf = _mtf_line(report) + if mtf: + lines += ["", f"📈 *MTF*: {mtf}"] + + # Reasons + bulls = _compact_reasons(report.bullish_reasons) + bears = _compact_reasons(report.bearish_reasons) + if bulls or bears: + lines.append("") + for r in bulls: + lines.append(f"✅ {r}") + for r in bears: + lines.append(f"❌ {r}") + + # Warnings + if report.warnings: + lines.append("") + for w in report.warnings: + lines.append(f"⚠️ {w}") + + # Trade setup — only for strong signals + if report.trade_setup and sig in (SignalStrength.STRONG_BUY, SignalStrength.STRONG_SELL): + ts = report.trade_setup + dir_emoji = "📈" if ts.direction == "BUY" else "📉" + lines += [ + "", + "━━━━━━━━━━━━━━━━━━━━", + f"🚀 *TRADE SETUP*", + f"{dir_emoji} Direction: *{ts.direction}*", + f"▶ Entry: `{ts.entry_price:.3f}`", + f"🛑 SL: `{ts.stop_loss:.3f}` ({ts.risk_pips:.1f} pips)", + f"🎯 TP1: `{ts.take_profit_1:.3f}` ({ts.reward_pips:.1f} pips)", + ] + if ts.take_profit_2: + lines.append(f"🎯 TP2: `{ts.take_profit_2:.3f}`") + lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}") + + lines += ["", "━━━━━━━━━━━━━━━━━━━━", + "_Educational / research use only._"] + + return "\n".join(lines) + + +# ── Keyboard helper ─────────────────────────────────────────────────── + +def scan_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup([[ + InlineKeyboardButton("🔍 Scan Signal", callback_data=SCAN_CB) + ]]) + + +# ── Async signal runner (offloaded to thread pool) ──────────────────── + +def _run_signal_sync() -> SignalReport: + config = PlatformConfig() + aggregator = SignalAggregator(config) + return aggregator.analyze_pair() + + +async def run_signal(loop: asyncio.AbstractEventLoop) -> SignalReport: + return await loop.run_in_executor(_executor, _run_signal_sync) + + +# ── Handlers ────────────────────────────────────────────────────────── + +async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + text = ( + "👋 *ForexBot2 — USD/JPY Signal Intelligence*\n\n" + "Press the button below to fetch a live signal.\n" + "Strong signals include a full trade setup with entry, SL and TP." + ) + await update.message.reply_text( + text, + parse_mode=ParseMode.MARKDOWN, + reply_markup=scan_keyboard(), + ) + + +async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + await query.answer() + + # Replace button with spinner while scanning + await query.edit_message_text( + "⏳ Scanning USD/JPY…", + parse_mode=ParseMode.MARKDOWN, + ) + + loop = asyncio.get_event_loop() + try: + report = await run_signal(loop) + text = format_signal_message(report) + except Exception as exc: + logger.exception("Signal scan failed") + text = f"❌ Scan failed: {exc}" + + await query.edit_message_text( + text, + parse_mode=ParseMode.MARKDOWN, + reply_markup=scan_keyboard(), + ) + + +# ── Entry point ─────────────────────────────────────────────────────── + +def main() -> None: + if not TOKEN: + sys.exit("TELEGRAM_TOKEN not set") + + logger.info("Starting ForexBot2 Telegram bot…") + + app = ( + Application.builder() + .token(TOKEN) + .build() + ) + + app.add_handler(CommandHandler("start", cmd_start)) + app.add_handler(CallbackQueryHandler(cb_scan, pattern=f"^{SCAN_CB}$")) + + logger.info("Bot running — press Ctrl+C to stop") + app.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main()