#!/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, timezone from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes import json from typing import Dict, Any, List, Optional from src.config.config import Config from src.monitoring.alarm_manager import AlarmManager from src.utils.token_display_formatter import get_formatter from src.stats import TradingStats from src.config.logging_config import LoggingManager from .info.base import InfoCommandsBase logger = logging.getLogger(__name__) def _normalize_token_case(token: str) -> str: """ Normalize token case: if any characters are already uppercase, keep as-is. Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK. """ # Check if any character is already uppercase if any(c.isupper() for c in token): return token # Keep original case for mixed-case tokens else: return token.upper() # Convert to uppercase for all-lowercase input class ManagementCommands(InfoCommandsBase): """Handles all management-related Telegram commands.""" def __init__(self, trading_engine, monitoring_coordinator): """Initialize with trading engine and monitoring coordinator.""" super().__init__(trading_engine) self.trading_engine = trading_engine self.monitoring_coordinator = monitoring_coordinator self.alarm_manager = AlarmManager() async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /monitoring command.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id # 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 } formatter = get_formatter() # Get monitoring status from coordinator monitoring_status = await self.monitoring_coordinator.get_monitoring_status() monitoring_active = monitoring_status.get('is_running', False) status_text = f""" ๐Ÿ”„ System Monitoring Status ๐Ÿ“Š Monitoring System: โ€ข Status: {'โœ… Active' if monitoring_active else 'โŒ Inactive'} โ€ข Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds โ€ข Position Tracker: {'โœ…' if monitoring_status.get('components', {}).get('position_tracker', False) else 'โŒ'} โ€ข Risk Manager: {'โœ…' if monitoring_status.get('components', {}).get('risk_manager', False) else 'โŒ'} โ€ข Pending Orders: {'โœ…' if monitoring_status.get('components', {}).get('pending_orders_manager', False) else 'โŒ'} ๐Ÿ’ฐ Balance Tracking: โ€ข Total Adjustments: {adjustments_summary['adjustment_count']} โ€ข Net Adjustment: {await formatter.format_price_with_symbol(adjustments_summary['net_adjustment'])} ๐Ÿ”” 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.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id 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 = _normalize_token_case(arg) 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 = _normalize_token_case(context.args[0]) target_price = float(context.args[1]) # Get current market price symbol = f"{token}/USDC:USDC" market_data = await 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) formatter = get_formatter() # 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 if current_price != 0 else 0 target_price_str = await formatter.format_price_with_symbol(target_price, token) current_price_str = await formatter.format_price_with_symbol(current_price, token) price_diff_str = await formatter.format_price_with_symbol(price_diff, token) message = f""" โœ… Price Alarm Created ๐Ÿ“Š Alarm Details: โ€ข Alarm ID: {alarm['id']} โ€ข Token: {token} โ€ข Target Price: {target_price_str} โ€ข Current Price: {current_price_str} โ€ข Direction: {alarm['direction'].upper()} {direction_emoji} Alert Condition: Will trigger when {token} price moves {alarm['direction']} {target_price_str} ๐Ÿ’ฐ Price Difference: โ€ข Distance: {price_diff_str} ({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.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id 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.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id try: # Get monitoring status monitoring_status = await self.monitoring_coordinator.get_monitoring_status() monitoring_active = monitoring_status.get('is_running', False) # 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'} ๐Ÿ”„ Monitoring System: โ€ข Running: {'โœ… Yes' if monitoring_active else 'โŒ No'} ๐Ÿ“ State Files: โ€ข Price Alarms: {'โœ… Exists' if os.path.exists('data/price_alarms.json') else 'โŒ Missing'} โ€ข Trading Stats: {'โœ… Exists' if os.path.exists('data/trading_stats.sqlite') 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.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id try: # Get monitoring status monitoring_status = await self.monitoring_coordinator.get_monitoring_status() monitoring_active = monitoring_status.get('is_running', False) # 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() # Ensure all required keys exist with safe defaults total_trades = basic_stats.get('total_trades', 0) completed_trades = basic_stats.get('completed_trades', 0) days_active = basic_stats.get('days_active', 0) start_date = basic_stats.get('start_date', 'Unknown') else: total_trades = 0 completed_trades = 0 days_active = 0 start_date = 'Unknown' version_text = f""" ๐Ÿค– Trading Bot Version & System Info ๐Ÿ“ฑ Bot Information: โ€ข Version: 3.0.316 โ€ข 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: {total_trades} โ€ข Completed Trades: {completed_trades} โ€ข Days Active: {days_active} โ€ข Start Date: {start_date} ๐Ÿ”„ Monitoring Status: โ€ข Monitoring System: {'โœ… Active' if monitoring_active 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 show the main keyboard.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id # Define default keyboard layout default_keyboard = [ [KeyboardButton("LONG"), KeyboardButton("SHORT"), KeyboardButton("EXIT")], [KeyboardButton("BALANCE"), KeyboardButton("POSITIONS"), KeyboardButton("ORDERS")], [KeyboardButton("STATS"), KeyboardButton("MARKET"), KeyboardButton("PERFORMANCE")], [KeyboardButton("DAILY"), KeyboardButton("WEEKLY"), KeyboardButton("MONTHLY")], [KeyboardButton("RISK"), KeyboardButton("ALARM"), KeyboardButton("MONITORING")], [KeyboardButton("LOGS"), KeyboardButton("DEBUG"), KeyboardButton("VERSION")], [KeyboardButton("COMMANDS"), KeyboardButton("KEYBOARD"), KeyboardButton("COO"), KeyboardButton("COPY")] ] # Try to use custom keyboard from config if enabled if Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED and Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT: try: # Parse layout from config: "cmd1,cmd2|cmd3,cmd4|cmd5,cmd6" rows = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT.split('|') keyboard = [] for row in rows: buttons = [] for cmd in row.split(','): cmd = cmd.strip() # Remove leading slash if present and convert to button text if cmd.startswith('/'): cmd = cmd[1:] buttons.append(KeyboardButton(cmd.upper())) if buttons: # Only add non-empty rows keyboard.append(buttons) except Exception as e: logger.warning(f"Error parsing custom keyboard layout: {e}, falling back to default") keyboard = default_keyboard else: # Use default keyboard when custom keyboard is disabled keyboard = default_keyboard reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) await context.bot.send_message( chat_id=chat_id, text="๐ŸŽน Trading Bot Keyboard\n\nUse the buttons below for quick access to commands:", reply_markup=reply_markup, parse_mode='HTML' ) async def sync_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /sync command to synchronize exchange orders with database.""" try: if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return # Get force parameter force = False if context.args and context.args[0].lower() == 'force': force = True await self._reply(update, "๐Ÿ”„ Starting order synchronization...") # Get monitoring coordinator monitoring_coordinator = self.monitoring_coordinator if not monitoring_coordinator: await self._reply(update, "โŒ Monitoring coordinator not available. Please restart the bot.") return # Check if exchange order sync is available, if not try to initialize it if not monitoring_coordinator.exchange_order_sync: await self._reply(update, "โš ๏ธ Exchange order sync not initialized. Attempting to initialize...") # Try to initialize the sync manually try: await self._initialize_exchange_order_sync(monitoring_coordinator) if not monitoring_coordinator.exchange_order_sync: await self._reply(update, "โŒ Failed to initialize exchange order sync. Please restart the bot.") return await self._reply(update, "โœ… Exchange order sync initialized successfully.") except Exception as e: logger.error(f"Failed to initialize exchange order sync: {e}") await self._reply(update, f"โŒ Failed to initialize exchange order sync: {str(e)}") return # Run synchronization sync_results = monitoring_coordinator.exchange_order_sync.sync_exchange_orders_to_database() # Format results message message = await self._format_sync_results(sync_results, force) await self._reply(update, message) except Exception as e: logger.error(f"Error in sync command: {e}", exc_info=True) await self._reply(update, "โŒ Error during synchronization.") async def _initialize_exchange_order_sync(self, monitoring_coordinator): """Try to manually initialize exchange order sync.""" try: from src.monitoring.exchange_order_sync import ExchangeOrderSync # Get trading stats from trading engine stats = self.trading_engine.get_stats() if not stats: raise Exception("Trading stats not available from trading engine") # Get hyperliquid client hl_client = self.trading_engine.client if not hl_client: raise Exception("Hyperliquid client not available") # Initialize the exchange order sync monitoring_coordinator.exchange_order_sync = ExchangeOrderSync(hl_client, stats) logger.info("โœ… Manually initialized exchange order sync") except Exception as e: logger.error(f"Error manually initializing exchange order sync: {e}") raise async def _format_sync_results(self, sync_results: Dict[str, Any], force: bool) -> str: """Format synchronization results for display.""" if 'error' in sync_results: return f"โŒ Synchronization failed: {sync_results['error']}" new_orders = sync_results.get('new_orders_added', 0) updated_orders = sync_results.get('orders_updated', 0) cancelled_orders = sync_results.get('orphaned_orders_cancelled', 0) errors = sync_results.get('errors', []) message_parts = ["โœ… Order Synchronization Complete\n"] # Results summary message_parts.append("๐Ÿ“Š Sync Results:") message_parts.append(f" โ€ข {new_orders} new orders added to database") message_parts.append(f" โ€ข {updated_orders} existing orders updated") message_parts.append(f" โ€ข {cancelled_orders} orphaned orders cancelled") if errors: message_parts.append(f" โ€ข {len(errors)} errors encountered") message_parts.append("") message_parts.append("โš ๏ธ Errors:") for error in errors[:3]: # Show first 3 errors message_parts.append(f" โ€ข {error}") if len(errors) > 3: message_parts.append(f" โ€ข ... and {len(errors) - 3} more errors") message_parts.append("") # Status messages if new_orders == 0 and updated_orders == 0 and cancelled_orders == 0: message_parts.append("โœ… All orders are already synchronized") else: message_parts.append("๐Ÿ”„ Database now matches exchange state") # Help text message_parts.append("") message_parts.append("๐Ÿ’ก About Sync:") message_parts.append("โ€ข Orders placed directly on exchange are now tracked") message_parts.append("โ€ข Bot-placed orders remain tracked as before") message_parts.append("โ€ข Sync runs automatically every 30 seconds") message_parts.append("โ€ข Use /orders to see all synchronized orders") return "\n".join(message_parts) async def sync_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /sync_status command to show synchronization diagnostics.""" try: if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return # Check monitoring coordinator monitoring_coordinator = self.monitoring_coordinator diagnostic_parts = ["๐Ÿ” Synchronization Diagnostics\n"] # Check monitoring coordinator if monitoring_coordinator: diagnostic_parts.append("โœ… Monitoring Coordinator: Available") diagnostic_parts.append(f" โ€ข Running: {'Yes' if monitoring_coordinator.is_running else 'No'}") # Check exchange order sync if monitoring_coordinator.exchange_order_sync: diagnostic_parts.append("โœ… Exchange Order Sync: Available") # Try to get sync stats try: sync_stats = self._get_sync_diagnostics(monitoring_coordinator) diagnostic_parts.extend(sync_stats) except Exception as e: diagnostic_parts.append(f"โš ๏ธ Sync Stats Error: {str(e)}") else: diagnostic_parts.append("โŒ Exchange Order Sync: Not Available") # Try to diagnose why diagnostic_parts.append("\n๐Ÿ” Troubleshooting:") # Check position tracker if hasattr(monitoring_coordinator, 'position_tracker'): diagnostic_parts.append("โœ… Position Tracker: Available") if hasattr(monitoring_coordinator.position_tracker, 'trading_stats'): if monitoring_coordinator.position_tracker.trading_stats: diagnostic_parts.append("โœ… Position Tracker Trading Stats: Available") else: diagnostic_parts.append("โŒ Position Tracker Trading Stats: None") else: diagnostic_parts.append("โŒ Position Tracker Trading Stats: Not Found") else: diagnostic_parts.append("โŒ Position Tracker: Not Available") # Check trading engine stats stats = self.trading_engine.get_stats() if stats: diagnostic_parts.append("โœ… Trading Engine Stats: Available") else: diagnostic_parts.append("โŒ Trading Engine Stats: Not Available") # Check hyperliquid client client = self.trading_engine.client if client: diagnostic_parts.append("โœ… Hyperliquid Client: Available") else: diagnostic_parts.append("โŒ Hyperliquid Client: Not Available") else: diagnostic_parts.append("โŒ Monitoring Coordinator: Not Available") diagnostic_parts.append("\n๐Ÿ’ก Solutions:") diagnostic_parts.append("โ€ข Try: /sync force") diagnostic_parts.append("โ€ข If that fails, restart the bot") diagnostic_parts.append("โ€ข Check logs for detailed error messages") await self._reply(update, "\n".join(diagnostic_parts)) except Exception as e: logger.error(f"Error in sync_status command: {e}", exc_info=True) await self._reply(update, f"โŒ Error getting sync status: {str(e)}") def _get_sync_diagnostics(self, monitoring_coordinator): """Get detailed sync diagnostics.""" diagnostic_parts = [] try: # Test if we can access the sync components sync_manager = monitoring_coordinator.exchange_order_sync if hasattr(sync_manager, 'hl_client'): diagnostic_parts.append("โœ… Hyperliquid Client: Connected") else: diagnostic_parts.append("โŒ Hyperliquid Client: Missing") if hasattr(sync_manager, 'trading_stats'): diagnostic_parts.append("โœ… Trading Stats: Connected") # Try to get a count of database orders try: db_orders = [] db_orders.extend(sync_manager.trading_stats.get_orders_by_status('open', limit=10)) db_orders.extend(sync_manager.trading_stats.get_orders_by_status('submitted', limit=10)) diagnostic_parts.append(f"โœ… Database Orders: {len(db_orders)} found") except Exception as e: diagnostic_parts.append(f"โš ๏ธ Database Orders: Error ({str(e)})") else: diagnostic_parts.append("โŒ Trading Stats: Missing") except Exception as e: diagnostic_parts.append(f"โŒ Sync Components: Error ({str(e)})") return diagnostic_parts async def deposit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /deposit command to record a deposit.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id try: # Parse arguments if not context.args or len(context.args) != 1: await context.bot.send_message( chat_id=chat_id, text="โŒ Usage: /deposit \n\nExample: /deposit 500.00" ) return amount = float(context.args[0]) if amount <= 0: await context.bot.send_message(chat_id=chat_id, text="โŒ Deposit amount must be positive.") return # Record the deposit stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="โŒ Trading stats not available.") return await stats.record_deposit( amount=amount, description=f"Manual deposit via Telegram command" ) # Get updated stats basic_stats = stats.get_basic_stats() formatter = get_formatter() message = f""" โœ… Deposit Recorded ๐Ÿ’ฐ Deposit Amount: {await formatter.format_price_with_symbol(amount)} ๐Ÿ“Š Updated Stats: โ€ข Effective Initial Balance: {await formatter.format_price_with_symbol(basic_stats['initial_balance'])} โ€ข Current P&L: {await formatter.format_price_with_symbol(basic_stats['total_pnl'])} โ€ข Total Return: {basic_stats['total_return_pct']:.2f}% ๐Ÿ’ก Your P&L calculations are now updated with the deposit. """ await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML') logger.info(f"Recorded deposit of ${amount:.2f} via Telegram command") except ValueError: await context.bot.send_message(chat_id=chat_id, text="โŒ Invalid amount. Please enter a valid number.") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"โŒ Error recording deposit: {str(e)}") logger.error(f"Error in deposit command: {e}") async def withdrawal_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /withdrawal command to record a withdrawal.""" if not self._is_authorized(update): await self._reply(update, "โŒ Unauthorized access.") return chat_id = update.effective_chat.id try: # Parse arguments if not context.args or len(context.args) != 1: await context.bot.send_message( chat_id=chat_id, text="โŒ Usage: /withdrawal \n\nExample: /withdrawal 200.00" ) return amount = float(context.args[0]) if amount <= 0: await context.bot.send_message(chat_id=chat_id, text="โŒ Withdrawal amount must be positive.") return # Record the withdrawal stats = self.trading_engine.get_stats() if not stats: await context.bot.send_message(chat_id=chat_id, text="โŒ Trading stats not available.") return await stats.record_withdrawal( amount=amount, description=f"Manual withdrawal via Telegram command" ) # Get updated stats basic_stats = stats.get_basic_stats() formatter = get_formatter() message = f""" โœ… Withdrawal Recorded ๐Ÿ’ธ Withdrawal Amount: {await formatter.format_price_with_symbol(amount)} ๐Ÿ“Š Updated Stats: โ€ข Effective Initial Balance: {await formatter.format_price_with_symbol(basic_stats['initial_balance'])} โ€ข Current P&L: {await formatter.format_price_with_symbol(basic_stats['total_pnl'])} โ€ข Total Return: {basic_stats['total_return_pct']:.2f}% ๐Ÿ’ก Your P&L calculations are now updated with the withdrawal. """ await context.bot.send_message(chat_id=chat_id, text=message.strip(), parse_mode='HTML') logger.info(f"Recorded withdrawal of ${amount:.2f} via Telegram command") except ValueError: await context.bot.send_message(chat_id=chat_id, text="โŒ Invalid amount. Please enter a valid number.") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"โŒ Error recording withdrawal: {str(e)}") logger.error(f"Error in withdrawal command: {e}")