Files
ForexBot2/telegram_bot.py
T
Harwinder 0e95a34d82 Align with Twelve Data API docs and add Telegram command menu
data_provider:
- Switch to Authorization header (recommended over apikey query param)
- Enforce 800 credits/day daily limit with warning at 80%
- Fall back to synthetic data when daily limit is reached
- Fix null OHLC field handling (API can return null for any field)

telegram_bot:
- Register commands with set_my_commands so Telegram shows / menu
- Add /credits command to show daily API credit usage and limits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:33:32 -04:00

629 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Forex Signal Intelligence Platform - Telegram Bot
===================================================
Exposes all platform functionality through a Telegram bot interface.
Usage:
export TELEGRAM_BOT_TOKEN="your_bot_token_here"
python telegram_bot.py
Commands:
/scan [TF] — Scan all major pairs
/signals [TF] — Only actionable signals (grade B+)
/pair EURUSD [TF] — Detailed analysis of one pair
/all [TF] — Scan major + cross pairs
/tf H1 — Set default timeframe
/watch [minutes] — Enable periodic scan alerts
/unwatch — Disable alerts
/status — Current settings
/help — Full command reference
"""
import asyncio
import logging
import os
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import Dict, Optional
from telegram import BotCommand, Update
from telegram.constants import ParseMode
from telegram.ext import Application, CallbackContext, CommandHandler, ContextTypes
from config import (
CROSS_PAIRS, MAJOR_PAIRS, SYMBOL_DISPLAY,
PlatformConfig, SignalStrength, Timeframe,
)
from signal_aggregator import SignalAggregator, SignalReport
logger = logging.getLogger(__name__)
# ── Constants ──────────────────────────────────────────────────────
TF_MAP = {
"M5": Timeframe.M5,
"M15": Timeframe.M15,
"M30": Timeframe.M30,
"H1": Timeframe.H1,
"H4": Timeframe.H4,
"D1": Timeframe.D1,
}
TF_CONFIRMATIONS = {
"M5": [Timeframe.M1, Timeframe.M15],
"M15": [Timeframe.M5, Timeframe.H1],
"M30": [Timeframe.M15, Timeframe.H1],
"H1": [Timeframe.M15, Timeframe.H4],
"H4": [Timeframe.H1, Timeframe.D1],
"D1": [Timeframe.H4, Timeframe.W1],
}
SIGNAL_EMOJI = {
SignalStrength.STRONG_BUY: "🟢⬆⬆",
SignalStrength.BUY: "🟢⬆",
SignalStrength.NEUTRAL: "⬜↔",
SignalStrength.SELL: "🔴⬇",
SignalStrength.STRONG_SELL: "🔴⬇⬇",
}
SIGNAL_NAME = {
SignalStrength.STRONG_BUY: "STRONG BUY",
SignalStrength.BUY: "BUY",
SignalStrength.NEUTRAL: "NEUTRAL",
SignalStrength.SELL: "SELL",
SignalStrength.STRONG_SELL: "STRONG SELL",
}
GRADE_EMOJI = {
"A+": "⭐⭐",
"A": "⭐",
"B": "🔵",
"C": "🟡",
"D": "🟠",
"F": "⚫",
}
WATCH_MIN_INTERVAL = 5 # minimum watch interval in minutes
WATCH_MAX_INTERVAL = 120 # maximum watch interval in minutes
WATCH_DEFAULT = 5
# ── Per-chat state ─────────────────────────────────────────────────
class ChatState:
def __init__(self):
self.timeframe: str = "H1"
self.pairs: list = list(MAJOR_PAIRS)
self.watch_enabled: bool = False
# ── HTML helpers ───────────────────────────────────────────────────
def _esc(text: str) -> str:
"""Escape HTML special characters for Telegram HTML parse mode."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def _split_messages(lines: list[str], limit: int = 4000) -> list[str]:
"""Join lines into messages that stay under Telegram's 4096-char limit."""
messages, current, current_len = [], [], 0
for line in lines:
line_len = len(line) + 1 # +1 for newline
if current and current_len + line_len > limit:
messages.append("\n".join(current))
current, current_len = [], 0
current.append(line)
current_len += line_len
if current:
messages.append("\n".join(current))
return messages
# ── Message formatters ─────────────────────────────────────────────
def _format_scan(reports: list[SignalReport], timeframe: str, title: str = "FOREX SIGNAL SCAN") -> str:
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines = [
f"<b>📊 {title} — {timeframe}</b>",
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>",
f"<code>🕐 {now}</code>\n",
]
for r in reports:
sig_em = SIGNAL_EMOJI.get(r.final_signal, "❔")
grade_em = GRADE_EMOJI.get(r.signal_quality, "")
sig_name = SIGNAL_NAME.get(r.final_signal, "?")
event_flag = " ⚠️" if r.event_risk == "danger" else (" ⚡" if r.event_risk == "caution" else "")
lines.append(
f"{sig_em} <b>{_esc(r.display_name)}</b> "
f"{grade_em}<code>{r.signal_quality}</code> "
f"<code>{_esc(sig_name):<11}</code> "
f"<code>{r.final_confidence:.0f}%</code>{event_flag}"
)
lines += ["", "<i>Use /pair EURUSD for detailed analysis</i>"]
return "\n".join(lines)
def _format_pair_detail(r: SignalReport) -> list[str]:
"""
Return a list of message strings for a single pair.
Split across messages so each stays under Telegram's 4096-char limit.
"""
sig_em = SIGNAL_EMOJI.get(r.final_signal, "❔")
grade_em = GRADE_EMOJI.get(r.signal_quality, "")
sig_name = SIGNAL_NAME.get(r.final_signal, "?")
# ── Message 1: Header, MTF, Trade Setup, Key Levels ───────────
m1 = [
f"{sig_em} <b>{_esc(r.display_name)}</b> — <b>{_esc(sig_name)}</b>",
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>",
f"<b>Price:</b> <code>{r.current_price:.5f}</code>",
f"<b>Regime:</b> {_esc(r.market_regime)} <b>Vol:</b> {_esc(r.volatility_regime)}",
"",
f"<b>Score:</b> <code>{r.final_score:+.1f}</code> "
f"<b>Grade:</b> {grade_em}<code>{r.signal_quality}</code> "
f"<b>Conf:</b> <code>{r.final_confidence:.0f}%</code>",
f"<b>Action:</b> {_esc(r.action_recommendation)}",
]
if r.event_risk != "clear":
icon = "⚠️" if r.event_risk == "danger" else "⚡"
m1.append(f"{icon} <b>Event:</b> {_esc(r.event_description)}")
if r.news_description and r.news_description != "Neutral news sentiment":
m1.append(f"📰 <b>News:</b> {_esc(r.news_description)}")
# Multi-timeframe
if r.mtf_signals:
agree = "✅ ALIGNED" if r.mtf_agreement else "⚡ DIVERGENT"
m1 += ["", f"<b>📊 Multi-Timeframe:</b> {agree}"]
for tf, sig in r.mtf_signals.items():
score = r.mtf_scores.get(tf, 0)
m1.append(
f" <code>{tf:>5}</code> {SIGNAL_EMOJI.get(sig, '?')} "
f"<code>{score:+.1f}</code>"
)
# Trade setup
if r.trade_setup:
ts = r.trade_setup
dir_em = "🟢" if ts.direction == "BUY" else "🔴"
m1 += [
"",
"<b>💼 Trade Setup</b>",
f" <b>Direction:</b> {dir_em} {ts.direction}",
f" <b>Entry:</b> <code>{ts.entry_price:.5f}</code>",
f" <b>Stop Loss:</b> <code>{ts.stop_loss:.5f}</code> ({ts.risk_pips:.1f} pips)",
f" <b>Take Profit 1:</b> <code>{ts.take_profit_1:.5f}</code> ({ts.reward_pips:.1f} pips)",
f" <b>Take Profit 2:</b> <code>{ts.take_profit_2:.5f}</code>",
f" <b>Risk:Reward:</b> 1:{ts.risk_reward:.1f}",
]
# Key levels
if r.primary_analysis and r.primary_analysis.key_levels:
lvls = r.primary_analysis.key_levels
shown = [(k, lvls[k]) for k in ("r3", "r2", "r1", "pivot", "s1", "s2", "s3") if k in lvls]
if shown:
m1 += ["", "<b>📍 Key Levels:</b>"]
for k, v in shown:
arrow = "🔴" if v > r.current_price else "🟢"
m1.append(f" {arrow} <code>{k.upper():>6}:</code> <code>{v:.5f}</code>")
# ── Message 2: Indicators + Reasons ───────────────────────────
m2 = []
if r.primary_analysis:
ind_lines = []
for name, ind in r.primary_analysis.indicators.items():
if name.startswith("_") or ind.weight == 0:
continue
em = SIGNAL_EMOJI.get(ind.signal, "↔")
# Trim long descriptions to keep message readable
desc = _esc(ind.description[:90])
ind_lines.append(
f" {em} <b>{_esc(ind.name)}</b> <code>{ind.confidence:.0f}%</code>"
f"\n <i>{desc}</i>"
)
if ind_lines:
m2 += ["<b>📉 Indicators:</b>"] + ind_lines
if r.bullish_reasons:
if m2:
m2.append("")
m2.append("<b>🟢 Bullish Factors:</b>")
for reason in r.bullish_reasons[:5]:
m2.append(f" ▲ {_esc(reason)}")
if r.bearish_reasons:
if m2:
m2.append("")
m2.append("<b>🔴 Bearish Factors:</b>")
for reason in r.bearish_reasons[:5]:
m2.append(f" ▼ {_esc(reason)}")
if r.warnings:
if m2:
m2.append("")
m2.append("<b>⚠️ Warnings:</b>")
for w in r.warnings:
m2.append(f" ! {_esc(w)}")
result = _split_messages(m1)
if m2:
result += _split_messages(m2)
return result
# ══════════════════════════════════════════════════════════════════
# BOT CLASS
# ══════════════════════════════════════════════════════════════════
class ForexTelegramBot:
def __init__(self):
self._chat_states: Dict[int, ChatState] = {}
self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="forex")
def _state(self, chat_id: int) -> ChatState:
if chat_id not in self._chat_states:
self._chat_states[chat_id] = ChatState()
return self._chat_states[chat_id]
def _build_config(self, state: ChatState) -> PlatformConfig:
config = PlatformConfig()
config.primary_timeframe = TF_MAP[state.timeframe]
config.confirmation_timeframes = TF_CONFIRMATIONS.get(
state.timeframe, [Timeframe.M15, Timeframe.H4]
)
config.watch_pairs = list(state.pairs)
return config
# ── Blocking analysis (run in thread pool) ─────────────────────
def _run_scan(self, state: ChatState) -> list[SignalReport]:
config = self._build_config(state)
return SignalAggregator(config).analyze_all_pairs()
def _run_pair(self, symbol: str, state: ChatState) -> Optional[SignalReport]:
config = self._build_config(state)
config.watch_pairs = [symbol]
reports = SignalAggregator(config).analyze_all_pairs()
return reports[0] if reports else None
async def _analyze(self, func, *args):
"""Run a blocking analysis function in the thread pool."""
loop = asyncio.get_running_loop()
return await loop.run_in_executor(self._executor, func, *args)
# ── Parse helpers ──────────────────────────────────────────────
@staticmethod
def _parse_tf(args: list[str]) -> Optional[str]:
"""Return the first arg that is a valid timeframe key, or None."""
for a in args:
if a.upper() in TF_MAP:
return a.upper()
return None
# ── Commands ───────────────────────────────────────────────────
async def cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
text = (
"<b>🌐 Forex Signal Intelligence Platform</b>\n"
"<code>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</code>\n\n"
"Multi-layer technical + fundamental signal analysis across major Forex pairs.\n\n"
"<b>Commands:</b>\n"
" /scan — Scan all major pairs\n"
" /signals — Actionable signals only (grade B+)\n"
" /pair EURUSD — Detailed single-pair analysis\n"
" /all — Include cross pairs in scan\n"
" /tf H1 — Set default timeframe\n"
" /watch — Periodic scan alerts (every 5 min)\n"
" /unwatch — Disable alerts\n"
" /status — Current settings\n"
" /help — Full command reference\n\n"
"<i>⚠️ Educational/research use only. Not financial advice.</i>"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
async def cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
text = (
"<b>📖 Command Reference</b>\n\n"
"<b>/scan</b> — Scan all major pairs (uses default TF)\n"
"<b>/scan H4</b> — Scan with a specific timeframe\n\n"
"<b>/signals</b> — Grade B+ signals only\n"
"<b>/signals H4</b> — With a specific timeframe\n\n"
"<b>/pair EURUSD</b> — Detailed analysis (uses default TF)\n"
"<b>/pair EURUSD H4</b> — With a specific timeframe\n\n"
"<b>/all</b> — Scan major + cross pairs\n"
"<b>/all H4</b> — With a specific timeframe\n\n"
"<b>/tf H1</b> — Set your default timeframe\n"
" Options: <code>M5 M15 M30 H1 H4 D1</code>\n\n"
"<b>/watch</b> — Alert every 5 min (actionable signals only)\n"
"<b>/watch 15</b> — Alert every 15 min (5–120 min range)\n"
"<b>/unwatch</b> — Stop alerts\n\n"
"<b>/status</b> — View current settings\n\n"
"<b>Signal key:</b>\n"
" 🟢⬆⬆ Strong Buy 🟢⬆ Buy ⬜↔ Neutral\n"
" 🔴⬇ Sell 🔴⬇⬇ Strong Sell\n\n"
"<b>Grade key:</b>\n"
" A+ ⭐⭐ A ⭐ B 🔵 C 🟡 D 🟠 F ⚫"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
async def cmd_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
state = self._state(update.effective_chat.id)
pairs_display = ", ".join(SYMBOL_DISPLAY.get(p, p) for p in state.pairs)
watch_str = "✅ enabled" if state.watch_enabled else "❌ disabled"
text = (
"<b>⚙️ Current Settings</b>\n\n"
f"<b>Timeframe:</b> {state.timeframe}\n"
f"<b>Pairs:</b> {_esc(pairs_display)}\n"
f"<b>Watch alerts:</b> {watch_str}"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
async def cmd_tf(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
tf = self._parse_tf(context.args or [])
if not tf:
opts = " ".join(TF_MAP.keys())
await update.message.reply_text(
f"Usage: <b>/tf &lt;timeframe&gt;</b>\nOptions: <code>{opts}</code>",
parse_mode=ParseMode.HTML,
)
return
self._state(update.effective_chat.id).timeframe = tf
await update.message.reply_text(
f"✅ Default timeframe set to <b>{tf}</b>", parse_mode=ParseMode.HTML
)
async def cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
state = self._state(chat_id)
# Allow inline TF override: /scan H4
scan_state = ChatState()
scan_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
scan_state.pairs = list(state.pairs)
msg = await update.message.reply_text("🔄 Scanning markets, please wait…")
try:
reports = await self._analyze(self._run_scan, scan_state)
text = _format_scan(reports, scan_state.timeframe)
await msg.edit_text(text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Scan error: {e}")
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
async def cmd_signals(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
state = self._state(chat_id)
scan_state = ChatState()
scan_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
scan_state.pairs = list(state.pairs)
msg = await update.message.reply_text("🔄 Scanning for signals…")
try:
reports = await self._analyze(self._run_scan, scan_state)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
if actionable:
text = _format_scan(actionable, scan_state.timeframe, title="🎯 ACTIONABLE SIGNALS")
else:
text = (
f"<b>🎯 ACTIONABLE SIGNALS — {scan_state.timeframe}</b>\n\n"
"No grade B+ signals right now.\n"
"Use /scan to see all pairs."
)
await msg.edit_text(text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Signals error: {e}")
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
async def cmd_pair(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
state = self._state(chat_id)
args = context.args or []
if not args:
await update.message.reply_text(
"Usage: <b>/pair &lt;symbol&gt; [timeframe]</b>\n"
"Example: <code>/pair EURUSD H4</code>",
parse_mode=ParseMode.HTML,
)
return
raw = args[0].upper().replace("/", "").replace("_", "")
if not raw.endswith("=X"):
raw += "=X"
pair_state = ChatState()
pair_state.timeframe = self._parse_tf(args[1:]) or state.timeframe
display = SYMBOL_DISPLAY.get(raw, raw)
msg = await update.message.reply_text(f"🔄 Analyzing {_esc(display)}…")
try:
report = await self._analyze(self._run_pair, raw, pair_state)
if report is None:
await msg.edit_text(
f"❌ No data returned for <code>{_esc(raw)}</code>.",
parse_mode=ParseMode.HTML,
)
return
messages = _format_pair_detail(report)
await msg.edit_text(messages[0], parse_mode=ParseMode.HTML)
for m in messages[1:]:
await update.message.reply_text(m, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Pair analysis error for {raw}: {e}")
await msg.edit_text(f"❌ Analysis failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
async def cmd_all(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
state = self._state(chat_id)
all_state = ChatState()
all_state.timeframe = self._parse_tf(context.args or []) or state.timeframe
all_state.pairs = list(MAJOR_PAIRS) + list(CROSS_PAIRS)
msg = await update.message.reply_text("🔄 Scanning all pairs (major + cross)…")
try:
reports = await self._analyze(self._run_scan, all_state)
text = _format_scan(reports, all_state.timeframe, title="FULL PAIR SCAN")
await msg.edit_text(text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"All-pairs scan error: {e}")
await msg.edit_text(f"❌ Scan failed: {_esc(str(e))}", parse_mode=ParseMode.HTML)
async def cmd_watch(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
state = self._state(chat_id)
interval_minutes = WATCH_DEFAULT
if context.args:
try:
interval_minutes = int(context.args[0])
interval_minutes = max(WATCH_MIN_INTERVAL, min(WATCH_MAX_INTERVAL, interval_minutes))
except ValueError:
pass
# Remove any existing watch job for this chat
for job in context.job_queue.get_jobs_by_name(f"watch_{chat_id}"):
job.schedule_removal()
state.watch_enabled = True
context.job_queue.run_repeating(
self._watch_job,
interval=interval_minutes * 60,
first=5,
chat_id=chat_id,
name=f"watch_{chat_id}",
)
await update.message.reply_text(
f"✅ Watch alerts enabled — scanning every <b>{interval_minutes} min</b>.\n"
"Alerts fire when grade B+ signals appear. Use /unwatch to stop.",
parse_mode=ParseMode.HTML,
)
async def cmd_unwatch(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
self._state(chat_id).watch_enabled = False
for job in context.job_queue.get_jobs_by_name(f"watch_{chat_id}"):
job.schedule_removal()
await update.message.reply_text("❌ Watch alerts disabled.")
async def cmd_credits(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show API credit usage against the free-tier daily limit."""
from data_provider import ForexDataProvider
config = self._build_config(self._state(update.effective_chat.id))
provider = ForexDataProvider(config)
used = provider._daily_credits
limit = provider.DAILY_LIMIT
remaining = limit - used
pct = (used / limit) * 100 if limit else 0
bar_filled = int(pct / 5)
bar = "█" * bar_filled + "░" * (20 - bar_filled)
status = "🟢" if pct < 60 else ("🟡" if pct < 80 else "🔴")
text = (
"<b>📊 API Credit Usage</b>\n\n"
f"<code>{bar}</code> {status}\n"
f"<b>Used:</b> {used} / {limit} credits\n"
f"<b>Remaining:</b> {remaining} credits\n\n"
"<i>Free tier: 8 credits/min, 800 credits/day\n"
"Each full scan uses ~21 credits (7 pairs × 3 timeframes)\n"
"≈ 38 scans available per day</i>"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML)
# ── Watch job ──────────────────────────────────────────────────
async def _watch_job(self, context: CallbackContext):
chat_id = context.job.chat_id
state = self._state(chat_id)
if not state.watch_enabled:
return
try:
reports = await self._analyze(self._run_scan, state)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
if actionable:
text = (
"🔔 <b>SIGNAL ALERT</b>\n\n"
+ _format_scan(actionable, state.timeframe, title="ACTIONABLE SIGNALS")
)
else:
now = datetime.now(timezone.utc).strftime("%H:%M UTC")
text = f"📊 <b>Watch scan {now}</b> — No grade B+ signals detected."
await context.bot.send_message(chat_id, text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Watch job error for chat {chat_id}: {e}")
# ── Application builder ────────────────────────────────────────
def build_application(self) -> Application:
token = os.environ.get("TELEGRAM_BOT_TOKEN")
if not token:
raise ValueError(
"TELEGRAM_BOT_TOKEN environment variable is not set.\n"
"Get a token from @BotFather on Telegram, then:\n"
" export TELEGRAM_BOT_TOKEN='your_token_here'"
)
async def _register_commands(app: Application) -> None:
"""Register commands with Telegram so they appear in the / menu."""
await app.bot.set_my_commands([
BotCommand("scan", "Scan all major pairs"),
BotCommand("signals", "Actionable signals only (grade B+)"),
BotCommand("pair", "Detailed pair analysis — /pair EURUSD"),
BotCommand("all", "Scan major + cross pairs"),
BotCommand("tf", "Set timeframe — /tf H4"),
BotCommand("watch", "Enable periodic alerts"),
BotCommand("unwatch", "Disable alerts"),
BotCommand("credits", "View API credit usage"),
BotCommand("status", "View current settings"),
BotCommand("help", "Full command reference"),
])
app = (
Application.builder()
.token(token)
.post_init(_register_commands)
.build()
)
app.add_handler(CommandHandler("start", self.cmd_start))
app.add_handler(CommandHandler("help", self.cmd_help))
app.add_handler(CommandHandler("status", self.cmd_status))
app.add_handler(CommandHandler("tf", self.cmd_tf))
app.add_handler(CommandHandler("scan", self.cmd_scan))
app.add_handler(CommandHandler("signals", self.cmd_signals))
app.add_handler(CommandHandler("pair", self.cmd_pair))
app.add_handler(CommandHandler("all", self.cmd_all))
app.add_handler(CommandHandler("watch", self.cmd_watch))
app.add_handler(CommandHandler("unwatch", self.cmd_unwatch))
app.add_handler(CommandHandler("credits", self.cmd_credits))
return app
# ══════════════════════════════════════════════════════════════════
def main():
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
bot = ForexTelegramBot()
app = bot.build_application()
print("🤖 Forex Signal Bot started. Press Ctrl+C to stop.")
app.run_polling(drop_pending_updates=True)
if __name__ == "__main__":
main()