#!/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
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.
"""
# 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:
"""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
# 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."""
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 = _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."""
chat_id = update.effective_chat.id
if not self._is_authorized(chat_id):
await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
return
try:
logs_dir = "logs"
if not os.path.exists(logs_dir):
await context.bot.send_message(chat_id=chat_id, text="๐ No logs directory found.")
return
# Get log files
log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
if not log_files:
await context.bot.send_message(chat_id=chat_id, text="๐ No log files found.")
return
# Handle cleanup command
if context.args and context.args[0].lower() == 'cleanup':
days_to_keep = 30 # Default
if len(context.args) > 1:
try:
days_to_keep = int(context.args[1])
except ValueError:
await context.bot.send_message(chat_id=chat_id, text="โ Invalid number of days. Using default (30 days).")
# Clean up old log files
cutoff_date = datetime.now() - timedelta(days=days_to_keep)
cleaned_files = 0
total_size_cleaned = 0
for log_file in log_files:
file_path = os.path.join(logs_dir, log_file)
file_stat = os.stat(file_path)
file_date = datetime.fromtimestamp(file_stat.st_mtime)
if file_date < cutoff_date:
file_size = file_stat.st_size
os.remove(file_path)
cleaned_files += 1
total_size_cleaned += file_size
size_cleaned_mb = total_size_cleaned / (1024 * 1024)
await context.bot.send_message(chat_id=chat_id, text=
f"๐งน Log cleanup complete.\n"
f"โข Files older than {days_to_keep} days removed.\n"
f"โข Total files deleted: {cleaned_files}\n"
f"โข Total size cleaned: {size_cleaned_mb:.2f} MB"
)
return
# Show log statistics
total_size = 0
oldest_file = None
newest_file = None
recent_files = []
for log_file in sorted(log_files):
file_path = os.path.join(logs_dir, log_file)
file_stat = os.stat(file_path)
file_size = file_stat.st_size
file_date = datetime.fromtimestamp(file_stat.st_mtime)
total_size += file_size
if oldest_file is None or file_date < oldest_file[1]:
oldest_file = (log_file, file_date)
if newest_file is None or file_date > newest_file[1]:
newest_file = (log_file, file_date)
# Keep track of recent files
if len(recent_files) < 5:
recent_files.append((log_file, file_size, file_date))
logs_message = f"""
๐ System Logging Status
๐ Log Directory: {logs_dir}/
โข Total Files: {len(log_files)}
โข Total Size: {total_size / 1024 / 1024:.2f} MB
โข Oldest File: {oldest_file[0]} ({oldest_file[1].strftime('%m/%d/%Y')})
โข Newest File: {newest_file[0]} ({newest_file[1].strftime('%m/%d/%Y')})
๐ Recent Log Files:
"""
for log_file, file_size, file_date in reversed(recent_files):
size_mb = file_size / 1024 / 1024
logs_message += f"โข {log_file} ({size_mb:.2f} MB) - {file_date.strftime('%m/%d %H:%M')}\n"
logs_message += f"""
๐ Log Management:
โข Location: ./logs/
โข Rotation: Daily
โข Retention: Manual cleanup available
โข Format: timestamp - module - level - message
๐งน Cleanup Commands:
โข /logs cleanup
- Remove logs older than 30 days
โข /logs cleanup 7
- Remove logs older than 7 days
๐ก Log Levels:
โข INFO: Normal operations
โข ERROR: Error conditions
โข DEBUG: Detailed debugging (if enabled)
"""
await context.bot.send_message(chat_id=chat_id, text=logs_message.strip(), parse_mode='HTML')
except Exception as e:
error_message = f"โ Error processing logs command: {str(e)}"
await context.bot.send_message(chat_id=chat_id, text=error_message)
logger.error(f"Error in logs command: {e}")
async def debug_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle the /debug command."""
chat_id = update.effective_chat.id
if not self._is_authorized(chat_id):
await context.bot.send_message(chat_id=chat_id, text="โ Unauthorized access.")
return
try:
# Get 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."""
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 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()
else:
basic_stats = {'total_trades': 0, 'completed_trades': 0, 'days_active': 0, 'start_date': 'Unknown'}
version_text = f"""
๐ค Trading Bot Version & System Info
๐ฑ Bot Information:
โข Version: 2.1.2
โข Network: {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}
โข Uptime: {uptime_info}
โข Default Token: {Config.DEFAULT_TRADING_TOKEN}
๐ป System Information:
โข Python: {sys.version.split()[0]}
โข Platform: {platform.system()} {platform.release()}
โข Architecture: {platform.machine()}
๐ Trading Stats:
โข Total Orders: {basic_stats['total_trades']}
โข Completed Trades: {basic_stats['completed_trades']}
โข Days Active: {basic_stats['days_active']}
โข Start Date: {basic_stats['start_date']}
๐ Monitoring Status:
โข 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."""
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
# 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")]
]
# 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 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:
# Get confirmation from user unless force flag is provided
force_sync = len(context.args) > 0 and context.args[0].lower() == "force"
if not force_sync:
# Send confirmation message
await context.bot.send_message(
chat_id=chat_id,
text=(
"โ ๏ธ Data Synchronization Warning\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 /sync force
to proceed\n"
"โ Use any other command to cancel"
),
parse_mode='HTML'
)
return
# Send processing message
processing_msg = await context.bot.send_message(
chat_id=chat_id,
text="๐ Synchronizing with Exchange...\n\nโณ Step 1/5: Clearing local data...",
parse_mode='HTML'
)
# Step 1: Clear local trading data
stats = self.trading_engine.get_stats()
if stats:
# Clear trades table (keep only position_closed for history)
stats.db_manager._execute_query(
"DELETE FROM trades WHERE status IN ('pending', 'executed', 'position_opened', 'cancelled')"
)
# Clear orders table (keep only filled/cancelled for history)
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")
# Update status
await context.bot.edit_message_text(
chat_id=chat_id,
message_id=processing_msg.message_id,
text="๐ Synchronizing with Exchange...\n\nโ
Step 1/5: Local data cleared\nโณ Step 2/5: Clearing pending stop losses...",
parse_mode='HTML'
)
# Step 2: Clear pending stop loss orders
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}")
# Update status
await context.bot.edit_message_text(
chat_id=chat_id,
message_id=processing_msg.message_id,
text="๐ Synchronizing with Exchange...\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'
)
# Step 3: Fetch current exchange state
exchange_positions = self.trading_engine.get_positions() or []
exchange_orders = self.trading_engine.get_orders() or []
# Update status
await context.bot.edit_message_text(
chat_id=chat_id,
message_id=processing_msg.message_id,
text="๐ Synchronizing with Exchange...\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'
)
# Step 4: Recreate position tracking for open positions
positions_synced = 0
orders_synced = 0
if stats:
# Create trade lifecycles for open positions
for position in exchange_positions:
try:
size = float(position.get('contracts', 0))
if abs(size) > 1e-8: # Position exists
symbol = position.get('symbol', '')
if symbol:
# Extract token from symbol (e.g., "BTC/USDC:USDC" -> "BTC")
token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
# Create a new trade lifecycle for this existing position
# Use the side field from exchange (more reliable than contracts sign)
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:
# Fallback to contracts sign if side field missing
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: # Valid position data
# Create the lifecycle entry (this generates and returns lifecycle_id)
lifecycle_id = stats.create_trade_lifecycle(
symbol=symbol,
side=side,
stop_loss_price=None, # Will be detected from orders
trade_type='sync'
)
if lifecycle_id:
# Update to position opened status
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]}"
)
# Update with current market data
unrealized_pnl = float(position.get('unrealizedPnl', 0))
# Handle None markPrice safely
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}")
# Link existing orders to positions
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:
# This might be a stop loss or take profit order
# Find the corresponding position
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:
# Link as stop loss
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:
# Link as take profit
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}")
# Update status
await context.bot.edit_message_text(
chat_id=chat_id,
message_id=processing_msg.message_id,
text="๐ Synchronizing with Exchange...\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'
)
# Step 5: Update current balance
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}")
# Final success message
formatter = get_formatter()
success_message = f"""
โ
Synchronization Complete!
๐ Sync Results:
โข Positions Recreated: {positions_synced}
โข Orders Linked: {orders_synced}
โข Current Balance: {await formatter.format_price_with_symbol(current_balance)}
๐ What was reset:
โข Local trade tracking
โข Pending orders database
โข Order monitoring state
๐ฏ What was preserved:
โข 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"โ Sync Failed\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:
# 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."""
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:
# 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}")