123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- #!/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"""
- 🤖 <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}% 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
- <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()
- 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.
- logger.info(f"▶️ Starting polling (drop_pending_updates={Config.TELEGRAM_DROP_PENDING_UPDATES})...")
- await self.application.run_polling(drop_pending_updates=Config.TELEGRAM_DROP_PENDING_UPDATES)
- logger.info("✅ Bot is initialized and polling. Awaiting stop signal or Ctrl+C.")
- # No need for keep_running_future with run_polling
-
- except (KeyboardInterrupt, SystemExit) as e: # Added SystemExit here
- logger.info(f"🛑 Bot run interrupted by {type(e).__name__}. Initiating shutdown (v20.x style)...")
- except asyncio.CancelledError:
- logger.info("🛑 Bot run task cancelled. Initiating shutdown (v20.x style)...")
- except Exception as e:
- logger.error(f"❌ Unhandled error in bot run loop (v20.x style): {e}", exc_info=True)
- 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 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)
|