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