#!/usr/bin/env python3 """ Management Commands - Handles management and monitoring Telegram commands. """ import logging import os import platform import sys from datetime import datetime, timedelta from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes import json from src.config.config import Config from src.monitoring.alarm_manager import AlarmManager logger = logging.getLogger(__name__) class ManagementCommands: """Handles all management-related Telegram commands.""" def __init__(self, trading_engine, market_monitor): """Initialize with trading engine and market monitor.""" self.trading_engine = trading_engine self.market_monitor = market_monitor self.alarm_manager = AlarmManager() def _is_authorized(self, chat_id: str) -> bool: """Check if the chat ID is authorized.""" return str(chat_id) == str(Config.TELEGRAM_CHAT_ID) async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /monitoring command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return # Get alarm statistics alarm_stats = self.alarm_manager.get_statistics() # Get balance adjustments info stats = self.trading_engine.get_stats() adjustments_summary = stats.get_balance_adjustments_summary() if stats else { 'total_deposits': 0, 'total_withdrawals': 0, 'net_adjustment': 0, 'adjustment_count': 0 } # Safety checks for monitoring attributes monitoring_active = self.market_monitor.is_running status_text = f""" ๐Ÿ”„ System Monitoring Status ๐Ÿ“Š Order Monitoring: โ€ข Active: {'โœ… Yes' if monitoring_active else 'โŒ No'} โ€ข Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds โ€ข Market Monitor: {'โœ… Running' if monitoring_active else 'โŒ Stopped'} ๐Ÿ’ฐ Balance Tracking: โ€ข Total Adjustments: {adjustments_summary['adjustment_count']} โ€ข Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f} ๐Ÿ”” Price Alarms: โ€ข Active Alarms: {alarm_stats['total_active']} โ€ข Triggered Today: {alarm_stats['total_triggered']} โ€ข Tokens Monitored: {alarm_stats['tokens_tracked']} โ€ข Next Alarm ID: {alarm_stats['next_id']} ๐Ÿ”„ External Trade Monitoring: โ€ข Auto Stats Update: โœ… Enabled โ€ข External Notifications: โœ… Enabled ๐Ÿ›ก๏ธ Risk Management: โ€ข Automatic Stop Loss: {'โœ… Enabled' if hasattr(Config, 'RISK_MANAGEMENT_ENABLED') and Config.RISK_MANAGEMENT_ENABLED else 'โŒ Disabled'} โ€ข Order-based Stop Loss: โœ… Enabled ๐Ÿ“ˆ Notifications: โ€ข ๐Ÿš€ Position Opened/Increased โ€ข ๐Ÿ“‰ Position Partially/Fully Closed โ€ข ๐ŸŽฏ P&L Calculations โ€ข ๐Ÿ”” Price Alarm Triggers โ€ข ๐Ÿ”„ External Trade Detection โ€ข ๐Ÿ›‘ Order-based Stop Loss Placement ๐Ÿ’พ Bot State Persistence: โ€ข Trading Engine State: โœ… Saved โ€ข Order Tracking: โœ… Saved โ€ข State Survives Restarts: โœ… Yes โฐ Last Check: {datetime.now().strftime('%H:%M:%S')} ๐Ÿ’ก Monitoring Features: โ€ข Real-time order fill detection โ€ข Automatic P&L calculation โ€ข Position change tracking โ€ข Price alarm monitoring โ€ข External trade monitoring โ€ข Auto stats synchronization โ€ข Order-based stop loss placement โ€ข Instant Telegram notifications """ if alarm_stats['token_breakdown']: status_text += f"\n\n๐Ÿ“‹ Active Alarms by Token:\n" for token, count in alarm_stats['token_breakdown'].items(): status_text += f"โ€ข {token}: {count} alarm{'s' if count != 1 else ''}\n" await context.bot.send_message(chat_id=chat_id, text=status_text.strip(), parse_mode='HTML') async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /alarm command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return try: if not context.args or len(context.args) == 0: # No arguments - list all alarms alarms = self.alarm_manager.get_all_active_alarms() message = self.alarm_manager.format_alarm_list(alarms) await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML') return elif len(context.args) == 1: arg = context.args[0] # Check if argument is a number (alarm ID to remove) try: alarm_id = int(arg) # Remove alarm by ID if self.alarm_manager.remove_alarm(alarm_id): await context.bot.send_message(chat_id=chat_id, text=f"โœ… Alarm ID {alarm_id} has been removed.") else: await context.bot.send_message(chat_id=chat_id, text=f"โŒ Alarm ID {alarm_id} not found.") return except ValueError: # Not a number, treat as token token = arg.upper() alarms = self.alarm_manager.get_alarms_by_token(token) message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms") await context.bot.send_message(chat_id=chat_id, text=message, parse_mode='HTML') return elif len(context.args) == 2: # Set new alarm: /alarm TOKEN PRICE token = context.args[0].upper() target_price = float(context.args[1]) # Get current market price symbol = f"{token}/USDC:USDC" market_data = self.trading_engine.get_market_data(symbol) if not market_data or not market_data.get('ticker'): await context.bot.send_message(chat_id=chat_id, text=f"โŒ Could not fetch current price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await context.bot.send_message(chat_id=chat_id, text=f"โŒ Invalid current price for {token}") return # Create the alarm alarm = self.alarm_manager.create_alarm(token, target_price, current_price) # Format confirmation message direction_emoji = "๐Ÿ“ˆ" if alarm['direction'] == 'above' else "๐Ÿ“‰" price_diff = abs(target_price - current_price) price_diff_percent = (price_diff / current_price) * 100 message = f""" โœ… Price Alarm Created ๐Ÿ“Š Alarm Details: โ€ข Alarm ID: {alarm['id']} โ€ข Token: {token} โ€ข Target Price: ${target_price:,.2f} โ€ข Current Price: ${current_price:,.2f} โ€ข Direction: {alarm['direction'].upper()} {direction_emoji} Alert Condition: Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f} ๐Ÿ’ฐ Price Difference: โ€ข Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%) โ€ข Status: ACTIVE โœ… โฐ Created: {datetime.now().strftime('%H:%M:%S')} ๐Ÿ’ก The alarm will be checked every {Config.BOT_HEARTBEAT_SECONDS} seconds and you'll receive a notification when triggered. """ await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML') else: # Too many arguments await context.bot.send_message(chat_id=chat_id, text=( "โŒ Invalid usage. Examples:\n\n" "โ€ข /alarm - List all alarms\n" "โ€ข /alarm BTC - List BTC alarms\n" "โ€ข /alarm BTC 50000 - Set alarm for BTC at $50,000\n" "โ€ข /alarm 3 - Remove alarm ID 3" ), parse_mode='HTML') except ValueError: await context.bot.send_message(chat_id=chat_id, text="โŒ Invalid price format. Please use numbers only.") except Exception as e: error_message = f"โŒ Error processing alarm command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in alarm command: {e}") async def logs_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /logs command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return try: logs_dir = "logs" if not os.path.exists(logs_dir): await context.bot.send_message(chat_id=chat_id, text="๐Ÿ“œ No logs directory found.") return # Get log files log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')] if not log_files: await context.bot.send_message(chat_id=chat_id, text="๐Ÿ“œ No log files found.") return # Handle cleanup command if context.args and context.args[0].lower() == 'cleanup': days_to_keep = 30 # Default if len(context.args) > 1: try: days_to_keep = int(context.args[1]) except ValueError: await context.bot.send_message(chat_id=chat_id, text="โŒ Invalid number of days. Using default (30 days).") # Clean up old log files cutoff_date = datetime.now() - timedelta(days=days_to_keep) cleaned_files = 0 total_size_cleaned = 0 for log_file in log_files: file_path = os.path.join(logs_dir, log_file) file_stat = os.stat(file_path) file_date = datetime.fromtimestamp(file_stat.st_mtime) if file_date < cutoff_date: file_size = file_stat.st_size os.remove(file_path) cleaned_files += 1 total_size_cleaned += file_size size_cleaned_mb = total_size_cleaned / (1024 * 1024) await context.bot.send_message(chat_id=chat_id, text= f"๐Ÿงน Log cleanup complete.\n" f"โ€ข Files older than {days_to_keep} days removed.\n" f"โ€ข Total files deleted: {cleaned_files}\n" f"โ€ข Total size cleaned: {size_cleaned_mb:.2f} MB" ) return # Show log statistics total_size = 0 oldest_file = None newest_file = None recent_files = [] for log_file in sorted(log_files): file_path = os.path.join(logs_dir, log_file) file_stat = os.stat(file_path) file_size = file_stat.st_size file_date = datetime.fromtimestamp(file_stat.st_mtime) total_size += file_size if oldest_file is None or file_date < oldest_file[1]: oldest_file = (log_file, file_date) if newest_file is None or file_date > newest_file[1]: newest_file = (log_file, file_date) # Keep track of recent files if len(recent_files) < 5: recent_files.append((log_file, file_size, file_date)) logs_message = f""" ๐Ÿ“œ System Logging Status ๐Ÿ“ Log Directory: {logs_dir}/ โ€ข Total Files: {len(log_files)} โ€ข Total Size: {total_size / 1024 / 1024:.2f} MB โ€ข Oldest File: {oldest_file[0]} ({oldest_file[1].strftime('%m/%d/%Y')}) โ€ข Newest File: {newest_file[0]} ({newest_file[1].strftime('%m/%d/%Y')}) ๐Ÿ“‹ Recent Log Files: """ for log_file, file_size, file_date in reversed(recent_files): size_mb = file_size / 1024 / 1024 logs_message += f"โ€ข {log_file} ({size_mb:.2f} MB) - {file_date.strftime('%m/%d %H:%M')}\n" logs_message += f""" ๐Ÿ“Š Log Management: โ€ข Location: ./logs/ โ€ข Rotation: Daily โ€ข Retention: Manual cleanup available โ€ข Format: timestamp - module - level - message ๐Ÿงน Cleanup Commands: โ€ข /logs cleanup - Remove logs older than 30 days โ€ข /logs cleanup 7 - Remove logs older than 7 days ๐Ÿ’ก Log Levels: โ€ข INFO: Normal operations โ€ข ERROR: Error conditions โ€ข DEBUG: Detailed debugging (if enabled) """ await context.bot.send_message(chat_id=chat_id, text=logs_message.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing logs command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in logs command: {e}") async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /debug command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return try: # Get system information debug_info = f""" ๐Ÿ› Debug Information ๐Ÿ’ป System Info: โ€ข Python: {sys.version.split()[0]} โ€ข Platform: {platform.system()} {platform.release()} โ€ข Architecture: {platform.machine()} ๐Ÿ“Š Trading Engine: โ€ข Stats Available: {'โœ… Yes' if self.trading_engine.get_stats() else 'โŒ No'} โ€ข Client Connected: {'โœ… Yes' if self.trading_engine.client else 'โŒ No'} ๐Ÿ”„ Market Monitor: โ€ข Running: {'โœ… Yes' if self.market_monitor.is_running else 'โŒ No'} ๐Ÿ“ State Files: โ€ข Trading Engine State: {'โœ… Exists' if os.path.exists('data/trading_engine_state.json') else 'โŒ Missing'} โ€ข Price Alarms: {'โœ… Exists' if os.path.exists('data/price_alarms.json') else 'โŒ Missing'} โ€ข Trading Stats: {'โœ… Exists' if os.path.exists('data/trading_stats.json') else 'โŒ Missing'} ๐Ÿ”” Alarm Manager: โ€ข Active Alarms: {self.alarm_manager.get_statistics()['total_active']} โ€ข Triggered Alarms: {self.alarm_manager.get_statistics()['total_triggered']} โฐ Timestamps: โ€ข Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} โ€ข Debug Generated: {datetime.now().isoformat()} """ # Get current positions for debugging try: positions = self.trading_engine.get_positions() if positions: debug_info += f"\n๐Ÿ“ˆ Current Positions: {len(positions)} found\n" for pos in positions[:3]: # Show first 3 positions symbol = pos.get('symbol', 'Unknown').replace('/USDC:USDC', '') contracts = pos.get('contracts', 0) if float(contracts) != 0: debug_info += f" โ€ข {symbol}: {contracts} contracts\n" else: debug_info += "\n๐Ÿ“ˆ Positions: No positions found\n" except Exception as e: debug_info += f"\n๐Ÿ“ˆ Positions: Error fetching ({str(e)})\n" # Get balance for debugging try: balance = self.trading_engine.get_balance() if balance and balance.get('total'): usdc_balance = float(balance['total'].get('USDC', 0)) debug_info += f"\n๐Ÿ’ฐ USDC Balance: ${usdc_balance:,.2f}\n" else: debug_info += "\n๐Ÿ’ฐ Balance: No balance data\n" except Exception as e: debug_info += f"\n๐Ÿ’ฐ Balance: Error fetching ({str(e)})\n" await context.bot.send_message(chat_id=chat_id, text=debug_info.strip(), parse_mode='HTML') except Exception as e: logger.error(f"โŒ Error in debug command: {e}") await context.bot.send_message(chat_id=chat_id, text=f"โŒ Debug error: {e}") async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /version command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return try: # Get uptime info uptime_info = "Unknown" try: import psutil process = psutil.Process() create_time = datetime.fromtimestamp(process.create_time()) uptime = datetime.now() - create_time days = uptime.days hours, remainder = divmod(uptime.seconds, 3600) minutes, _ = divmod(remainder, 60) uptime_info = f"{days}d {hours}h {minutes}m" except ImportError: pass # Get stats info stats = self.trading_engine.get_stats() if stats: basic_stats = stats.get_basic_stats() else: basic_stats = {'total_trades': 0, 'completed_trades': 0, 'days_active': 0, 'start_date': 'Unknown'} version_text = f""" ๐Ÿค– Trading Bot Version & System Info ๐Ÿ“ฑ Bot Information: โ€ข Version: 2.1.2 โ€ข Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'} โ€ข Uptime: {uptime_info} โ€ข Default Token: {Config.DEFAULT_TRADING_TOKEN} ๐Ÿ’ป System Information: โ€ข Python: {sys.version.split()[0]} โ€ข Platform: {platform.system()} {platform.release()} โ€ข Architecture: {platform.machine()} ๐Ÿ“Š Trading Stats: โ€ข Total Orders: {basic_stats['total_trades']} โ€ข Completed Trades: {basic_stats['completed_trades']} โ€ข Days Active: {basic_stats['days_active']} โ€ข Start Date: {basic_stats['start_date']} ๐Ÿ”„ Monitoring Status: โ€ข Market Monitor: {'โœ… Active' if self.market_monitor.is_running else 'โŒ Inactive'} โ€ข External Trades: โœ… Active โ€ข Price Alarms: โœ… Active ({self.alarm_manager.get_statistics()['total_active']} active) โฐ Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} """ await context.bot.send_message(chat_id=chat_id, text=version_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing version command: {str(e)}" await context.bot.send_message(chat_id=chat_id, text=error_message) logger.error(f"Error in version command: {e}") async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /keyboard command to enable/show custom keyboard.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="โŒ Unauthorized access.") return # Check if custom keyboard is enabled in config keyboard_enabled = getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_ENABLED', False) if keyboard_enabled: # Create a simple reply keyboard with common commands keyboard = [ [KeyboardButton("/balance"), KeyboardButton("/positions")], [KeyboardButton("/orders"), KeyboardButton("/stats")], [KeyboardButton("/daily"), KeyboardButton("/performance")], [KeyboardButton("/help"), KeyboardButton("/commands")] ] reply_markup = ReplyKeyboardMarkup( keyboard, resize_keyboard=True, one_time_keyboard=False, selective=True ) await context.bot.send_message(chat_id=chat_id, text="โŒจ๏ธ Custom Keyboard Activated!\n\n๐ŸŽฏ Your quick buttons are now ready:\nโ€ข /balance - Account balance\nโ€ข /positions - Open positions\nโ€ข /orders - Active orders\nโ€ข /stats - Trading statistics\nโ€ข /daily - Daily performance\nโ€ข /performance - Performance stats\nโ€ข /help - Help guide\nโ€ข /commands - Command menu\n\n๐Ÿ’ก How to use:\nTap any button below to send the command instantly!\n\n๐Ÿ”ง These buttons will stay at the bottom of your chat.", reply_markup=reply_markup, parse_mode='HTML') else: await context.bot.send_message(chat_id=chat_id, text="โŒ Custom Keyboard Disabled\n\n๐Ÿ”ง To enable:\nโ€ข Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\nโ€ข Restart the bot\nโ€ข Run /keyboard again\n\n๐Ÿ“‹ Current config:\nโ€ข Enabled: {keyboard_enabled}\nโ€ข Layout: {getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', 'default')}", parse_mode='HTML')