#!/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, Timeframe, EURUSD, USDJPY, PAIR_INFO 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_BUTTONS = [ {"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR/USD 15M"}, {"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR/USD 1H"}, {"cb": "scan_usdjpy_m15", "symbol": USDJPY, "tf": Timeframe.M15, "label": "USD/JPY 15M"}, {"cb": "scan_usdjpy_h1", "symbol": USDJPY, "tf": Timeframe.H1, "label": "USD/JPY 1H"}, ] SCAN_LOOKUP = {b["cb"]: b for b in SCAN_BUTTONS} _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) _ARROW = { SignalStrength.STRONG_BUY: "⬆️", SignalStrength.BUY: "↗️", SignalStrength.NEUTRAL: "➡️", SignalStrength.SELL: "↘️", SignalStrength.STRONG_SELL: "⬇️", } def _indicator_line(name: str, ind) -> str: arrow = _ARROW.get(ind.signal, "➡️") short = ind.description.split(" | ")[0] if " | " in ind.description else ind.description return f"{arrow} *{ind.name}*: {short}" 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"🔍 *{report.display_name} Signal Analysis* ({report.primary_analysis.timeframe if report.primary_analysis else ''})", f"🕐 {now}", "", f"💰 Price: *{report.current_price:.5f}*" if report.current_price < 10 else 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}"] # Indicator breakdown with arrows 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)) # 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(f"🔍 {b['label']}", callback_data=b["cb"]) for b in SCAN_BUTTONS[:2]], [InlineKeyboardButton(f"🔍 {b['label']}", callback_data=b["cb"]) for b in SCAN_BUTTONS[2:]], ]) # ── Async signal runner (offloaded to thread pool) ──────────────────── TF_CONFIRMATIONS = { Timeframe.M15: [Timeframe.M5, Timeframe.H1], Timeframe.H1: [Timeframe.M15, Timeframe.H4], } def _run_signal_sync(symbol: str, timeframe: Timeframe) -> SignalReport: config = PlatformConfig() config.symbol = symbol config.primary_timeframe = timeframe config.confirmation_timeframes = TF_CONFIRMATIONS.get( timeframe, [Timeframe.M15, Timeframe.H4] ) aggregator = SignalAggregator(config) return aggregator.analyze_pair(symbol=symbol) async def run_signal(symbol: str, timeframe: Timeframe) -> SignalReport: loop = asyncio.get_event_loop() return await loop.run_in_executor(_executor, _run_signal_sync, symbol, timeframe) # ── Handlers ────────────────────────────────────────────────────────── async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: text = ( "👋 *ForexBot2 — Signal Intelligence*\n\n" "Pick a pair and timeframe to scan.\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() btn = SCAN_LOOKUP.get(query.data) if not btn: return pair_display = btn["label"] await query.edit_message_text( f"⏳ Scanning {pair_display}…", parse_mode=ParseMode.MARKDOWN, ) try: report = await run_signal(btn["symbol"], btn["tf"]) 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="^scan_")) logger.info("Bot running — press Ctrl+C to stop") app.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main()