123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- """
- 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
- from src.utils.token_display_formatter import get_formatter
- from src.stats import TradingStats
- from src.config.logging_config import LoggingManager
- 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.
- """
-
- if any(c.isupper() for c in token):
- return token
- else:
- return token.upper()
- class ManagementCommands:
- """Handles all management-related Telegram commands."""
-
- def __init__(self, trading_engine, monitoring_coordinator):
- """Initialize with trading engine and monitoring coordinator."""
- self.trading_engine = trading_engine
- self.monitoring_coordinator = monitoring_coordinator
- 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
-
-
- alarm_stats = self.alarm_manager.get_statistics()
-
-
- 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()
-
-
- 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."""
- 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:
-
- 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]
-
-
- try:
- alarm_id = int(arg)
-
- 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:
-
- 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:
-
- token = _normalize_token_case(context.args[0])
- target_price = float(context.args[1])
-
-
- 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
-
-
- alarm = self.alarm_manager.create_alarm(token, target_price, current_price)
- formatter = get_formatter()
-
-
- 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:
-
- 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
-
-
- 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
-
-
- if context.args and context.args[0].lower() == 'cleanup':
- days_to_keep = 30
- 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).")
-
-
- 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
-
-
- 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)
-
-
- 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:
-
- monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
- monitoring_active = monitoring_status.get('is_running', False)
-
-
- 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()}
- """
-
-
- 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]:
- 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"
-
-
- 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:
-
- monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
- monitoring_active = monitoring_status.get('is_running', False)
-
- 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
-
-
- 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>
- • 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."""
- 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
-
- 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")]
- ]
-
- if Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED and Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT:
- try:
-
- rows = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT.split('|')
- keyboard = []
- for row in rows:
- buttons = []
- for cmd in row.split(','):
- cmd = cmd.strip()
-
- if cmd.startswith('/'):
- cmd = cmd[1:]
- buttons.append(KeyboardButton(cmd.upper()))
- if buttons:
- keyboard.append(buttons)
- except Exception as e:
- logger.warning(f"Error parsing custom keyboard layout: {e}, falling back to default")
- keyboard = default_keyboard
- else:
-
- 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 bot state with exchange."""
- 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:
-
- force_sync = len(context.args) > 0 and context.args[0].lower() == "force"
-
- if not force_sync:
-
- await context.bot.send_message(
- chat_id=chat_id,
- text=(
- "⚠️ <b>Data Synchronization Warning</b>\n\n"
- "This will:\n"
- "• Clear ALL local trades, orders, and pending stop losses\n"
- "• Reset bot tracking state\n"
- "• Sync with current exchange positions/orders\n"
- "• Preserve balance and performance history\n\n"
- "🔄 Use <code>/sync force</code> to proceed\n"
- "❌ Use any other command to cancel"
- ),
- parse_mode='HTML'
- )
- return
-
- processing_msg = await context.bot.send_message(
- chat_id=chat_id,
- text="🔄 <b>Synchronizing with Exchange...</b>\n\n⏳ Step 1/5: Clearing local data...",
- parse_mode='HTML'
- )
-
- stats = self.trading_engine.get_stats()
- if stats:
-
- stats.db_manager._execute_query(
- "DELETE FROM trades WHERE status IN ('pending', 'executed', 'position_opened', 'cancelled')"
- )
-
-
- stats.db_manager._execute_query(
- "DELETE FROM orders WHERE status IN ('pending_submission', 'open', 'submitted', 'pending_trigger')"
- )
-
- logger.info("🧹 Cleared local trading state from database")
-
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n⏳ Step 2/5: Clearing pending stop losses...",
- parse_mode='HTML'
- )
-
- try:
- if hasattr(self.monitoring_coordinator, 'pending_orders_manager'):
- pending_manager = self.monitoring_coordinator.pending_orders_manager
- if pending_manager and hasattr(pending_manager, 'db_path'):
- import sqlite3
- with sqlite3.connect(pending_manager.db_path) as conn:
- conn.execute("DELETE FROM pending_stop_loss WHERE status IN ('pending', 'placed')")
- conn.commit()
- logger.info("🧹 Cleared pending stop loss orders")
- except Exception as e:
- logger.warning(f"Could not clear pending orders: {e}")
-
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n⏳ Step 3/5: Fetching exchange state...",
- parse_mode='HTML'
- )
-
- exchange_positions = self.trading_engine.get_positions() or []
- exchange_orders = self.trading_engine.get_orders() or []
-
-
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n⏳ Step 4/5: Recreating position tracking...",
- parse_mode='HTML'
- )
-
- positions_synced = 0
- orders_synced = 0
-
- if stats:
-
- for position in exchange_positions:
- try:
- size = float(position.get('contracts', 0))
- if abs(size) > 1e-8:
- symbol = position.get('symbol', '')
- if symbol:
-
- token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-
-
-
- exchange_side = position.get('side', '').lower()
- if exchange_side == 'long':
- side = 'buy'
- position_side = 'long'
- elif exchange_side == 'short':
- side = 'sell'
- position_side = 'short'
- else:
-
- side = 'buy' if size > 0 else 'sell'
- position_side = 'long' if size > 0 else 'short'
- logger.warning(f"Using contracts sign fallback for {token}: size={size}, exchange_side='{exchange_side}'")
-
- entry_price = float(position.get('entryPrice', 0))
-
- if entry_price > 0:
-
- lifecycle_id = stats.create_trade_lifecycle(
- symbol=symbol,
- side=side,
- stop_loss_price=None,
- trade_type='sync'
- )
-
- if lifecycle_id:
-
- await stats.update_trade_position_opened(
- lifecycle_id=lifecycle_id,
- entry_price=entry_price,
- entry_amount=abs(size),
- exchange_fill_id=f"sync_{lifecycle_id[:8]}"
- )
-
-
- unrealized_pnl = float(position.get('unrealizedPnl', 0))
-
- mark_price_raw = position.get('markPrice')
- mark_price = float(mark_price_raw) if mark_price_raw is not None else entry_price
-
- stats.update_trade_market_data(
- trade_lifecycle_id=lifecycle_id,
- current_position_size=abs(size),
- unrealized_pnl=unrealized_pnl,
- mark_price=mark_price,
- position_value=abs(size) * mark_price
- )
-
- positions_synced += 1
- logger.info(f"🔄 Recreated position tracking for {token}: {position_side} {abs(size)} @ {entry_price}")
-
- except Exception as e:
- logger.error(f"Error recreating position for {position}: {e}")
-
- for order in exchange_orders:
- try:
- order_symbol = order.get('symbol', '')
- order_id = order.get('id', '')
- order_type = order.get('type', '').lower()
- is_reduce_only = order.get('reduceOnly', False)
-
- if order_symbol and order_id and is_reduce_only:
-
-
- matching_trade = stats.get_trade_by_symbol_and_status(order_symbol, 'position_opened')
- if matching_trade:
- lifecycle_id = matching_trade.get('trade_lifecycle_id')
- if lifecycle_id:
- order_price = float(order.get('price', 0))
- stop_price = float(order.get('stopPrice', 0)) or order_price
-
- if 'stop' in order_type and stop_price > 0:
-
- await stats.link_stop_loss_to_trade(lifecycle_id, order_id, stop_price)
- orders_synced += 1
- logger.info(f"🔗 Linked stop loss order {order_id} to position {lifecycle_id[:8]}")
- elif order_type in ['limit', 'take_profit'] and order_price > 0:
-
- await stats.link_take_profit_to_trade(lifecycle_id, order_id, order_price)
- orders_synced += 1
- logger.info(f"🔗 Linked take profit order {order_id} to position {lifecycle_id[:8]}")
-
- except Exception as e:
- logger.error(f"Error linking order {order}: {e}")
-
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n✅ Step 4/5: Position tracking recreated\n⏳ Step 5/5: Updating balance...",
- parse_mode='HTML'
- )
-
- current_balance = 0.0
- try:
- balance_data = self.trading_engine.get_balance()
- if balance_data and balance_data.get('total'):
- current_balance = float(balance_data['total'].get('USDC', 0))
- if stats:
- await stats.record_balance_snapshot(current_balance, unrealized_pnl=0.0, notes="Post-sync balance")
- except Exception as e:
- logger.warning(f"Could not update balance after sync: {e}")
-
- formatter = get_formatter()
- success_message = f"""
- ✅ <b>Synchronization Complete!</b>
- 📊 <b>Sync Results:</b>
- • Positions Recreated: {positions_synced}
- • Orders Linked: {orders_synced}
- • Current Balance: {await formatter.format_price_with_symbol(current_balance)}
- 🔄 <b>What was reset:</b>
- • Local trade tracking
- • Pending orders database
- • Order monitoring state
- 🎯 <b>What was preserved:</b>
- • Historical performance data
- • Balance adjustment history
- • Completed trade statistics
- 💡 The bot is now synchronized with your exchange state and ready for trading!
- """
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text=success_message.strip(),
- parse_mode='HTML'
- )
- logger.info(f"🎉 Sync completed: {positions_synced} positions, {orders_synced} orders linked")
- except Exception as e:
- error_message = f"❌ <b>Sync Failed</b>\n\nError: {str(e)}\n\n💡 Check logs for details."
- try:
- await context.bot.edit_message_text(
- chat_id=chat_id,
- message_id=processing_msg.message_id,
- text=error_message,
- parse_mode='HTML'
- )
- except:
- await context.bot.send_message(chat_id=chat_id, text=error_message, parse_mode='HTML')
-
- logger.error(f"Error in sync command: {e}", exc_info=True)
- async def deposit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Handle the /deposit command to record a deposit."""
- 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) != 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
-
- 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"
- )
-
- 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."""
- 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) != 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
-
- 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"
- )
-
- 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}")
|