Add async Telegram bot — live scan button with trade setup

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.
This commit is contained in:
Harwinder
2026-05-07 23:05:01 -04:00
parent a55aca21b1
commit a12084537d
2 changed files with 244 additions and 0 deletions
+1
View File
@@ -4,3 +4,4 @@ requests>=2.28
pyarrow>=14.0
tqdm>=4.66
twelvedata>=1.2.0
python-telegram-bot>=20.0
+243
View File
@@ -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=<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()