123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- #!/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"""
- 🔄 <b>System Monitoring Status</b>
- 📊 <b>Order Monitoring:</b>
- • Active: {'✅ Yes' if monitoring_active else '❌ No'}
- • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
- • Market Monitor: {'✅ Running' if monitoring_active else '❌ Stopped'}
- 💰 <b>Balance Tracking:</b>
- • Total Adjustments: {adjustments_summary['adjustment_count']}
- • Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f}
- 🔔 <b>Price Alarms:</b>
- • 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']}
- 🔄 <b>External Trade Monitoring:</b>
- • Auto Stats Update: ✅ Enabled
- • External Notifications: ✅ Enabled
- 🛡️ <b>Risk Management:</b>
- • Automatic Stop Loss: {'✅ Enabled' if hasattr(Config, 'RISK_MANAGEMENT_ENABLED') and Config.RISK_MANAGEMENT_ENABLED else '❌ Disabled'}
- • Order-based Stop Loss: ✅ Enabled
- 📈 <b>Notifications:</b>
- • 🚀 Position Opened/Increased
- • 📉 Position Partially/Fully Closed
- • 🎯 P&L Calculations
- • 🔔 Price Alarm Triggers
- • 🔄 External Trade Detection
- • 🛑 Order-based Stop Loss Placement
- 💾 <b>Bot State Persistence:</b>
- • Trading Engine State: ✅ Saved
- • Order Tracking: ✅ Saved
- • State Survives Restarts: ✅ Yes
- ⏰ <b>Last Check:</b> {datetime.now().strftime('%H:%M:%S')}
- 💡 <b>Monitoring Features:</b>
- • 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📋 <b>Active Alarms by Token:</b>\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"""
- ✅ <b>Price Alarm Created</b>
- 📊 <b>Alarm Details:</b>
- • Alarm ID: {alarm['id']}
- • Token: {token}
- • Target Price: ${target_price:,.2f}
- • Current Price: ${current_price:,.2f}
- • Direction: {alarm['direction'].upper()}
- {direction_emoji} <b>Alert Condition:</b>
- Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
- 💰 <b>Price Difference:</b>
- • Distance: ${price_diff:,.2f} ({price_diff_percent:.2f}%)
- • Status: ACTIVE ✅
- ⏰ <b>Created:</b> {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"
- "• <code>/alarm</code> - List all alarms\n"
- "• <code>/alarm BTC</code> - List BTC alarms\n"
- "• <code>/alarm BTC 50000</code> - Set alarm for BTC at $50,000\n"
- "• <code>/alarm 3</code> - 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"""
- 📜 <b>System Logging Status</b>
- 📁 <b>Log Directory:</b> {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')})
- 📋 <b>Recent Log Files:</b>
- """
-
- 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"""
- 📊 <b>Log Management:</b>
- • Location: ./logs/
- • Rotation: Daily
- • Retention: Manual cleanup available
- • Format: timestamp - module - level - message
- 🧹 <b>Cleanup Commands:</b>
- • <code>/logs cleanup</code> - Remove logs older than 30 days
- • <code>/logs cleanup 7</code> - Remove logs older than 7 days
- 💡 <b>Log Levels:</b>
- • 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"""
- 🐛 <b>Debug Information</b>
- 💻 <b>System Info:</b>
- • Python: {sys.version.split()[0]}
- • Platform: {platform.system()} {platform.release()}
- • Architecture: {platform.machine()}
- 📊 <b>Trading Engine:</b>
- • Stats Available: {'✅ Yes' if self.trading_engine.get_stats() else '❌ No'}
- • Client Connected: {'✅ Yes' if self.trading_engine.client else '❌ No'}
- 🔄 <b>Market Monitor:</b>
- • Running: {'✅ Yes' if self.market_monitor.is_running else '❌ No'}
- 📁 <b>State Files:</b>
- • 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'}
- 🔔 <b>Alarm Manager:</b>
- • Active Alarms: {self.alarm_manager.get_statistics()['total_active']}
- • Triggered Alarms: {self.alarm_manager.get_statistics()['total_triggered']}
- ⏰ <b>Timestamps:</b>
- • 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📈 <b>Current Positions:</b> {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📈 <b>Positions:</b> No positions found\n"
- except Exception as e:
- debug_info += f"\n📈 <b>Positions:</b> 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💰 <b>USDC Balance:</b> ${usdc_balance:,.2f}\n"
- else:
- debug_info += "\n💰 <b>Balance:</b> No balance data\n"
- except Exception as e:
- debug_info += f"\n💰 <b>Balance:</b> 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"""
- 🤖 <b>Trading Bot Version & System Info</b>
- 📱 <b>Bot Information:</b>
- • Version: <code>2.1.2</code>
- • Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
- • Uptime: {uptime_info}
- • Default Token: {Config.DEFAULT_TRADING_TOKEN}
- 💻 <b>System Information:</b>
- • Python: {sys.version.split()[0]}
- • Platform: {platform.system()} {platform.release()}
- • Architecture: {platform.machine()}
- 📊 <b>Trading Stats:</b>
- • Total Orders: {basic_stats['total_trades']}
- • Completed Trades: {basic_stats['completed_trades']}
- • Days Active: {basic_stats['days_active']}
- • Start Date: {basic_stats['start_date']}
- 🔄 <b>Monitoring Status:</b>
- • 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)
- ⏰ <b>Current Time:</b> {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="⌨️ <b>Custom Keyboard Activated!</b>\n\n🎯 <b>Your quick buttons are now ready:</b>\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💡 <b>How to use:</b>\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="❌ <b>Custom Keyboard Disabled</b>\n\n🔧 <b>To enable:</b>\n• Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n• Restart the bot\n• Run /keyboard again\n\n📋 <b>Current config:</b>\n• Enabled: {keyboard_enabled}\n• Layout: {getattr(Config, 'TELEGRAM_CUSTOM_KEYBOARD_LAYOUT', 'default')}", parse_mode='HTML')
|