Upgrade Telegram Bot to 2-way interactive Command Center with inline buttons, remote trade control, and quick launcher batch scripts

This commit is contained in:
Mazonia
2026-09-21 01:13:49 +00:00
parent ae9b15f773
commit b220e0f499
6 changed files with 798 additions and 79 deletions
+13 -2
View File
@@ -85,11 +85,13 @@ class TradingBot:
self.news_analyzer = NewsAnalyzer()
# ── Notifications ────────────────────────────────────────────────
self.telegram = TelegramNotifier()
self.telegram = TelegramNotifier(bot_instance=self)
# ── State ────────────────────────────────────────────────────────
self._cycle_count = 0
self._last_news_check: Optional[datetime] = None
self._last_regime = None
self._trading_paused = False
def _setup_logging(self):
"""Configure loguru logging."""
@@ -158,6 +160,9 @@ class TradingBot:
f"Mode: {mode}\nBalance: {balance_str}\nSymbol: XAUUSD"
)
# Start interactive 2-way Telegram polling in background
await self.telegram.start_polling()
# Register shutdown handlers
self._running = True
@@ -176,6 +181,7 @@ class TradingBot:
"""Gracefully stop the bot."""
logger.info("Shutting down...")
self._running = False
await self.telegram.stop_polling()
self.mt5.disconnect()
logger.info("Bot stopped successfully")
@@ -213,6 +219,7 @@ class TradingBot:
df_h4_analyzed = self.technical.add_all_indicators(df_h4.copy()) if df_h4 is not None else None
if df_h4_analyzed is not None:
regime = self.regime_detector.analyze(df_h4_analyzed)
self._last_regime = regime
else:
regime = None
@@ -229,7 +236,11 @@ class TradingBot:
best_signal = None
best_confluence = None
for strategy_name, strategy in self.strategies.items():
# Skip strategy execution if paused via Telegram remote control
if getattr(self, "_trading_paused", False):
best_signal = None
else:
for strategy_name, strategy in self.strategies.items():
if not strategy.enabled:
continue
+769 -77
View File
@@ -1,89 +1,770 @@
"""
Telegram Bot — Notifications & Remote Control
Interactive Telegram Bot — 2-Way Command & Control Center
Sends alerts for:
- Trade entries/exits
- Daily P&L summaries
- High-impact news
- Bot status updates
Commands:
- /status — Bot status + open positions
- /trades — Recent trade history
- /pnl — Current P&L
- /pause / /resume — Control bot
- /risk — Current risk exposure
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, timezone
from typing import Optional
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:
"""
Sends formatted trading alerts and summaries via Telegram.
Unified 2-way Telegram bot for automated trading alerts and interactive remote control.
"""
def __init__(self):
settings = get_settings()
self.bot_token = settings.telegram.bot_token
self.chat_id = settings.telegram.chat_id
self._bot = None
self._enabled = bool(self.bot_token and self.chat_id)
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
self._app: Optional[Application] = None
self._is_polling = False
self._enabled = bool(self.bot_token and self.chat_id and self.chat_id != "0")
if not self._enabled:
logger.warning("Telegram not configured — notifications disabled")
logger.warning("Telegram bot not configured or chat ID missing — remote control disabled")
def _get_bot(self):
"""Lazy-load the Telegram bot."""
if self._bot is None and self._enabled:
try:
from telegram import Bot
self._bot = Bot(token=self.bot_token)
except Exception as e:
logger.error(f"Failed to initialize Telegram bot: {e}")
return self._bot
def set_bot_instance(self, bot_instance: Any):
"""Link the parent TradingBot instance for live data and control."""
self.bot_instance = bot_instance
async def send_message(self, text: str, parse_mode: str = "HTML"):
"""Send a message to the configured chat."""
if not self._enabled:
def _is_authorized(self, update: Update) -> bool:
"""Verify message sender matches the authorized Telegram chat ID."""
if not update.effective_chat:
return False
sender_id = str(update.effective_chat.id)
if sender_id != self.chat_id:
logger.warning(f"Unauthorized Telegram access attempt from Chat ID: {sender_id}")
return False
return True
# ── 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")
)
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("🏷️ Gold Price & Spread", callback_data="cb_price"),
pause_btn,
],
[
InlineKeyboardButton("🛑 EMERGENCY CLOSE ALL", callback_data="cb_confirm_closeall"),
InlineKeyboardButton("🔄 Refresh Menu", callback_data="cb_menu"),
],
]
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._is_authorized(update):
await update.effective_message.reply_text("⛔ Unauthorized access.")
return
bot = self._get_bot()
if not bot:
mode = "DEMO" if self.settings.demo_mode else "LIVE"
text = (
f"⚡ <b>XAUUSD AI TRADING BOT — COMMAND CENTER</b> ⚡\n\n"
f"<b>Status:</b> 🟢 ONLINE ({mode})\n"
f"<b>Symbol:</b> <code>{self.settings.symbol}</code>\n"
f"<b>Lot Size:</b> <code>{self.settings.trading_params.get('risk', {}).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 update.effective_message.reply_text(
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"):
account = self.bot_instance.mt5.get_account_info()
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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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 = []
if self.bot_instance and hasattr(self.bot_instance, "mt5"):
positions = self.bot_instance.mt5.get_open_positions(self.settings.symbol)
keyboard_rows = []
if not positions:
msg = (
f"📈 <b>OPEN POSITIONS</b>\n"
f"{'━' * 25}\n\n"
f"<i>No open positions currently active on {self.settings.symbol}.</i>\n"
f"The bot is scanning market regimes and waiting for high-confluence entry signals."
)
else:
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("open_price", 0.0)
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"
)
# Add action button for each position
keyboard_rows.append([
InlineKeyboardButton(f"❌ Close #{ticket} (${profit:+,.1f})", callback_data=f"cb_close_{ticket}")
])
keyboard_rows.append([
InlineKeyboardButton("🔄 Refresh Positions", callback_data="cb_positions"),
InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu"),
])
reply_markup = InlineKeyboardMarkup(keyboard_rows)
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=reply_markup, parse_mode="HTML")
else:
await update.effective_message.reply_text(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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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=6)
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 6H Sentiment:</b> {sent_emoji} (<code>{sentiment_score:+.2f}</code>)\n"
f"<b>AI Engine:</b> Gemini 3.6 Flash + FinBERT\n\n"
)
# Pull top news events from DB
session = crud.get_session()
try:
from database.models import NewsEvent
events = session.query(NewsEvent).order_by(NewsEvent.created_at.desc()).limit(3).all()
if events:
msg += "<b>Latest Macro Analysis:</b>\n"
for ev in events:
impact_emoji = "🔴" if ev.impact_level == "HIGH" else "🟡"
msg += (
f"{impact_emoji} <b>{ev.headline[:75]}...</b>\n"
f" Sentiment: {ev.sentiment} ({ev.sentiment_score:+.2f})\n"
)
if ev.gemini_analysis:
msg += f" <i>AI Note:</i> {ev.gemini_analysis[:100]}...\n\n"
else:
msg += "<i>No recent news cached. The bot queries Finnhub and Alpha Vantage every 15 minutes.</i>\n"
finally:
session.close()
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("🔄 Refresh News", callback_data="cb_news")],
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
async def _handle_regime(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /regime command or button."""
if not self._is_authorized(update):
return
msg = (
f"🧠 <b>MARKET REGIME & ACTIVE STRATEGIES</b>\n"
f"{'━' * 28}\n\n"
)
if self.bot_instance and hasattr(self.bot_instance, "regime_detector"):
regime = getattr(self.bot_instance, "_last_regime", None)
if regime:
msg += (
f"<b>Current Regime:</b> <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>Recommended Strategies:</b>\n"
)
for s in regime.recommended_strategies:
msg += f" • <code>{s}</code>\n"
else:
msg += "<i>Regime analysis will be updated after the next H4 candle cycle.</i>\n"
else:
msg += "<i>Bot instance offline or warming up.</i>\n"
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("🔙 Back to Main Menu", callback_data="cb_menu")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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"):
tick = self.bot_instance.mt5.get_current_tick(self.settings.symbol)
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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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
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")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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
closed_count = 0
if self.bot_instance and hasattr(self.bot_instance, "trade_executor"):
closed_count = self.bot_instance.trade_executor.close_all_positions()
elif self.bot_instance and hasattr(self.bot_instance, "mt5"):
closed_count = self.bot_instance.mt5.close_all_positions(self.settings.symbol)
msg = (
f"🛑 <b>ALL POSITIONS CLOSED</b>\n\n"
f"Successfully closed <b>{closed_count}</b> open position(s).\n"
f"All trades have been flattened."
)
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("📊 View Status", callback_data="cb_status")],
[InlineKeyboardButton("🔙 Main Menu", callback_data="cb_menu")],
])
if update.callback_query:
await update.callback_query.edit_message_text(text=msg, reply_markup=keyboard, parse_mode="HTML")
else:
await update.effective_message.reply_text(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"):
success = self.bot_instance.trade_executor.close_position(ticket, comment="Telegram manual close")
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 update.callback_query.edit_message_text(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 & Menus:</b>\n"
f"• <code>/start</code> or <code>/menu</code> — Open interactive button dashboard\n"
f"• <code>/help</code> — Show this 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>/price</code> — Real-time Gold Bid, Ask, Spread\n\n"
f"<b>Remote Control & Emergency:</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"
)
await update.effective_message.reply_text(msg, parse_mode="HTML")
async def _callback_router(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Route callback queries from inline buttons."""
query = update.callback_query
await query.answer()
data = query.data
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_regime":
await self._handle_regime(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_confirm_closeall":
await self._handle_confirm_closeall(update, context)
elif data == "cb_do_closeall":
await self._handle_do_closeall(update, context)
elif data.startswith("cb_close_"):
ticket = int(data.split("_")[-1])
await self._handle_close_single(update, ticket)
# ── 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(["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("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))
await self._app.initialize()
await self._app.start()
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:
logger.error(f"Telegram send error: {e}")
async def send_trade_opened(self, trade: dict):
"""Send trade entry notification."""
"""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> {trade['direction']}\n"
f"<b>Volume:</b> {trade['volume']} lots\n"
f"<b>Entry:</b> ${trade['entry_price']:,.2f}\n"
f"<b>Stop Loss:</b> ${trade['stop_loss']:,.2f}\n"
f"<b>Take Profit:</b> ${trade['take_profit']:,.2f}\n"
f"<b>Strategy:</b> {trade['strategy']}\n"
f"<b>Confluence:</b> {trade['confluence_score']:.0f}/100\n"
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M UTC')}"
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')}"
)
await self.send_message(msg)
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,
@@ -93,23 +774,24 @@ class TelegramNotifier:
entry: float,
exit_price: float,
duration_min: int = 0,
reason: str = "TP/SL",
):
"""Send trade close notification."""
emoji = "✅" if profit > 0 else "❌"
profit_color = "🟢" if profit > 0 else "🔴"
msg = (
f"{emoji} <b>TRADE CLOSED</b>\n\n"
f"<b>Ticket:</b> #{ticket}\n"
f"<b>Direction:</b> {direction}\n"
f"<b>Entry:</b> ${entry:,.2f}\n"
f"<b>Exit:</b> ${exit_price:,.2f}\n"
f"{profit_color} <b>P&L:</b> ${profit:+,.2f}\n"
f"<b>Duration:</b> {duration_min} min\n"
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M UTC')}"
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')}"
)
await self.send_message(msg)
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,
@@ -122,19 +804,22 @@ class TelegramNotifier:
):
"""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 SUMMARY</b>\n"
f"{'━' * 25}\n\n"
f"<b>Balance:</b> ${balance:,.2f}\n"
f"<b>Daily P&L:</b> ${daily_pnl:+,.2f}\n"
f"<b>Trades:</b> {total_trades}\n"
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> {win_rate:.1f}%\n\n"
f"📅 {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
f"<b>Win Rate:</b> <b>{win_rate:.1f}%</b>\n\n"
f"📅 Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
)
await self.send_message(msg)
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."""
@@ -142,23 +827,30 @@ class TelegramNotifier:
sentiment_emoji = {"BULLISH": "📈", "BEARISH": "📉", "NEUTRAL": "➡️"}.get(sentiment, "➡️")
msg = (
f"{impact_emoji} <b>NEWS ALERT — {impact} IMPACT</b>\n\n"
f"<b>Headline:</b> {headline}\n"
f"{sentiment_emoji} <b>Sentiment:</b> {sentiment} ({score:+.2f})\n"
f"<b>Time:</b> {datetime.now(timezone.utc).strftime('%H:%M UTC')}"
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."
)
await self.send_message(msg)
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 ALERT</b>\n\n{message}"
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>Bot Status:</b> {status}"
emoji = "🟢" if status == "RUNNING" else ("🔴" if status == "STOPPED" else "🟡")
msg = f"{emoji} <b>XAUUSD Bot Status: {status}</b>"
if details:
msg += f"\n{details}"
await self.send_message(msg)
msg += f"\n\n{details}"
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("📱 Open Menu", callback_data="cb_menu")],
])
await self.send_message(msg, reply_markup=keyboard)
+7
View File
@@ -0,0 +1,7 @@
@echo off
set VENV_PY=C:\Users\Mazonia\.gemini\antigravity-ide\scratch\XAUUSD-AI-Trading-Bot\venv\Scripts\python.exe
if exist "%VENV_PY%" (
"%VENV_PY%" main.py %*
) else (
python main.py %*
)
+3
View File
@@ -0,0 +1,3 @@
@echo off
call run.bat --backtest
pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
call run.bat --demo
pause
+3
View File
@@ -0,0 +1,3 @@
@echo off
call run.bat --dashboard
pause