#!/usr/bin/env python3 """ Core Telegram Bot - Handles only bot setup, authentication, and basic messaging. """ import asyncio import logging import telegram # Import telegram to check version from datetime import datetime from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import Application, ContextTypes, CommandHandler, CallbackQueryHandler, MessageHandler, filters from src.config.config import Config from src.trading.trading_engine import TradingEngine from src.monitoring.market_monitor import MarketMonitor from src.notifications.notification_manager import NotificationManager from src.commands.trading_commands import TradingCommands from src.commands.info_commands import InfoCommands from src.commands.management_commands import ManagementCommands logger = logging.getLogger(__name__) class TelegramTradingBot: """Core Telegram bot handling only authentication, messaging, and command routing.""" def __init__(self): """Initialize the core bot with minimal responsibilities.""" # Core bot attributes self.application = None self.version = "Unknown" # Initialize subsystems self.trading_engine = TradingEngine() self.notification_manager = NotificationManager() self.market_monitor = MarketMonitor(self.trading_engine, self.notification_manager) # Initialize command handlers self.info_commands = InfoCommands(self.trading_engine) self.management_commands = ManagementCommands(self.trading_engine, self.market_monitor) # Pass info and management command handlers to TradingCommands self.trading_commands = TradingCommands(self.trading_engine, self.notification_manager, info_commands_handler=self.info_commands, management_commands_handler=self.management_commands) def is_authorized(self, chat_id: str) -> bool: """Check if the chat ID is authorized to use the bot.""" authorized = str(chat_id) == str(Config.TELEGRAM_CHAT_ID) logger.info(f"Authorization check: Incoming chat_id='{chat_id}', Configured TELEGRAM_CHAT_ID='{Config.TELEGRAM_CHAT_ID}', Authorized={authorized}") return authorized async def send_message(self, text: str, parse_mode: str = 'HTML') -> None: """Send a message to the authorized chat.""" if self.application and Config.TELEGRAM_CHAT_ID: try: await self.application.bot.send_message( chat_id=Config.TELEGRAM_CHAT_ID, text=text, parse_mode=parse_mode ) except Exception as e: logger.error(f"Failed to send message: {e}") def setup_handlers(self): """Set up command handlers for the bot.""" if not self.application: return # Basic bot commands (directly on application for v20.x) self.application.add_handler(CommandHandler("start", self.start_command)) self.application.add_handler(CommandHandler("help", self.help_command)) # Trading commands self.application.add_handler(CommandHandler("long", self.trading_commands.long_command)) self.application.add_handler(CommandHandler("short", self.trading_commands.short_command)) self.application.add_handler(CommandHandler("exit", self.trading_commands.exit_command)) self.application.add_handler(CommandHandler("sl", self.trading_commands.sl_command)) self.application.add_handler(CommandHandler("tp", self.trading_commands.tp_command)) self.application.add_handler(CommandHandler("coo", self.trading_commands.coo_command)) # Info commands self.application.add_handler(CommandHandler("balance", self.info_commands.balance_command)) self.application.add_handler(CommandHandler("positions", self.info_commands.positions_command)) self.application.add_handler(CommandHandler("orders", self.info_commands.orders_command)) self.application.add_handler(CommandHandler("stats", self.info_commands.stats_command)) self.application.add_handler(CommandHandler("trades", self.info_commands.trades_command)) self.application.add_handler(CommandHandler("cycles", self.info_commands.cycles_command)) self.application.add_handler(CommandHandler("market", self.info_commands.market_command)) self.application.add_handler(CommandHandler("price", self.info_commands.price_command)) self.application.add_handler(CommandHandler("performance", self.info_commands.performance_command)) self.application.add_handler(CommandHandler("daily", self.info_commands.daily_command)) self.application.add_handler(CommandHandler("weekly", self.info_commands.weekly_command)) self.application.add_handler(CommandHandler("monthly", self.info_commands.monthly_command)) self.application.add_handler(CommandHandler("risk", self.info_commands.risk_command)) self.application.add_handler(CommandHandler("balance_adjustments", self.info_commands.balance_adjustments_command)) self.application.add_handler(CommandHandler("commands", self.info_commands.commands_command)) self.application.add_handler(CommandHandler("c", self.info_commands.commands_command)) # Alias # Management commands self.application.add_handler(CommandHandler("monitoring", self.management_commands.monitoring_command)) self.application.add_handler(CommandHandler("alarm", self.management_commands.alarm_command)) self.application.add_handler(CommandHandler("logs", self.management_commands.logs_command)) self.application.add_handler(CommandHandler("debug", self.management_commands.debug_command)) self.application.add_handler(CommandHandler("version", self.management_commands.version_command)) self.application.add_handler(CommandHandler("keyboard", self.management_commands.keyboard_command)) # Callback and message handlers self.application.add_handler(CallbackQueryHandler(self.trading_commands.button_callback)) async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /start command.""" logger.info(f"/start command triggered by chat_id: {update.effective_chat.id}") logger.debug(f"Full Update object in start_command: {update}") chat_id = update.effective_chat.id if not self.is_authorized(chat_id): logger.warning(f"Unauthorized access attempt by chat_id: {chat_id} in /start.") try: await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") except Exception as e: logger.error(f"Error sending unauthorized message in /start: {e}") return # Determine risk management and stop loss details from Config risk_enabled = getattr(Config, 'RISK_MANAGEMENT_ENABLED', False) stop_loss_percentage = getattr(Config, 'STOP_LOSS_PERCENTAGE', 0) bot_heartbeat = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10) welcome_text = f""" 🤖 <b>Welcome to Hyperliquid Trading Bot v{self.version}</b> 📱 <b>Quick Actions:</b> • Trading: /long {Config.DEFAULT_TRADING_TOKEN} 100 or /short {Config.DEFAULT_TRADING_TOKEN} 50 • Exit: /exit {Config.DEFAULT_TRADING_TOKEN} (closes position) • Info: /balance, /positions, /orders 📊 <b>Market Data:</b> • /market - Detailed market overview • /price - Quick price check <b>⚡ Quick Commands:</b> • /balance - Account balance • /positions - Open positions • /orders - Active orders • /market - Market data & prices <b>🚀 Trading:</b> • /long {Config.DEFAULT_TRADING_TOKEN} 100 - Long position • /long {Config.DEFAULT_TRADING_TOKEN} 100 45000 - Limit order • /long {Config.DEFAULT_TRADING_TOKEN} 100 sl:44000 - With stop loss • /short {Config.DEFAULT_TRADING_TOKEN} 50 - Short position • /short {Config.DEFAULT_TRADING_TOKEN} 50 3500 sl:3600 - With stop loss • /exit {Config.DEFAULT_TRADING_TOKEN} - Close position • /coo {Config.DEFAULT_TRADING_TOKEN} - Cancel open orders <b>🛡️ Risk Management:</b> • Enabled: {'✅ Yes' if risk_enabled else '❌ No'} • Auto Stop Loss: {stop_loss_percentage}% • Order Stop Loss: Use sl:price parameter • /sl {Config.DEFAULT_TRADING_TOKEN} 44000 - Manual stop loss • /tp {Config.DEFAULT_TRADING_TOKEN} 50000 - Take profit order <b>📈 Performance & Analytics:</b> • /stats - Complete trading statistics • /performance - Token performance ranking & detailed stats • /daily - Daily performance (last 10 days) • /weekly - Weekly performance (last 10 weeks) • /monthly - Monthly performance (last 10 months) • /risk - Sharpe ratio, drawdown, VaR • /version - Bot version & system information • /trades - Recent trade history <b>🔔 Price Alerts:</b> • /alarm - List all active alarms • /alarm {Config.DEFAULT_TRADING_TOKEN} 50000 - Set alarm for {Config.DEFAULT_TRADING_TOKEN} at $50,000 • /alarm {Config.DEFAULT_TRADING_TOKEN} - Show all {Config.DEFAULT_TRADING_TOKEN} alarms • /alarm 3 - Remove alarm ID 3 <b>🔄 Automatic Monitoring:</b> • Real-time order fill alerts • Position opened/closed notifications • P&L calculations on trade closure • Price alarm triggers • External trade detection & sync • Auto stats synchronization • Automatic stop loss placement • {bot_heartbeat}-second monitoring interval <b>📊 Universal Trade Tracking:</b> • Bot trades: Full logging & notifications • Platform trades: Auto-detected & synced • Mobile app trades: Monitored & recorded • API trades: Tracked & included in stats Type /help for detailed command information. <b>🔄 Order Monitoring:</b> • /monitoring - View monitoring status • /logs - View log file statistics and cleanup <b>⚙️ Configuration:</b> • Default Token: {Config.DEFAULT_TRADING_TOKEN} • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'} <b>🛡️ Safety Features:</b> • All trades logged automatically • Comprehensive performance tracking • Real-time balance monitoring • Risk metrics calculation • Automatic stop loss protection <b>📱 Mobile Optimized:</b> • Quick action buttons via /commands • Instant notifications • Clean, readable layout <b>💡 Quick Access:</b> • /commands or /c - One-tap button menu for all commands For support, contact your bot administrator. """ logger.debug(f"In /start, update.message is: {update.message}, update.effective_chat.id is: {chat_id}") try: await context.bot.send_message(chat_id=chat_id, text=welcome_text, parse_mode='HTML') except Exception as e: logger.error(f"Error sending welcome message in /start: {e}") async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /help command.""" logger.info(f"/help command triggered by chat_id: {update.effective_chat.id}") logger.debug(f"Full Update object in help_command: {update}") chat_id = update.effective_chat.id if not self.is_authorized(chat_id): logger.warning(f"Unauthorized access attempt by chat_id: {chat_id} in /help.") try: await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") except Exception as e: logger.error(f"Error sending unauthorized message in /help: {e}") return help_text = """ 📖 <b>Complete Command Reference</b> 🔄 <b>Trading Commands:</b> • /long [token] [USDC] [price] [sl:price] - Open long position • /short [token] [USDC] [price] [sl:price] - Open short position • /exit [token] - Close position (market order) • /sl [token] [price] - Set stop loss order • /tp [token] [price] - Set take profit order • /coo [token] - Cancel all open orders 📊 <b>Account Info:</b> • /balance - Account balance and equity • /positions - Open positions with P&L • /orders - Active orders • /trades - Recent trade history • /stats - Comprehensive trading statistics 📈 <b>Market Data:</b> • /market [token] - Market data and orderbook • /price [token] - Quick price check ⚙️ <b>Management:</b> • /monitoring - View/toggle order monitoring • /alarm [token] [price] - Set price alerts • /logs - View recent bot logs • /debug - Bot internal state (troubleshooting) For support or issues, check the logs or contact the administrator. """ logger.debug(f"In /help, update.message is: {update.message}, update.effective_chat.id is: {chat_id}") try: await context.bot.send_message(chat_id=chat_id, text=help_text, parse_mode='HTML') except Exception as e: logger.error(f"Error sending help message in /help: {e}") async def run(self): """Run the Telegram bot with manual initialization and shutdown (v20.x style).""" if not Config.TELEGRAM_BOT_TOKEN: logger.error("❌ TELEGRAM_BOT_TOKEN not configured") return if not Config.TELEGRAM_CHAT_ID: logger.error("❌ TELEGRAM_CHAT_ID not configured") return logger.info(f"🔧 Using python-telegram-bot version: {telegram.__version__} (Running in v20.x style)") # Create application self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build() # Connect notification manager to the bot application self.notification_manager.set_bot_application(self.application) # Set up handlers self.setup_handlers() keep_running_future = asyncio.Future() # Future to keep the bot alive try: logger.info("🚀 Initializing bot application (v20.x style)...") await self.application.initialize() logger.info(f"🚀 Starting Telegram trading bot v{self.version} (v20.x style)...") await self.send_message( f"🤖 <b>Manual Trading Bot v{self.version} Started (v20.x style)</b>\n\n" f"✅ Connected to Hyperliquid {('Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet')}\n" f"📊 Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n" f"🔄 All systems ready!\n\n" "Use /start for quick actions or /help for all commands." ) await self.market_monitor.start() logger.info("▶️ Starting PTB application's internal tasks (update processing, job queue).") await self.application.start() # This is non-blocking and starts the Application's processing loop. if self.application.updater: logger.info(f"▶️ Activating PTB updater to fetch updates (drop_pending_updates={Config.TELEGRAM_DROP_PENDING_UPDATES}).") # updater.start_polling is an async method that starts fetching updates and populates the application's update_queue. # It needs to run as a background task, managed by the existing event loop. # We don't await it directly here if we want other code (like keep_running_future) to proceed. # However, for graceful shutdown, we'll manage its lifecycle. # For this structure, we expect the main script to keep the event loop running. await self.application.updater.start_polling(drop_pending_updates=Config.TELEGRAM_DROP_PENDING_UPDATES) else: logger.error("❌ Critical: Application updater is not initialized. Bot cannot receive Telegram updates.") # If updater is critical, we might want to stop here or raise an error. # For now, we'll let keep_running_future potentially handle the stop. if not keep_running_future.done(): keep_running_future.set_exception(RuntimeError("Updater not available")) logger.info("✅ Bot is initialized and updater is polling. Awaiting stop signal via keep_running_future or Ctrl+C.") await keep_running_future except (KeyboardInterrupt, SystemExit) as e: # Added SystemExit here logger.info(f"🛑 Bot run interrupted by {type(e).__name__}. Initiating shutdown (v20.x style)...") if not keep_running_future.done(): keep_running_future.set_exception(e if isinstance(e, SystemExit) else KeyboardInterrupt()) except asyncio.CancelledError: logger.info("🛑 Bot run task cancelled. Initiating shutdown (v20.x style)...") if not keep_running_future.done(): keep_running_future.cancel() except Exception as e: logger.error(f"❌ Unhandled error in bot run loop (v20.x style): {e}", exc_info=True) if not keep_running_future.done(): keep_running_future.set_exception(e) finally: logger.info("🔌 Starting graceful shutdown sequence in TelegramTradingBot.run (v20.x style)...") try: logger.info("Stopping market monitor...") await self.market_monitor.stop() logger.info("Market monitor stopped.") if self.application: # Stop the updater first if it's running if self.application.updater and self.application.updater.running: logger.info("Stopping PTB updater polling...") await self.application.updater.stop() logger.info("PTB updater polling stopped.") # Then stop the application's own processing if self.application.running: # Check if application was started logger.info("Stopping PTB application components (handlers, job queue)...") await self.application.stop() logger.info("PTB application components stopped.") # Finally, shutdown the application logger.info("Shutting down PTB application (bot, persistence, etc.)...") await self.application.shutdown() logger.info("PTB application shut down.") else: logger.warning("Application object was None during shutdown in TelegramTradingBot.run.") logger.info("✅ Graceful shutdown sequence in TelegramTradingBot.run (v20.x style) complete.") except Exception as e: logger.error(f"💥 Error during shutdown sequence in TelegramTradingBot.run (v20.x style): {e}", exc_info=True)