#!/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()