1503 lines
70 KiB
Python
1503 lines
70 KiB
Python
"""
|
||
Interactive Telegram Bot — 2-Way Command & Control Center
|
||
|
||
Features:
|
||
1. Real-time Outbound Alerts:
|
||
- Trade entry alerts with inline action buttons (Close Position, Move SL to Breakeven)
|
||
- Trade exit alerts with P&L and duration
|
||
- High-impact news alerts with Gemini sentiment score
|
||
- Daily performance summaries and risk alerts
|
||
2. Interactive 2-Way Remote Control:
|
||
- Inline Keyboard Menu (one-tap dashboard control)
|
||
- /start & /menu — Interactive Command Center Dashboard
|
||
- /status — Live balance, equity, margin, running state, current gold spread
|
||
- /positions — Real-time open trades with floating P&L and per-trade close buttons
|
||
- /trades — Recent closed trade history and win rate
|
||
- /pnl — Today's net P&L and performance statistics
|
||
- /news — Live financial news with AI sentiment breakdown
|
||
- /regime — Current market regime (trend vs ranging) and active strategies
|
||
- /price — Real-time XAUUSD Bid, Ask, and Spread ticker
|
||
- /pause & /resume — Remotely toggle automated trading
|
||
- /closeall — Emergency remote kill-switch to flatten all positions
|
||
- /setlot <size> — Adjust default lot size on the fly
|
||
- /setrisk <pct> — Adjust risk % per trade on the fly
|
||
- /help — Command cheatsheet
|
||
3. Security:
|
||
- Whitelist authorization guard (rejects unauthorized users)
|
||
"""
|
||
|
||
import asyncio
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any, Dict, List, Optional
|
||
from loguru import logger
|
||
|
||
from telegram import (
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
Update,
|
||
)
|
||
from telegram.ext import (
|
||
Application,
|
||
ApplicationBuilder,
|
||
CallbackQueryHandler,
|
||
CommandHandler,
|
||
ContextTypes,
|
||
)
|
||
|
||
from config.settings import get_settings
|
||
from database import crud
|
||
|
||
|
||
class TelegramNotifier:
|
||
"""
|
||
Unified 2-way Telegram bot for automated trading alerts and interactive remote control.
|
||
"""
|
||
|
||
def __init__(self, bot_instance: Optional[Any] = None):
|
||
self.settings = get_settings()
|
||
self.bot_token = self.settings.telegram.bot_token
|
||
self.chat_id = str(self.settings.telegram.chat_id)
|
||
self.bot_instance = bot_instance
|
||
|
||
# Parse authorized chat IDs (supports comma-separated list of IDs)
|
||
raw_chat_id = str(self.settings.telegram.chat_id or "").strip()
|
||
self.authorized_ids: set[str] = {
|
||
cid.strip()
|
||
for cid in raw_chat_id.split(",")
|
||
if cid.strip() and cid.strip() != "0"
|
||
}
|
||
|
||
self._app: Optional[Application] = None
|
||
self._is_polling = False
|
||
self._enabled = bool(self.bot_token and self.chat_id and self.chat_id != "0")
|
||
|
||
# News alert deduplication cache
|
||
self._sent_news_alert_hashes: set[str] = set()
|
||
self._preload_alerted_news()
|
||
|
||
if not self._enabled:
|
||
logger.warning("Telegram bot not configured or chat ID missing — remote control disabled")
|
||
|
||
def _preload_alerted_news(self):
|
||
"""Preload hashes of recent news from DB so restarts don't re-alert old high-impact items."""
|
||
try:
|
||
from database.models import get_session, NewsEvent
|
||
from news.news_utils import compute_news_hash
|
||
session = get_session()
|
||
try:
|
||
cutoff = datetime.now(timezone.utc) - timedelta(hours=48)
|
||
events = session.query(NewsEvent.headline, NewsEvent.url).filter(
|
||
NewsEvent.fetched_at >= cutoff
|
||
).all()
|
||
for h_text, u_text in events:
|
||
if h_text:
|
||
self._sent_news_alert_hashes.add(compute_news_hash(h_text, u_text or ""))
|
||
finally:
|
||
session.close()
|
||
except Exception as e:
|
||
logger.debug(f"Could not preload alerted news: {e}")
|
||
|
||
def set_bot_instance(self, bot_instance: Any):
|
||
"""Link the parent TradingBot instance for live data and control."""
|
||
self.bot_instance = bot_instance
|
||
|
||
def _is_authorized(self, update: Update) -> bool:
|
||
"""
|
||
Verify message sender matches the authorized Telegram chat or user ID whitelist.
|
||
Rejects unauthorized users with security logging.
|
||
"""
|
||
if not self.authorized_ids:
|
||
return False
|
||
|
||
sender_chat_id = str(update.effective_chat.id) if update.effective_chat else ""
|
||
sender_user_id = str(update.effective_user.id) if update.effective_user else ""
|
||
username = update.effective_user.username if update.effective_user else "unknown"
|
||
|
||
if sender_chat_id in self.authorized_ids or sender_user_id in self.authorized_ids:
|
||
return True
|
||
|
||
logger.warning(
|
||
f"⛔ UNAUTHORIZED Telegram access attempt | User: @{username} "
|
||
f"(User ID: {sender_user_id}, Chat ID: {sender_chat_id})"
|
||
)
|
||
return False
|
||
|
||
|
||
async def _safe_edit_or_reply(self, update: Update, text: str, reply_markup: Any = None, parse_mode: str = "HTML"):
|
||
"""Safely edit the current callback message, handling 'Message is not modified' gracefully."""
|
||
if update.callback_query:
|
||
try:
|
||
await update.callback_query.edit_message_text(
|
||
text=text,
|
||
reply_markup=reply_markup,
|
||
parse_mode=parse_mode,
|
||
)
|
||
return
|
||
except Exception as e:
|
||
err_str = str(e)
|
||
if "Message is not modified" in err_str:
|
||
try:
|
||
await update.callback_query.answer("Already up to date")
|
||
except Exception:
|
||
pass
|
||
return
|
||
logger.debug(f"Could not edit message, falling back to reply: {e}")
|
||
|
||
if update.effective_message:
|
||
try:
|
||
await update.effective_message.reply_text(
|
||
text=text,
|
||
reply_markup=reply_markup,
|
||
parse_mode=parse_mode,
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to send reply message: {e}")
|
||
|
||
# ── Interactive Keyboards ─────────────────────────────────────────────
|
||
|
||
def _get_main_keyboard(self) -> InlineKeyboardMarkup:
|
||
"""Create the primary interactive Command Center keyboard."""
|
||
paused = getattr(self.bot_instance, "_trading_paused", False)
|
||
pause_btn = (
|
||
InlineKeyboardButton("▶️ Resume Trading", callback_data="cb_resume")
|
||
if paused
|
||
else InlineKeyboardButton("⏸️ Pause Trading", callback_data="cb_pause")
|
||
)
|
||
|
||
curr_mode = self.settings.active_mode.upper()
|
||
mode_icons = {"SAFE": "🛡️", "MODERATE": "⚖️", "AGGRESSIVE": "⚡"}
|
||
icon = mode_icons.get(curr_mode, "🎛️")
|
||
mode_btn = InlineKeyboardButton(f"{icon} Profile Mode: {curr_mode} ⚙️", callback_data="cb_mode_menu")
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📊 Status & Balance", callback_data="cb_status"),
|
||
InlineKeyboardButton("📈 Open Positions", callback_data="cb_positions"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("💰 P&L Summary", callback_data="cb_pnl"),
|
||
InlineKeyboardButton("📜 Recent Trades", callback_data="cb_trades"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📰 News & AI Sentiment", callback_data="cb_news"),
|
||
InlineKeyboardButton("🧠 Market Regime", callback_data="cb_regime"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🎓 Self-Learning & Adaptation", callback_data="cb_learning"),
|
||
InlineKeyboardButton("🏷️ Gold Price & Spread", callback_data="cb_price"),
|
||
],
|
||
[
|
||
mode_btn,
|
||
],
|
||
[
|
||
pause_btn,
|
||
InlineKeyboardButton("🛑 EMERGENCY CLOSE ALL", callback_data="cb_confirm_closeall"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🔄 Refresh Dashboard", callback_data="cb_menu"),
|
||
InlineKeyboardButton("📖 Help & Commands", callback_data="cb_help"),
|
||
],
|
||
]
|
||
return InlineKeyboardMarkup(keyboard)
|
||
|
||
# ── Command & Callback Handlers ──────────────────────────────────────
|
||
|
||
async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /start or /menu command."""
|
||
if not self.authorized_ids:
|
||
sender_id = update.effective_chat.id if update.effective_chat else (update.effective_user.id if update.effective_user else "unknown")
|
||
if update.effective_message:
|
||
await update.effective_message.reply_text(
|
||
f"⚠️ <b>TELEGRAM BOT SETUP REQUIRED</b>\n\n"
|
||
f"Your Telegram ID is: <code>{sender_id}</code>\n\n"
|
||
f"To authorize this account, add this to your <code>config/.env</code> file:\n"
|
||
f"<code>TELEGRAM_CHAT_ID={sender_id}</code>\n\n"
|
||
f"Then restart the bot to activate remote control.",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
if not self._is_authorized(update):
|
||
if update.effective_message:
|
||
await update.effective_message.reply_text(
|
||
"⛔ <b>Access Denied</b>\nThis bot is private and restricted to authorized operators.",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
mode = "DEMO" if self.settings.demo_mode else "LIVE"
|
||
active_mode = self.settings.active_mode.upper()
|
||
mode_icons = {"SAFE": "🛡️", "MODERATE": "⚖️", "AGGRESSIVE": "⚡"}
|
||
mode_icon = mode_icons.get(active_mode, "🎛️")
|
||
|
||
text = (
|
||
f"⚡ <b>XAUUSD AI TRADING BOT — COMMAND CENTER</b> ⚡\n\n"
|
||
f"<b>Status:</b> 🟢 ONLINE ({mode})\n"
|
||
f"<b>Active Mode:</b> {mode_icon} <code>{active_mode}</code>\n"
|
||
f"<b>Symbol:</b> <code>{self.settings.symbol}</code>\n"
|
||
f"<b>Lot Size:</b> <code>{self.settings.risk_params.get('default_lot_size', 0.02)}</code>\n"
|
||
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n"
|
||
f"Tap an action button below to monitor or control your bot:"
|
||
)
|
||
await self._safe_edit_or_reply(
|
||
update,
|
||
text=text,
|
||
reply_markup=self._get_main_keyboard(),
|
||
parse_mode="HTML",
|
||
)
|
||
|
||
async def _handle_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /status command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
account = None
|
||
if self.bot_instance and hasattr(self.bot_instance, "mt5"):
|
||
mt5_obj = self.bot_instance.mt5
|
||
else:
|
||
from core.mt5_connector import MT5Connector
|
||
mt5_obj = MT5Connector()
|
||
|
||
is_conn = mt5_obj.is_connected() if callable(getattr(mt5_obj, "is_connected", None)) else getattr(mt5_obj, "is_connected", False)
|
||
if not is_conn and hasattr(mt5_obj, "connect"):
|
||
is_conn = mt5_obj.connect(max_retries=1, retry_delay=0.1)
|
||
if is_conn:
|
||
account = mt5_obj.get_account_info(auto_reconnect=False)
|
||
|
||
paused = getattr(self.bot_instance, "_trading_paused", False)
|
||
state_str = "⏸️ PAUSED" if paused else "🟢 RUNNING"
|
||
mode_str = "DEMO" if self.settings.demo_mode else "LIVE"
|
||
|
||
if account:
|
||
balance_str = f"${account.get('balance', 0.0):,.2f}"
|
||
equity_str = f"${account.get('equity', 0.0):,.2f}"
|
||
profit_str = f"${account.get('profit', 0.0):+,.2f}"
|
||
margin_level = account.get("margin_level", 0.0)
|
||
margin_level_str = f"{margin_level:.1f}%" if margin_level else "N/A"
|
||
free_margin_str = f"${account.get('free_margin', 0.0):,.2f}"
|
||
else:
|
||
balance_str = equity_str = profit_str = margin_level_str = free_margin_str = "Unavailable (MT5 Offline)"
|
||
|
||
daily_pnl = crud.get_daily_pnl()
|
||
open_trades = crud.get_open_trades()
|
||
|
||
msg = (
|
||
f"📊 <b>BOT & ACCOUNT STATUS</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>State:</b> {state_str} ({mode_str})\n"
|
||
f"<b>Account:</b> <code>{self.settings.mt5.login}</code> ({self.settings.mt5.server})\n\n"
|
||
f"💰 <b>Balance:</b> <code>{balance_str}</code>\n"
|
||
f"📈 <b>Equity:</b> <code>{equity_str}</code>\n"
|
||
f"💵 <b>Floating P&L:</b> <code>{profit_str}</code>\n"
|
||
f"📅 <b>Today's Realized P&L:</b> <code>${daily_pnl:+,.2f}</code>\n"
|
||
f"🛡️ <b>Margin Level:</b> <code>{margin_level_str}</code>\n"
|
||
f"🔓 <b>Free Margin:</b> <code>{free_margin_str}</code>\n\n"
|
||
f"<b>Open Positions:</b> {len(open_trades)}\n"
|
||
f"<b>Symbol:</b> <code>{self.settings.symbol}</code>"
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📈 View Open Positions", callback_data="cb_positions")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_positions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /positions command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
positions = []
|
||
pending = []
|
||
is_conn = False
|
||
if self.bot_instance and hasattr(self.bot_instance, "mt5"):
|
||
mt5_obj = self.bot_instance.mt5
|
||
else:
|
||
from core.mt5_connector import MT5Connector
|
||
mt5_obj = MT5Connector()
|
||
|
||
is_conn = mt5_obj.is_connected() if callable(getattr(mt5_obj, "is_connected", None)) else getattr(mt5_obj, "is_connected", False)
|
||
if not is_conn and hasattr(mt5_obj, "connect"):
|
||
is_conn = mt5_obj.connect(max_retries=1, retry_delay=0.1)
|
||
if is_conn:
|
||
positions = mt5_obj.get_open_positions(self.settings.symbol, auto_reconnect=False)
|
||
if hasattr(mt5_obj, "get_pending_orders"):
|
||
pending = mt5_obj.get_pending_orders(self.settings.symbol, auto_reconnect=False)
|
||
|
||
keyboard_rows = []
|
||
|
||
if not is_conn:
|
||
msg = (
|
||
f"📈 <b>OPEN POSITIONS</b>\n"
|
||
f"{'━' * 25}\n\n"
|
||
f"⚠️ <i>MT5 Terminal is currently Offline.</i>\n\n"
|
||
f"Cannot query live positions. Please ensure MetaTrader 5 is launched on your desktop with your Exness account logged in."
|
||
)
|
||
elif not positions and not pending:
|
||
msg = (
|
||
f"📈 <b>OPEN POSITIONS & ORDERS</b>\n"
|
||
f"{'━' * 25}\n\n"
|
||
f"<i>No open positions or pending orders active on {self.settings.symbol}.</i>\n"
|
||
f"The bot is scanning market regimes and waiting for high-confluence entry signals."
|
||
)
|
||
else:
|
||
msg = ""
|
||
if positions:
|
||
msg += f"📈 <b>OPEN POSITIONS ({len(positions)})</b>\n{'━' * 25}\n\n"
|
||
for pos in positions:
|
||
ticket = pos.get("ticket")
|
||
direction = pos.get("type", "BUY")
|
||
volume = pos.get("volume", 0.0)
|
||
entry = pos.get("price_open", pos.get("open_price", 0.0))
|
||
current = pos.get("price_current", pos.get("current_price", 0.0))
|
||
profit = pos.get("profit", 0.0)
|
||
p_emoji = "🟢" if profit >= 0 else "🔴"
|
||
dir_emoji = "📈" if direction == "BUY" else "📉"
|
||
|
||
msg += (
|
||
f"{dir_emoji} <b>Ticket #{ticket}</b> | {direction} {volume} lots\n"
|
||
f" • Entry: <code>${entry:,.2f}</code> → Now: <code>${current:,.2f}</code>\n"
|
||
f" • P&L: {p_emoji} <code>${profit:+,.2f}</code>\n"
|
||
f" • SL: <code>${pos.get('sl', 0.0):,.2f}</code> | TP: <code>${pos.get('tp', 0.0):,.2f}</code>\n\n"
|
||
)
|
||
keyboard_rows.append([
|
||
InlineKeyboardButton(f"❌ Close #{ticket} (${profit:+,.1f})", callback_data=f"cb_close_{ticket}")
|
||
])
|
||
|
||
if pending:
|
||
msg += f"📌 <b>SCHEDULED PENDING ORDERS ({len(pending)})</b>\n{'━' * 25}\n\n"
|
||
for o in pending:
|
||
ticket = o.get("ticket")
|
||
direction = o.get("type", "LIMIT")
|
||
volume = o.get("volume", 0.01)
|
||
target_p = o.get("price", 0.0)
|
||
sl = o.get("sl", 0.0)
|
||
tp = o.get("tp", 0.0)
|
||
msg += (
|
||
f"⏳ <b>Order #{ticket}</b> | {direction} {volume} lots\n"
|
||
f" • Trigger Price: <code>${target_p:,.2f}</code>\n"
|
||
f" • SL: <code>${sl:,.2f}</code> | TP: <code>${tp:,.2f}</code>\n\n"
|
||
)
|
||
keyboard_rows.append([
|
||
InlineKeyboardButton(f"🗑️ Cancel Order #{ticket}", callback_data=f"cb_cancel_{ticket}")
|
||
])
|
||
|
||
keyboard_rows.append([
|
||
InlineKeyboardButton("🔄 Refresh Positions", callback_data="cb_positions"),
|
||
InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu"),
|
||
])
|
||
|
||
reply_markup = InlineKeyboardMarkup(keyboard_rows)
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=reply_markup, parse_mode="HTML")
|
||
|
||
async def _handle_trades(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /trades command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
recent_trades = crud.get_recent_trades(limit=5)
|
||
stats = crud.get_trade_stats(days=7)
|
||
|
||
if not recent_trades:
|
||
msg = (
|
||
f"📜 <b>TRADE HISTORY</b>\n"
|
||
f"{'━' * 25}\n\n"
|
||
f"<i>No completed trades recorded in the database yet.</i>"
|
||
)
|
||
else:
|
||
msg = (
|
||
f"📜 <b>RECENT TRADES (Last 7 Days)</b>\n"
|
||
f"{'━' * 28}\n"
|
||
f"Win Rate: <b>{stats.get('win_rate', 0.0):.1f}%</b> | Profit Factor: <b>{stats.get('profit_factor', 0.0):.2f}</b>\n\n"
|
||
)
|
||
for t in recent_trades:
|
||
p_emoji = "🟢" if t.profit >= 0 else "🔴"
|
||
msg += (
|
||
f"{p_emoji} <b>#{t.ticket}</b> {t.order_type} {t.volume} lots\n"
|
||
f" Strategy: <i>{t.strategy}</i>\n"
|
||
f" In: ${t.entry_price:,.2f} → Out: ${t.exit_price:,.2f}\n"
|
||
f" Net P&L: <b>${t.profit:+,.2f}</b> ({t.status})\n\n"
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("💰 View P&L Stats", callback_data="cb_pnl")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_pnl(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /pnl command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
daily_pnl = crud.get_daily_pnl()
|
||
stats_7d = crud.get_trade_stats(days=7)
|
||
stats_30d = crud.get_trade_stats(days=30)
|
||
|
||
daily_emoji = "🟢" if daily_pnl >= 0 else "🔴"
|
||
|
||
msg = (
|
||
f"💰 <b>P&L & PERFORMANCE OVERVIEW</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"{daily_emoji} <b>Today's P&L:</b> <code>${daily_pnl:+,.2f}</code>\n\n"
|
||
f"📅 <b>7-Day Performance:</b>\n"
|
||
f" • Total Trades: {stats_7d.get('total_trades', 0)}\n"
|
||
f" • Win Rate: <b>{stats_7d.get('win_rate', 0.0):.1f}%</b>\n"
|
||
f" • Net Profit: <code>${stats_7d.get('total_profit', 0.0):+,.2f}</code>\n"
|
||
f" • Profit Factor: {stats_7d.get('profit_factor', 0.0):.2f}\n\n"
|
||
f"📆 <b>30-Day Performance:</b>\n"
|
||
f" • Total Trades: {stats_30d.get('total_trades', 0)}\n"
|
||
f" • Win Rate: <b>{stats_30d.get('win_rate', 0.0):.1f}%</b>\n"
|
||
f" • Net Profit: <code>${stats_30d.get('total_profit', 0.0):+,.2f}</code>\n"
|
||
f" • Profit Factor: {stats_30d.get('profit_factor', 0.0):.2f}"
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📜 View Trade History", callback_data="cb_trades")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_news(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /news command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
sentiment_score = crud.get_recent_sentiment(hours=24)
|
||
sent_emoji = "📈 BULLISH" if sentiment_score > 0.1 else ("📉 BEARISH" if sentiment_score < -0.1 else "➡️ NEUTRAL")
|
||
|
||
msg = (
|
||
f"📰 <b>MARKET NEWS & AI SENTIMENT</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Composite 24H Sentiment:</b> {sent_emoji} (<code>{sentiment_score:+.2f}</code>)\n"
|
||
f"<b>AI Engine:</b> Gemini 3.6 Flash + Bullion Sentiment\n\n"
|
||
)
|
||
|
||
# Pull top distinct news events from DB
|
||
session = crud.get_session()
|
||
try:
|
||
from database.models import NewsEvent
|
||
from news.news_utils import normalize_headline
|
||
events = session.query(NewsEvent).order_by(NewsEvent.id.desc()).limit(20).all()
|
||
unique_events = []
|
||
seen_headlines = set()
|
||
for ev in events:
|
||
norm = normalize_headline(ev.headline)
|
||
if norm not in seen_headlines:
|
||
seen_headlines.add(norm)
|
||
unique_events.append(ev)
|
||
if len(unique_events) >= 4:
|
||
break
|
||
|
||
if unique_events:
|
||
msg += "<b>Latest Macro Analysis:</b>\n"
|
||
for ev in unique_events:
|
||
impact_emoji = "🔴" if ev.impact_level == "HIGH" else ("🟡" if ev.impact_level == "MEDIUM" else "🟢")
|
||
msg += (
|
||
f"{impact_emoji} <b>{ev.headline[:75]}...</b>\n"
|
||
f" Sentiment: <b>{ev.sentiment}</b> (<code>{ev.sentiment_score:+.2f}</code>) | Source: <i>{ev.source}</i>\n"
|
||
)
|
||
if ev.gemini_analysis:
|
||
clean_note = ev.gemini_analysis[:110].replace("<", "<").replace(">", ">")
|
||
msg += f" <i>AI Note:</i> {clean_note}...\n\n"
|
||
else:
|
||
msg += "\n"
|
||
else:
|
||
msg += "<i>No news cached yet. Tap '⚡ Fetch Fresh News' below to load live articles instantly.</i>\n"
|
||
finally:
|
||
session.close()
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("⚡ Fetch Fresh News Now", callback_data="cb_fetch_fresh_news")],
|
||
[InlineKeyboardButton("🔄 Refresh View", callback_data="cb_news")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_fetch_fresh_news(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Fetch live news from Finnhub & Alpha Vantage on-demand and update message."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
loading_msg = "⏳ <i>Fetching & analyzing latest market news from Finnhub and Alpha Vantage...</i>"
|
||
await self._safe_edit_or_reply(update, text=loading_msg, parse_mode="HTML")
|
||
|
||
try:
|
||
from news.news_fetcher import NewsFetcher
|
||
from news.news_analyzer import NewsAnalyzer
|
||
|
||
fetcher = NewsFetcher()
|
||
analyzer = NewsAnalyzer()
|
||
articles = await fetcher.fetch_all_news()
|
||
|
||
for article in articles[:5]:
|
||
headline = article.get("headline", "")
|
||
url = article.get("url", "")
|
||
if crud.is_news_already_saved(headline, url):
|
||
continue
|
||
analysis = await analyzer.analyze_article(article)
|
||
crud.save_news_event(
|
||
source=article.get("source", ""),
|
||
headline=headline,
|
||
summary=article.get("summary", ""),
|
||
url=url,
|
||
sentiment=analysis["sentiment"],
|
||
sentiment_score=analysis["combined_score"],
|
||
finbert_score=analysis["finbert"]["score"],
|
||
gemini_score=analysis.get("gemini", {}).get("score", 0),
|
||
gemini_analysis=analysis.get("gemini", {}).get("analysis", ""),
|
||
impact_level=analysis["impact_level"],
|
||
published_at=article.get("published_at"),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"On-demand news fetch failed: {e}")
|
||
|
||
# Render updated news
|
||
await self._handle_news(update, context)
|
||
|
||
async def _handle_regime(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /regime command or button with real-time on-demand calculation."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
msg = (
|
||
f"🧠 <b>MARKET REGIME & ACTIVE STRATEGIES</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
)
|
||
|
||
regime = None
|
||
if self.bot_instance and hasattr(self.bot_instance, "_last_regime"):
|
||
regime = getattr(self.bot_instance, "_last_regime", None)
|
||
|
||
if not regime:
|
||
# Dynamic on-demand regime computation via MT5 H4 candles
|
||
try:
|
||
from core.mt5_connector import MT5Connector
|
||
from strategies.regime_detector import RegimeDetector
|
||
from analysis.technical import TechnicalAnalyzer
|
||
|
||
mt5_obj = getattr(self.bot_instance, "mt5", None)
|
||
if not mt5_obj:
|
||
mt5_obj = MT5Connector()
|
||
|
||
if mt5_obj.connect(max_retries=1, retry_delay=0.1):
|
||
df_h4 = mt5_obj.get_rates(timeframe="H4", count=200, auto_reconnect=False)
|
||
if df_h4 is not None and not df_h4.empty:
|
||
ta = TechnicalAnalyzer()
|
||
df_analyzed = ta.add_all_indicators(df_h4.copy())
|
||
rd = RegimeDetector()
|
||
regime = rd.analyze(df_analyzed)
|
||
if self.bot_instance:
|
||
setattr(self.bot_instance, "_last_regime", regime)
|
||
except Exception as e:
|
||
logger.warning(f"On-demand regime analysis failed: {e}")
|
||
|
||
if regime:
|
||
regime_emoji = "📈" if "BULL" in regime.regime.value else ("📉" if "BEAR" in regime.regime.value else "🔄")
|
||
msg += (
|
||
f"<b>Current Regime:</b> {regime_emoji} <code>{regime.regime.value}</code>\n"
|
||
f"<b>Trend Strength (ADX):</b> <code>{regime.adx_value:.1f}</code>\n"
|
||
f"<b>Volatility Percentile:</b> <code>{regime.volatility_percentile:.1f}% ({regime.volatility_label})</code>\n"
|
||
f"<b>Position Sizing Modifier:</b> <code>{regime.position_size_modifier:.2f}x</code>\n"
|
||
f"<b>Trading Allowed:</b> {'✅ YES' if regime.should_trade else '⏸️ PAUSED'}\n\n"
|
||
f"<b>Active Recommended Strategies:</b>\n"
|
||
)
|
||
for s in regime.recommended_strategies:
|
||
msg += f" • <code>{s.upper()}</code>\n"
|
||
|
||
# Temporal Self-Attention metrics (MetaQuotes Book Ch. 5)
|
||
if self.bot_instance and hasattr(self.bot_instance, "attention_scorer"):
|
||
try:
|
||
mt5_obj = getattr(self.bot_instance, "mt5", None)
|
||
if mt5_obj and mt5_obj.is_connected:
|
||
df_m15 = mt5_obj.get_rates(timeframe="M15", count=25, auto_reconnect=False)
|
||
if df_m15 is not None and len(df_m15) >= 20:
|
||
att_prof = self.bot_instance.attention_scorer.compute_attention(df_m15)
|
||
conc = att_prof.get("concentration", 0.0)
|
||
anchor = att_prof.get("anchor_type", "PIVOT")
|
||
msg += (
|
||
f"\n🧠 <b>Temporal Self-Attention:</b>\n"
|
||
f" • Focus Concentration: <code>{conc:.1f}%</code>\n"
|
||
f" • Structural Anchor: <code>{anchor}</code>\n"
|
||
)
|
||
except Exception:
|
||
pass
|
||
else:
|
||
msg += "<i>MT5 offline or H4 candle data unavailable. Ensure MT5 is running on your desktop.</i>\n"
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🔄 Refresh Regime", callback_data="cb_regime")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_learning(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /learning command or callback button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
try:
|
||
from ai.trade_learner import trade_learner
|
||
metrics = trade_learner.get_learning_metrics()
|
||
except Exception as e:
|
||
logger.error(f"Error getting learning metrics: {e}")
|
||
metrics = {}
|
||
|
||
mults = metrics.get("strategy_multipliers", {})
|
||
scalp = mults.get("scalping", 1.0)
|
||
day = mults.get("day_trading", 1.0)
|
||
swing = mults.get("swing_trading", 1.0)
|
||
|
||
weights = metrics.get("adaptive_weights", {})
|
||
tech = weights.get("technical", 40.0)
|
||
ai = weights.get("ai_prediction", 25.0)
|
||
sent = weights.get("sentiment", 20.0)
|
||
reg = weights.get("regime", 15.0)
|
||
|
||
memorized_count = metrics.get("active_mistakes_memorized", 0)
|
||
recent_lessons = metrics.get("recent_lessons", [])
|
||
|
||
msg = (
|
||
f"🎓 <b>AUTONOMOUS SELF-LEARNING & ADAPTATION</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Adaptive Strategy Multipliers:</b>\n"
|
||
f" • <b>Scalping:</b> <code>{scalp:.2f}x</code> {'(✅ Conviction Boost)' if scalp > 1.0 else ('(⚠️ Cautious Sizing)' if scalp < 1.0 else '')}\n"
|
||
f" • <b>Day Trading:</b> <code>{day:.2f}x</code> {'(✅ Conviction Boost)' if day > 1.0 else ('(⚠️ Cautious Sizing)' if day < 1.0 else '')}\n"
|
||
f" • <b>Swing:</b> <code>{swing:.2f}x</code>\n\n"
|
||
f"<b>Dynamic Confluence Distribution:</b>\n"
|
||
f" • 📊 Technical: <code>{tech:.1f}%</code>\n"
|
||
f" • 🤖 AI Model: <code>{ai:.1f}%</code>\n"
|
||
f" • 📰 News Sentiment: <code>{sent:.1f}%</code>\n"
|
||
f" • 🧠 Market Regime: <code>{reg:.1f}%</code>\n\n"
|
||
f"<b>Mistake Memory Guard:</b>\n"
|
||
f" • <b>Active Guarded Traps:</b> <code>{memorized_count}</code>\n"
|
||
)
|
||
|
||
if recent_lessons:
|
||
latest = recent_lessons[0]
|
||
cat = latest.get("category", "LESSON")
|
||
rule = latest.get("rule", latest.get("summary", "Keep risk contained."))
|
||
msg += f" • <b>Latest Learned Rule ({cat}):</b>\n <i>\"{rule}\"</i>\n"
|
||
else:
|
||
msg += " • <i>Mistake Memory Guard active — monitoring all trade closes.</i>\n"
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🔄 Refresh Learning", callback_data="cb_learning")],
|
||
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_price(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /price command or button."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
tick = None
|
||
if self.bot_instance and hasattr(self.bot_instance, "mt5"):
|
||
mt5_obj = self.bot_instance.mt5
|
||
is_conn = mt5_obj.is_connected() if callable(getattr(mt5_obj, "is_connected", None)) else getattr(mt5_obj, "is_connected", False)
|
||
if is_conn:
|
||
tick = mt5_obj.get_current_tick(self.settings.symbol, auto_reconnect=False)
|
||
|
||
if tick:
|
||
bid = tick.get("bid", 0.0)
|
||
ask = tick.get("ask", 0.0)
|
||
spread = tick.get("spread", 0.0)
|
||
msg = (
|
||
f"🏷️ <b>LIVE {self.settings.symbol} TICKER</b>\n"
|
||
f"{'━' * 25}\n\n"
|
||
f"<b>Bid:</b> <code>${bid:,.2f}</code>\n"
|
||
f"<b>Ask:</b> <code>${ask:,.2f}</code>\n"
|
||
f"<b>Spread:</b> <code>{spread:.1f} points</code>\n"
|
||
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}"
|
||
)
|
||
else:
|
||
msg = f"🏷️ <b>{self.settings.symbol} TICKER</b>\n\n<i>MT5 not connected or markets closed.</i>"
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🔄 Refresh Price", callback_data="cb_price")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_pause(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Pause the trading bot."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
if self.bot_instance:
|
||
self.bot_instance._trading_paused = True
|
||
logger.warning("Bot trading paused via Telegram remote control")
|
||
msg = "⏸️ <b>Trading PAUSED</b>\n\nThe bot will NOT open new trades. Existing positions will still be managed by trailing stops."
|
||
else:
|
||
msg = "⚠️ Bot instance not linked."
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("▶️ Resume Trading", callback_data="cb_resume")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_resume(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Resume the trading bot."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
if self.bot_instance:
|
||
self.bot_instance._trading_paused = False
|
||
logger.info("Bot trading resumed via Telegram remote control")
|
||
msg = "▶️ <b>Trading RESUMED</b>\n\nAutomated strategy scanning and execution is active."
|
||
else:
|
||
msg = "⚠️ Bot instance not linked."
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📊 View Status", callback_data="cb_status")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_mode(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /mode [safe|moderate|aggressive] or show mode menu."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
if context and context.args:
|
||
target_mode = context.args[0].lower().strip()
|
||
if target_mode in ["safe", "moderate", "aggressive"]:
|
||
await self._set_mode(update, target_mode)
|
||
return
|
||
|
||
# No arg or invalid arg: display mode selection menu
|
||
await self._show_mode_menu(update)
|
||
|
||
async def _set_mode(self, update: Update, target_mode: str):
|
||
"""Apply a new trading mode and notify user."""
|
||
success = self.settings.set_active_mode(target_mode)
|
||
if success:
|
||
# Reset any risk manager overrides so mode defaults take effect
|
||
if self.bot_instance and hasattr(self.bot_instance, "risk_manager"):
|
||
rm = self.bot_instance.risk_manager
|
||
rm._max_risk_pct_override = None
|
||
rm._default_lot_override = None
|
||
rm._max_spread_override = None
|
||
rm._max_concurrent_override = None
|
||
|
||
mode_info = self.settings.active_mode_config
|
||
name = mode_info.get("name", target_mode.title())
|
||
desc = mode_info.get("description", "")
|
||
scalp_conf = mode_info.get("scalping_min_confluence", 50)
|
||
day_conf = mode_info.get("day_trading_min_confluence", 55)
|
||
risk_pct = mode_info.get("max_risk_per_trade_pct", 2.5)
|
||
spread = mode_info.get("max_spread_points", 70)
|
||
max_trades = mode_info.get("max_concurrent_trades", 4)
|
||
lot = mode_info.get("default_lot_size", 0.02)
|
||
|
||
msg = (
|
||
f"✅ <b>TRADING MODE SWITCHED</b>\n\n"
|
||
f"<b>Active Mode:</b> <code>{name}</code>\n"
|
||
f"<i>{desc}</i>\n\n"
|
||
f"<b>Confluence Thresholds:</b>\n"
|
||
f" • Scalping (M5): <code>≥ {scalp_conf} pts</code>\n"
|
||
f" • Day Trading (H1): <code>≥ {day_conf} pts</code>\n\n"
|
||
f"<b>Risk Parameters:</b>\n"
|
||
f" • Risk Per Trade: <code>{risk_pct}%</code> (Default Lot: <code>{lot}</code>)\n"
|
||
f" • Max Concurrent Trades: <code>{max_trades}</code>\n"
|
||
f" • Max Spread: <code>{spread} pts</code>"
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🎛️ Change Mode", callback_data="cb_mode_menu")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
logger.info(f"Trading mode switched to {target_mode.upper()} via Telegram")
|
||
else:
|
||
await self._safe_edit_or_reply(
|
||
update,
|
||
text="❌ Failed to switch mode. Valid options: <code>safe</code>, <code>moderate</code>, <code>aggressive</code>",
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
async def _show_mode_menu(self, update: Update):
|
||
"""Display interactive mode selection menu."""
|
||
curr_mode = self.settings.active_mode.lower()
|
||
mode_info = self.settings.active_mode_config
|
||
name = mode_info.get("name", curr_mode.title())
|
||
desc = mode_info.get("description", "")
|
||
|
||
msg = (
|
||
f"🎛️ <b>TRADING PROFILE & RISK MODES</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Current Active Mode:</b> <code>{name}</code>\n"
|
||
f"<i>{desc}</i>\n\n"
|
||
f"Choose a mode below to tune confirmation assurance and trade frequency in real-time:"
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton(
|
||
f"{'🔘' if curr_mode == 'safe' else '⚪'} 🛡️ Safe (Conservative)",
|
||
callback_data="cb_set_mode_safe"
|
||
),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
f"{'🔘' if curr_mode == 'moderate' else '⚪'} ⚖️ Moderate (Calculated Risk)",
|
||
callback_data="cb_set_mode_moderate"
|
||
),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
f"{'🔘' if curr_mode == 'aggressive' else '⚪'} ⚡ Aggressive (High Frequency)",
|
||
callback_data="cb_set_mode_aggressive"
|
||
),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🔄 Refresh View", callback_data="cb_mode_menu"),
|
||
InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu"),
|
||
],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_confirm_closeall(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Prompt confirmation for emergency close all."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
mt5_obj = getattr(self.bot_instance, "mt5", None)
|
||
is_conn = False
|
||
if mt5_obj:
|
||
is_conn = mt5_obj.is_connected() or mt5_obj.connect(max_retries=1, retry_delay=0.1)
|
||
|
||
if not is_conn:
|
||
msg = (
|
||
f"⚠️ <b>CANNOT CLOSE POSITIONS — MT5 OFFLINE</b>\n\n"
|
||
f"MetaTrader 5 terminal is not running or connected.\n"
|
||
f"Please start your MetaTrader 5 terminal first."
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📊 View Status", callback_data="cb_status")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
return
|
||
|
||
msg = (
|
||
f"⚠️ <b>EMERGENCY KILL-SWITCH CONFIRMATION</b> ⚠️\n\n"
|
||
f"Are you sure you want to <b>CLOSE ALL OPEN POSITIONS</b> immediately?\n"
|
||
f"This will execute market close orders on all active trades."
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🛑 YES, CLOSE ALL NOW", callback_data="cb_do_closeall")],
|
||
[InlineKeyboardButton("❌ Cancel", callback_data="cb_positions")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_do_closeall(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Execute emergency close all positions."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
mt5_obj = getattr(self.bot_instance, "mt5", None)
|
||
is_conn = False
|
||
if mt5_obj:
|
||
is_conn = mt5_obj.is_connected() or mt5_obj.connect(max_retries=1, retry_delay=0.1)
|
||
|
||
if not is_conn:
|
||
msg = (
|
||
f"⚠️ <b>CANNOT CLOSE POSITIONS — MT5 OFFLINE</b>\n\n"
|
||
f"MetaTrader 5 terminal is not running or connected.\n"
|
||
f"Please launch MetaTrader 5 on your desktop and verify your Exness login."
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📊 View Status", callback_data="cb_status")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
return
|
||
|
||
closed_count = 0
|
||
if self.bot_instance and hasattr(self.bot_instance, "trade_executor"):
|
||
te = self.bot_instance.trade_executor
|
||
if hasattr(te, "close_all_positions"):
|
||
closed_count = te.close_all_positions(self.settings.symbol)
|
||
elif mt5_obj:
|
||
closed_count = mt5_obj.close_all_positions(self.settings.symbol)
|
||
elif mt5_obj:
|
||
closed_count = mt5_obj.close_all_positions(self.settings.symbol)
|
||
|
||
if closed_count > 0:
|
||
msg = (
|
||
f"🛑 <b>ALL POSITIONS CLOSED</b>\n\n"
|
||
f"Successfully closed <b>{closed_count}</b> open position(s).\n"
|
||
f"All active trades have been flattened."
|
||
)
|
||
else:
|
||
msg = (
|
||
f"ℹ️ <b>NO OPEN POSITIONS</b>\n\n"
|
||
f"There were no active positions open to close."
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📊 View Status", callback_data="cb_status")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_close_single(self, update: Update, ticket: int):
|
||
"""Close a specific position by ticket number."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
success = False
|
||
if self.bot_instance and hasattr(self.bot_instance, "trade_executor"):
|
||
te = self.bot_instance.trade_executor
|
||
if hasattr(te, "close_trade"):
|
||
success = te.close_trade(ticket, reason="telegram_manual")
|
||
elif hasattr(te, "close_position"):
|
||
success = te.close_position(ticket, comment="Telegram manual close")
|
||
elif self.bot_instance and hasattr(self.bot_instance, "mt5"):
|
||
success = self.bot_instance.mt5.close_position(ticket, comment="telegram_manual")
|
||
else:
|
||
from core.mt5_connector import MT5Connector
|
||
mt5_obj = MT5Connector()
|
||
success = mt5_obj.close_position(ticket, comment="telegram_manual")
|
||
|
||
if success:
|
||
msg = f"✅ Position <b>#{ticket}</b> closed successfully."
|
||
else:
|
||
msg = f"❌ Failed to close position <b>#{ticket}</b>. Check MT5 connection."
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📈 View Positions", callback_data="cb_positions")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_cancel_single(self, update: Update, ticket: int):
|
||
"""Cancel a specific pending order by ticket number."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
success = False
|
||
if self.bot_instance and hasattr(self.bot_instance, "mt5"):
|
||
success = self.bot_instance.mt5.cancel_order(ticket)
|
||
else:
|
||
from core.mt5_connector import MT5Connector
|
||
mt5_obj = MT5Connector()
|
||
success = mt5_obj.cancel_order(ticket)
|
||
|
||
if success:
|
||
msg = f"🗑️ Scheduled Pending Order <b>#{ticket}</b> canceled successfully."
|
||
else:
|
||
msg = f"❌ Failed to cancel Order <b>#{ticket}</b>. It may have already triggered or expired."
|
||
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📈 View Positions & Orders", callback_data="cb_positions")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _handle_set_lot(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Set default lot size via /setlot <size>."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
if not context.args:
|
||
await update.effective_message.reply_text(
|
||
"Usage: <code>/setlot 0.02</code> (range: 0.01 to 0.10)", parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
try:
|
||
new_lot = float(context.args[0])
|
||
if new_lot < 0.01 or new_lot > 0.50:
|
||
await update.effective_message.reply_text("❌ Lot size must be between 0.01 and 0.50.")
|
||
return
|
||
|
||
# Update live settings
|
||
self.settings.trading_params.setdefault("risk", {})["default_lot_size"] = new_lot
|
||
if self.bot_instance and hasattr(self.bot_instance, "risk_manager"):
|
||
self.bot_instance.risk_manager.default_lot = new_lot
|
||
|
||
await update.effective_message.reply_text(
|
||
f"✅ <b>Default Lot Size Updated:</b> <code>{new_lot}</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
logger.info(f"Default lot size changed to {new_lot} via Telegram")
|
||
|
||
except ValueError:
|
||
await update.effective_message.reply_text("❌ Invalid number format. Example: <code>/setlot 0.02</code>")
|
||
|
||
async def _handle_set_risk(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Set max risk percent per trade via /setrisk <pct>."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
if not context.args:
|
||
await update.effective_message.reply_text(
|
||
"Usage: <code>/setrisk 2.5</code> (range: 0.5% to 5.0%)", parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
try:
|
||
new_risk = float(context.args[0])
|
||
if new_risk < 0.5 or new_risk > 5.0:
|
||
await update.effective_message.reply_text("❌ Risk per trade must be between 0.5% and 5.0%.")
|
||
return
|
||
|
||
self.settings.trading_params.setdefault("risk", {})["max_risk_per_trade_pct"] = new_risk
|
||
if self.bot_instance and hasattr(self.bot_instance, "risk_manager"):
|
||
self.bot_instance.risk_manager.max_risk_pct = new_risk
|
||
|
||
await update.effective_message.reply_text(
|
||
f"✅ <b>Max Risk Per Trade Updated:</b> <code>{new_risk:.1f}%</code>",
|
||
parse_mode="HTML",
|
||
)
|
||
logger.info(f"Risk per trade changed to {new_risk}% via Telegram")
|
||
|
||
except ValueError:
|
||
await update.effective_message.reply_text("❌ Invalid number format. Example: <code>/setrisk 2.0</code>")
|
||
|
||
async def _handle_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle /help command."""
|
||
if not self._is_authorized(update):
|
||
return
|
||
|
||
msg = (
|
||
f"📖 <b>TELEGRAM BOT COMMAND CHEATSHEET</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Navigation & Profile Controls:</b>\n"
|
||
f"• <code>/start</code> or <code>/menu</code> — Open interactive button dashboard\n"
|
||
f"• <code>/mode</code> — Switch trading profile (Safe / Moderate / Aggressive)\n"
|
||
f"• <code>/help</code> — Show this command cheatsheet\n\n"
|
||
f"<b>Market & Account Monitoring:</b>\n"
|
||
f"• <code>/status</code> — Account balance, equity, and bot health\n"
|
||
f"• <code>/positions</code> — List open trades with floating P&L\n"
|
||
f"• <code>/trades</code> — Recent closed trade log\n"
|
||
f"• <code>/pnl</code> — Today's and 30-day performance\n"
|
||
f"• <code>/news</code> — Macro news and Gemini sentiment\n"
|
||
f"• <code>/regime</code> — Current market regime & active strategies\n"
|
||
f"• <code>/learning</code> — Self-learning performance & weight adaptations\n"
|
||
f"• <code>/price</code> — Real-time Gold Bid, Ask, Spread\n\n"
|
||
f"<b>Remote Control & Parameters:</b>\n"
|
||
f"• <code>/pause</code> — Pause opening new trades\n"
|
||
f"• <code>/resume</code> — Resume automated trading\n"
|
||
f"• <code>/closeall</code> — Flatten all open positions immediately\n"
|
||
f"• <code>/setlot 0.02</code> — Set default lot size\n"
|
||
f"• <code>/setrisk 2.5</code> — Set max risk percent"
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("🎛️ Switch Mode", callback_data="cb_mode_menu")],
|
||
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
|
||
])
|
||
await self._safe_edit_or_reply(update, text=msg, reply_markup=keyboard, parse_mode="HTML")
|
||
|
||
async def _callback_router(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Route callback queries from inline buttons with gatekeeper authorization check."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return
|
||
|
||
if not self._is_authorized(update):
|
||
try:
|
||
await query.answer("⛔ Unauthorized: You are not authorized to control this bot.", show_alert=True)
|
||
except Exception:
|
||
pass
|
||
return
|
||
|
||
try:
|
||
await query.answer()
|
||
except Exception:
|
||
pass
|
||
|
||
data = query.data
|
||
try:
|
||
if data == "cb_menu":
|
||
await self._handle_start(update, context)
|
||
elif data == "cb_status":
|
||
await self._handle_status(update, context)
|
||
elif data == "cb_positions":
|
||
await self._handle_positions(update, context)
|
||
elif data == "cb_trades":
|
||
await self._handle_trades(update, context)
|
||
elif data == "cb_pnl":
|
||
await self._handle_pnl(update, context)
|
||
elif data == "cb_news":
|
||
await self._handle_news(update, context)
|
||
elif data == "cb_fetch_fresh_news":
|
||
await self._handle_fetch_fresh_news(update, context)
|
||
elif data == "cb_regime":
|
||
await self._handle_regime(update, context)
|
||
elif data == "cb_learning":
|
||
await self._handle_learning(update, context)
|
||
elif data == "cb_price":
|
||
await self._handle_price(update, context)
|
||
elif data == "cb_pause":
|
||
await self._handle_pause(update, context)
|
||
elif data == "cb_resume":
|
||
await self._handle_resume(update, context)
|
||
elif data == "cb_mode_menu":
|
||
await self._show_mode_menu(update)
|
||
elif data == "cb_set_mode_safe":
|
||
await self._set_mode(update, "safe")
|
||
elif data == "cb_set_mode_moderate":
|
||
await self._set_mode(update, "moderate")
|
||
elif data == "cb_set_mode_aggressive":
|
||
await self._set_mode(update, "aggressive")
|
||
elif data == "cb_confirm_closeall":
|
||
await self._handle_confirm_closeall(update, context)
|
||
elif data == "cb_do_closeall":
|
||
await self._handle_do_closeall(update, context)
|
||
elif data == "cb_help":
|
||
await self._handle_help(update, context)
|
||
elif data.startswith("cb_close_"):
|
||
ticket = int(data.split("_")[-1])
|
||
await self._handle_close_single(update, ticket)
|
||
elif data.startswith("cb_cancel_"):
|
||
ticket = int(data.split("_")[-1])
|
||
await self._handle_cancel_single(update, ticket)
|
||
except Exception as e:
|
||
logger.error(f"Callback error for {data}: {e}")
|
||
try:
|
||
if query.message:
|
||
await query.message.reply_text(f"⚠️ Operation error: {e}")
|
||
except Exception:
|
||
pass
|
||
|
||
# ── Background Polling Lifecycle ──────────────────────────────────────
|
||
|
||
async def start_polling(self):
|
||
"""Start the interactive Telegram bot polling in the background."""
|
||
if not self._enabled:
|
||
return
|
||
|
||
try:
|
||
self._app = ApplicationBuilder().token(self.bot_token).build()
|
||
|
||
# Register command handlers
|
||
self._app.add_handler(CommandHandler(["start", "menu"], self._handle_start))
|
||
self._app.add_handler(CommandHandler("status", self._handle_status))
|
||
self._app.add_handler(CommandHandler(["mode", "setmode"], self._handle_mode))
|
||
self._app.add_handler(CommandHandler(["positions", "pos"], self._handle_positions))
|
||
self._app.add_handler(CommandHandler("trades", self._handle_trades))
|
||
self._app.add_handler(CommandHandler("pnl", self._handle_pnl))
|
||
self._app.add_handler(CommandHandler("news", self._handle_news))
|
||
self._app.add_handler(CommandHandler("regime", self._handle_regime))
|
||
self._app.add_handler(CommandHandler("learning", self._handle_learning))
|
||
self._app.add_handler(CommandHandler("price", self._handle_price))
|
||
self._app.add_handler(CommandHandler("pause", self._handle_pause))
|
||
self._app.add_handler(CommandHandler("resume", self._handle_resume))
|
||
self._app.add_handler(CommandHandler("closeall", self._handle_confirm_closeall))
|
||
self._app.add_handler(CommandHandler("setlot", self._handle_set_lot))
|
||
self._app.add_handler(CommandHandler("setrisk", self._handle_set_risk))
|
||
self._app.add_handler(CommandHandler("help", self._handle_help))
|
||
|
||
# Register callback button router
|
||
self._app.add_handler(CallbackQueryHandler(self._callback_router))
|
||
|
||
# Register error handler for network/conflict issues
|
||
async def _on_telegram_error(update: object, context: ContextTypes.DEFAULT_TYPE):
|
||
err = context.error
|
||
err_msg = str(err)
|
||
if "Conflict" in type(err).__name__ or "terminated by other getUpdates" in err_msg:
|
||
logger.warning(
|
||
"⚠️ Telegram Conflict: Another bot instance is currently active on Telegram. "
|
||
"Trading alerts will still be sent, but interactive chat commands are handled by the other instance."
|
||
)
|
||
else:
|
||
logger.error(f"Telegram error: {err}")
|
||
|
||
self._app.add_error_handler(_on_telegram_error)
|
||
|
||
await self._app.initialize()
|
||
await self._app.start()
|
||
|
||
# Register native Telegram Menu commands for quick auto-complete
|
||
try:
|
||
from telegram import BotCommand
|
||
cmds = [
|
||
BotCommand("start", "Command Center & Main Menu"),
|
||
BotCommand("mode", "Switch Trading Profile (Safe / Moderate / Aggressive)"),
|
||
BotCommand("status", "Account Balance, Equity & Health"),
|
||
BotCommand("positions", "Open Positions & Floating P&L"),
|
||
BotCommand("pnl", "Daily & Monthly P&L Performance"),
|
||
BotCommand("trades", "Recent Trade History"),
|
||
BotCommand("news", "Macro News & Gemini Sentiment"),
|
||
BotCommand("regime", "Market Regime & Active Strategies"),
|
||
BotCommand("learning", "Autonomous Self-Learning Metrics"),
|
||
BotCommand("price", "Live Gold Price & Spread"),
|
||
BotCommand("pause", "Pause Automatic Order Execution"),
|
||
BotCommand("resume", "Resume Automatic Order Execution"),
|
||
BotCommand("setlot", "Set Default Lot Size (e.g. /setlot 0.02)"),
|
||
BotCommand("setrisk", "Set Risk % Per Trade (e.g. /setrisk 2.0)"),
|
||
BotCommand("closeall", "Emergency Close All Positions"),
|
||
BotCommand("help", "Command Cheatsheet & Documentation"),
|
||
]
|
||
await self._app.bot.set_my_commands(cmds)
|
||
logger.info("⚡ Registered native Telegram Menu commands via set_my_commands")
|
||
except Exception as e:
|
||
logger.warning(f"Could not register Telegram menu commands: {e}")
|
||
|
||
await self._app.updater.start_polling(drop_pending_updates=True)
|
||
self._is_polling = True
|
||
logger.info("⚡ Telegram Interactive Bot polling started successfully")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to start Telegram polling: {e}")
|
||
|
||
async def stop_polling(self):
|
||
"""Stop background polling gracefully."""
|
||
if self._app and self._is_polling:
|
||
try:
|
||
await self._app.updater.stop()
|
||
await self._app.stop()
|
||
await self._app.shutdown()
|
||
self._is_polling = False
|
||
logger.info("Telegram Interactive Bot polling stopped")
|
||
except Exception as e:
|
||
logger.debug(f"Telegram stop error: {e}")
|
||
|
||
# ── Outbound Notifications ────────────────────────────────────────────
|
||
|
||
async def send_message(self, text: str, parse_mode: str = "HTML", reply_markup: Any = None):
|
||
"""Send a message to the configured owner chat."""
|
||
if not self._enabled:
|
||
return
|
||
|
||
try:
|
||
if self._app and self._app.bot:
|
||
bot = self._app.bot
|
||
else:
|
||
from telegram import Bot
|
||
bot = Bot(token=self.bot_token)
|
||
|
||
await bot.send_message(
|
||
chat_id=self.chat_id,
|
||
text=text,
|
||
parse_mode=parse_mode,
|
||
reply_markup=reply_markup,
|
||
)
|
||
except Exception as e:
|
||
err_msg = str(e)
|
||
if self.bot_token:
|
||
err_msg = err_msg.replace(self.bot_token, "***BOT_TOKEN***")
|
||
logger.error(f"Telegram send error: {err_msg}")
|
||
|
||
async def send_trade_opened(self, trade: dict):
|
||
"""Send trade entry alert with interactive quick-action buttons."""
|
||
direction_emoji = "📈" if trade["direction"] == "BUY" else "📉"
|
||
ticket = trade.get("ticket", "")
|
||
|
||
msg = (
|
||
f"{direction_emoji} <b>NEW TRADE OPENED</b>\n\n"
|
||
f"<b>Direction:</b> <b>{trade['direction']}</b> ({trade['volume']} lots)\n"
|
||
f"<b>Ticket:</b> <code>#{ticket}</code>\n"
|
||
f"<b>Entry Price:</b> <code>${trade['entry_price']:,.2f}</code>\n"
|
||
f"<b>Stop Loss:</b> <code>${trade['stop_loss']:,.2f}</code>\n"
|
||
f"<b>Take Profit:</b> <code>${trade['take_profit']:,.2f}</code>\n"
|
||
f"<b>Strategy:</b> <i>{trade['strategy']}</i>\n"
|
||
f"<b>Confluence:</b> <b>{trade['confluence_score']:.0f}/100</b>\n"
|
||
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}"
|
||
)
|
||
|
||
buttons = []
|
||
if ticket:
|
||
buttons.append([
|
||
InlineKeyboardButton(f"❌ Close Trade #{ticket}", callback_data=f"cb_close_{ticket}")
|
||
])
|
||
buttons.append([InlineKeyboardButton("📊 View Open Positions", callback_data="cb_positions")])
|
||
|
||
await self.send_message(msg, reply_markup=InlineKeyboardMarkup(buttons))
|
||
|
||
async def send_trade_closed(
|
||
self,
|
||
ticket: int | dict,
|
||
direction: str = "",
|
||
profit: float = 0.0,
|
||
entry: float = 0.0,
|
||
exit_price: float = 0.0,
|
||
duration_min: int = 0,
|
||
reason: str = "TP/SL",
|
||
):
|
||
"""Send trade close notification."""
|
||
if isinstance(ticket, dict):
|
||
d = ticket
|
||
ticket = d.get("ticket", 0)
|
||
direction = d.get("direction", "")
|
||
profit = d.get("profit", 0.0)
|
||
entry = d.get("entry_price", d.get("entry", 0.0))
|
||
exit_price = d.get("exit_price", d.get("close_price", 0.0))
|
||
duration_min = d.get("duration_minutes", d.get("duration_min", 0))
|
||
reason = d.get("reason", "TP/SL")
|
||
|
||
emoji = "✅" if profit > 0 else "❌"
|
||
profit_color = "🟢" if profit > 0 else "🔴"
|
||
|
||
msg = (
|
||
f"{emoji} <b>TRADE CLOSED ({reason})</b>\n\n"
|
||
f"<b>Ticket:</b> <code>#{ticket}</code> | <b>{direction}</b>\n"
|
||
f"<b>Entry:</b> ${entry:,.2f} → <b>Exit:</b> ${exit_price:,.2f}\n"
|
||
f"{profit_color} <b>Realized P&L:</b> <b>${profit:+,.2f}</b>\n"
|
||
f"<b>Duration:</b> {duration_min} minutes\n"
|
||
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}"
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("💰 View Daily P&L", callback_data="cb_pnl")],
|
||
])
|
||
await self.send_message(msg, reply_markup=keyboard)
|
||
|
||
async def send_daily_summary(
|
||
self,
|
||
balance: float,
|
||
daily_pnl: float,
|
||
total_trades: int,
|
||
winning: int,
|
||
losing: int,
|
||
win_rate: float,
|
||
):
|
||
"""Send end-of-day performance summary."""
|
||
pnl_emoji = "📊" if daily_pnl >= 0 else "📉"
|
||
profit_color = "🟢" if daily_pnl >= 0 else "🔴"
|
||
|
||
msg = (
|
||
f"{pnl_emoji} <b>DAILY TRADING SUMMARY</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Balance:</b> <code>${balance:,.2f}</code>\n"
|
||
f"{profit_color} <b>Today's Net P&L:</b> <code>${daily_pnl:+,.2f}</code>\n"
|
||
f"<b>Total Trades:</b> {total_trades}\n"
|
||
f"<b>Won:</b> {winning} | <b>Lost:</b> {losing}\n"
|
||
f"<b>Win Rate:</b> <b>{win_rate:.1f}%</b>\n\n"
|
||
f"📅 Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📜 View Trade History", callback_data="cb_trades")],
|
||
])
|
||
await self.send_message(msg, reply_markup=keyboard)
|
||
|
||
async def send_news_alert(self, headline: str, sentiment: str, impact: str, score: float):
|
||
"""Send high-impact news alert with strict deduplication."""
|
||
from news.news_utils import compute_news_hash
|
||
h_hash = compute_news_hash(headline)
|
||
if h_hash in self._sent_news_alert_hashes:
|
||
logger.info(f"Skipping duplicate Telegram news alert for: {headline[:60]}")
|
||
return
|
||
self._sent_news_alert_hashes.add(h_hash)
|
||
|
||
impact_emoji = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🟢"}.get(impact, "⚪")
|
||
sentiment_emoji = {"BULLISH": "📈", "BEARISH": "📉", "NEUTRAL": "➡️"}.get(sentiment, "➡️")
|
||
|
||
msg = (
|
||
f"{impact_emoji} <b>HIGH IMPACT MACRO ALERT</b>\n"
|
||
f"{'━' * 28}\n\n"
|
||
f"<b>Headline:</b> {headline}\n\n"
|
||
f"{sentiment_emoji} <b>Sentiment:</b> <b>{sentiment}</b> (<code>{score:+.2f}</code>)\n"
|
||
f"<b>Impact:</b> {impact}\n"
|
||
f"<b>Action:</b> Pre-event cooling pause activated for new entries."
|
||
)
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📰 View News Digest", callback_data="cb_news")],
|
||
])
|
||
await self.send_message(msg, reply_markup=keyboard)
|
||
|
||
async def send_risk_alert(self, message: str):
|
||
"""Send risk management alert."""
|
||
msg = f"⚠️ <b>RISK CONTROLLER ALERT</b>\n\n{message}"
|
||
await self.send_message(msg)
|
||
|
||
async def send_bot_status(self, status: str, details: str = ""):
|
||
"""Send bot status update."""
|
||
emoji = "🟢" if status == "RUNNING" else ("🔴" if status == "STOPPED" else "🟡")
|
||
msg = f"{emoji} <b>XAUUSD Bot Status: {status}</b>"
|
||
if details:
|
||
msg += f"\n\n{details}"
|
||
keyboard = InlineKeyboardMarkup([
|
||
[InlineKeyboardButton("📱 Open Menu", callback_data="cb_menu")],
|
||
])
|
||
await self.send_message(msg, reply_markup=keyboard)
|
||
|
||
|
||
def run_standalone(self):
|
||
"""Run standalone Telegram interactive polling loop using run_polling()."""
|
||
if not self._enabled:
|
||
logger.error("Telegram bot is not enabled. Check TELEGRAM_BOT_TOKEN.")
|
||
return
|
||
|
||
app = ApplicationBuilder().token(self.bot_token).build()
|
||
self._app = app
|
||
|
||
# Register command handlers
|
||
app.add_handler(CommandHandler(["start", "menu"], self._handle_start))
|
||
app.add_handler(CommandHandler("status", self._handle_status))
|
||
app.add_handler(CommandHandler(["mode", "setmode"], self._handle_mode))
|
||
app.add_handler(CommandHandler(["positions", "pos"], self._handle_positions))
|
||
app.add_handler(CommandHandler("trades", self._handle_trades))
|
||
app.add_handler(CommandHandler("pnl", self._handle_pnl))
|
||
app.add_handler(CommandHandler("news", self._handle_news))
|
||
app.add_handler(CommandHandler("regime", self._handle_regime))
|
||
app.add_handler(CommandHandler("learning", self._handle_learning))
|
||
app.add_handler(CommandHandler("price", self._handle_price))
|
||
app.add_handler(CommandHandler("pause", self._handle_pause))
|
||
app.add_handler(CommandHandler("resume", self._handle_resume))
|
||
app.add_handler(CommandHandler("closeall", self._handle_confirm_closeall))
|
||
app.add_handler(CommandHandler("setlot", self._handle_set_lot))
|
||
app.add_handler(CommandHandler("setrisk", self._handle_set_risk))
|
||
app.add_handler(CommandHandler("help", self._handle_help))
|
||
|
||
# Register callback button router
|
||
app.add_handler(CallbackQueryHandler(self._callback_router))
|
||
|
||
async def post_init(application):
|
||
logger.success("⚡ Telegram Bot is actively listening for your commands and button clicks!")
|
||
try:
|
||
from telegram import BotCommand
|
||
cmds = [
|
||
BotCommand("start", "Command Center & Main Menu"),
|
||
BotCommand("mode", "Switch Trading Profile (Safe / Moderate / Aggressive)"),
|
||
BotCommand("status", "Account Balance, Equity & Health"),
|
||
BotCommand("positions", "Open Positions & Floating P&L"),
|
||
BotCommand("pnl", "Daily & Monthly P&L Performance"),
|
||
BotCommand("trades", "Recent Trade History"),
|
||
BotCommand("news", "Macro News & Gemini Sentiment"),
|
||
BotCommand("regime", "Market Regime & Active Strategies"),
|
||
BotCommand("learning", "Autonomous Self-Learning Metrics"),
|
||
BotCommand("price", "Live Gold Price & Spread"),
|
||
BotCommand("pause", "Pause Automatic Order Execution"),
|
||
BotCommand("resume", "Resume Automatic Order Execution"),
|
||
BotCommand("setlot", "Set Default Lot Size (e.g. /setlot 0.02)"),
|
||
BotCommand("setrisk", "Set Risk % Per Trade (e.g. /setrisk 2.0)"),
|
||
BotCommand("closeall", "Emergency Close All Positions"),
|
||
BotCommand("help", "Command Cheatsheet & Documentation"),
|
||
]
|
||
await application.bot.set_my_commands(cmds)
|
||
logger.info("⚡ Registered native Telegram Menu commands via set_my_commands")
|
||
except Exception as e:
|
||
logger.warning(f"Could not set menu commands: {e}")
|
||
try:
|
||
await application.bot.send_message(
|
||
chat_id=self.chat_id,
|
||
text="⚡ <b>XAUUSD AI Trading Bot — Command Center Online!</b>\n\n"
|
||
"Your bot is actively listening with ZERO-LATENCY response.\n"
|
||
"Tap any button below to view status or control trading:",
|
||
reply_markup=self._get_main_keyboard(),
|
||
parse_mode="HTML"
|
||
)
|
||
except Exception as e:
|
||
logger.warning(f"Could not send welcome message: {e}")
|
||
|
||
app.post_init = post_init
|
||
logger.info("Starting Telegram Bot real-time polling (poll_interval=0.0s for instant response)...")
|
||
app.run_polling(poll_interval=0.0, drop_pending_updates=False)
|