#!/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.management_commands import ManagementCommands from src.commands.info.balance import BalanceCommands from src.commands.info.positions import PositionsCommands from src.commands.info.orders import OrdersCommands from src.commands.info.stats import StatsCommands from src.commands.info.trades import TradesCommands from src.commands.info.market import MarketCommands from src.commands.info.performance import PerformanceCommands from src.commands.info.daily import DailyCommands from src.commands.info.weekly import WeeklyCommands from src.commands.info.monthly import MonthlyCommands from src.commands.info.risk import RiskCommands from src.commands.info.price import PriceCommands from src.commands.info.balance_adjustments import BalanceAdjustmentsCommands from src.commands.info.commands import CommandsInfo 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) # 🆕 WIRE UP: Connect market monitor to trading engine for cached data sharing self.trading_engine.set_market_monitor(self.market_monitor) # Initialize command handlers self.management_commands = ManagementCommands(self.trading_engine, self.market_monitor) # Instantiate new info command classes self.balance_cmds = BalanceCommands(self.trading_engine, self.notification_manager) self.positions_cmds = PositionsCommands(self.trading_engine, self.notification_manager) self.orders_cmds = OrdersCommands(self.trading_engine, self.notification_manager) self.stats_cmds = StatsCommands(self.trading_engine, self.notification_manager) self.trades_cmds = TradesCommands(self.trading_engine, self.notification_manager) self.market_cmds = MarketCommands(self.trading_engine, self.notification_manager) self.performance_cmds = PerformanceCommands(self.trading_engine, self.notification_manager) self.daily_cmds = DailyCommands(self.trading_engine, self.notification_manager) self.weekly_cmds = WeeklyCommands(self.trading_engine, self.notification_manager) self.monthly_cmds = MonthlyCommands(self.trading_engine, self.notification_manager) self.risk_cmds = RiskCommands(self.trading_engine, self.notification_manager) self.price_cmds = PriceCommands(self.trading_engine, self.notification_manager) self.balance_adjustments_cmds = BalanceAdjustmentsCommands(self.trading_engine, self.notification_manager) self.commands_cmds = CommandsInfo(self.trading_engine, self.notification_manager) # Create a class to hold all info commands class InfoCommandsHandler: def __init__(self, bot): self.bot = bot self.balance_command = bot.balance_cmds.balance_command self.positions_command = bot.positions_cmds.positions_command self.orders_command = bot.orders_cmds.orders_command self.stats_command = bot.stats_cmds.stats_command self.price_command = bot.price_cmds.price_command self.market_command = bot.market_cmds.market_command self.performance_command = bot.performance_cmds.performance_command self.daily_command = bot.daily_cmds.daily_command self.weekly_command = bot.weekly_cmds.weekly_command self.monthly_command = bot.monthly_cmds.monthly_command self.trades_command = bot.trades_cmds.trades_command self.risk_command = bot.risk_cmds.risk_command self.info_commands = InfoCommandsHandler(self) # Pass info and management command handlers to TradingCommands self.trading_commands = TradingCommands( self.trading_engine, self.notification_manager, management_commands_handler=self.management_commands, info_commands_handler=self.info_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.balance_cmds.balance_command)) self.application.add_handler(CommandHandler("positions", self.positions_cmds.positions_command)) self.application.add_handler(CommandHandler("orders", self.orders_cmds.orders_command)) self.application.add_handler(CommandHandler("stats", self.stats_cmds.stats_command)) self.application.add_handler(CommandHandler("trades", self.trades_cmds.trades_command)) self.application.add_handler(CommandHandler("market", self.market_cmds.market_command)) self.application.add_handler(CommandHandler("price", self.price_cmds.price_command)) self.application.add_handler(CommandHandler("performance", self.performance_cmds.performance_command)) self.application.add_handler(CommandHandler("daily", self.daily_cmds.daily_command)) self.application.add_handler(CommandHandler("weekly", self.weekly_cmds.weekly_command)) self.application.add_handler(CommandHandler("monthly", self.monthly_cmds.monthly_command)) self.application.add_handler(CommandHandler("risk", self.risk_cmds.risk_command)) self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_cmds.balance_adjustments_command)) self.application.add_handler(CommandHandler("commands", self.commands_cmds.commands_command)) self.application.add_handler(CommandHandler("c", self.commands_cmds.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""" 🤖 Welcome to Hyperliquid Trading Bot v{self.version} 📱 Quick Actions: • 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 �� Market Data: • /market - Detailed market overview • /price - Quick price check ⚡ Quick Commands: • /balance - Account balance • /positions - Open positions • /orders - Active orders • /market - Market data & prices 🚀 Trading: • /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 🛡️ Risk Management: • Enabled: {'✅ Yes' if risk_enabled else '❌ No'} • Auto Stop Loss: {stop_loss_percentage}% ROE • 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 📈 Performance & Analytics: • /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 🔔 Price Alerts: • /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 🔄 Automatic Monitoring: • 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 📊 Universal Trade Tracking: • 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. 🔄 Order Monitoring: • /monitoring - View monitoring status • /logs - View log file statistics and cleanup ⚙️ Configuration: • Default Token: {Config.DEFAULT_TRADING_TOKEN} • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'} 🛡️ Safety Features: • All trades logged automatically • Comprehensive performance tracking • Real-time balance monitoring • Risk metrics calculation • Automatic stop loss protection 📱 Mobile Optimized: • Quick action buttons via /commands • Instant notifications • Clean, readable layout 💡 Quick Access: • /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 = """ 📖 Complete Command Reference 🔄 Trading Commands: • /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 📊 Account Info: • /balance - Account balance and equity • /positions - Open positions with P&L • /orders - Active orders • /trades - Recent trade history • /stats - Comprehensive trading statistics 📈 Market Data: • /market [token] - Market data and orderbook • /price [token] - Quick price check ⚙️ Management: • /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"🤖 Manual Trading Bot v{self.version} Started (v20.x style)\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)