a12084537d
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.
244 lines
7.8 KiB
Python
244 lines
7.8 KiB
Python
#!/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()
|