123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- #!/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)
|