123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896 |
- #!/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"""
- 🔄 <b>System Monitoring Status</b>
- 📊 <b>Monitoring System:</b>
- • 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 '❌'}
- 💰 <b>Balance Tracking:</b>
- • Total Adjustments: {adjustments_summary['adjustment_count']}
- • Net Adjustment: {await formatter.format_price_with_symbol(adjustments_summary['net_adjustment'])}
- 🔔 <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."""
- 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"""
- ✅ <b>Price Alarm Created</b>
- 📊 <b>Alarm Details:</b>
- • Alarm ID: {alarm['id']}
- • Token: {token}
- • Target Price: {target_price_str}
- • Current Price: {current_price_str}
- • Direction: {alarm['direction'].upper()}
- {direction_emoji} <b>Alert Condition:</b>
- Will trigger when {token} price moves {alarm['direction']} {target_price_str}
- 💰 <b>Price Difference:</b>
- • Distance: {price_diff_str} ({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."""
- 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"""
- 📜 <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."""
- 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"""
- 🐛 <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>Monitoring System:</b>
- • Running: {'✅ Yes' if monitoring_active else '❌ No'}
- 📁 <b>State Files:</b>
- • 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'}
- 🔔 <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."""
- 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"""
- 🤖 <b>Trading Bot Version & System Info</b>
- 📱 <b>Bot Information:</b>
- • Version: <code>3.0.316</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: {total_trades}
- • Completed Trades: {completed_trades}
- • Days Active: {days_active}
- • Start Date: {start_date}
- 🔄 <b>Monitoring Status:</b>
- • Monitoring System: {'✅ Active' if monitoring_active 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 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="🎹 <b>Trading Bot Keyboard</b>\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 = ["✅ <b>Order Synchronization Complete</b>\n"]
-
- # Results summary
- message_parts.append("📊 <b>Sync Results:</b>")
- 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("⚠️ <b>Errors:</b>")
- 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("💡 <b>About Sync:</b>")
- 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 = ["🔍 <b>Synchronization Diagnostics</b>\n"]
-
- # Check monitoring coordinator
- if monitoring_coordinator:
- diagnostic_parts.append("✅ <b>Monitoring Coordinator:</b> 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("✅ <b>Exchange Order Sync:</b> 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"⚠️ <b>Sync Stats Error:</b> {str(e)}")
- else:
- diagnostic_parts.append("❌ <b>Exchange Order Sync:</b> Not Available")
-
- # Try to diagnose why
- diagnostic_parts.append("\n🔍 <b>Troubleshooting:</b>")
-
- # 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("❌ <b>Monitoring Coordinator:</b> Not Available")
-
- diagnostic_parts.append("\n💡 <b>Solutions:</b>")
- 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 <amount>\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"""
- ✅ <b>Deposit Recorded</b>
- 💰 <b>Deposit Amount:</b> {await formatter.format_price_with_symbol(amount)}
- 📊 <b>Updated Stats:</b>
- • 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 <amount>\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"""
- ✅ <b>Withdrawal Recorded</b>
- 💸 <b>Withdrawal Amount:</b> {await formatter.format_price_with_symbol(amount)}
- 📊 <b>Updated Stats:</b>
- • 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}")
|