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. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,3 +4,4 @@ requests>=2.28
|
||||
pyarrow>=14.0
|
||||
tqdm>=4.66
|
||||
twelvedata>=1.2.0
|
||||
python-telegram-bot>=20.0
|
||||
|
||||
+243
@@ -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()
|
||||
Reference in New Issue
Block a user