#!/usr/bin/env python3 """ Telegram Bot for Hyperliquid Trading This module provides a Telegram interface for manual Hyperliquid trading with comprehensive statistics tracking and phone-friendly controls. """ import asyncio import json import os from datetime import datetime, timedelta from typing import Optional, Dict, Any from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters from hyperliquid_client import HyperliquidClient from trading_stats import TradingStats from config import Config from alarm_manager import AlarmManager from logging_config import setup_logging, cleanup_logs, format_log_stats # Set up logging using the new configuration system logger = setup_logging().getChild(__name__) class TelegramTradingBot: """Telegram trading bot for manual trading operations.""" def __init__(self): """Initialize the Telegram trading bot.""" self.client = HyperliquidClient() self.application = None self.order_monitoring_task = None self.last_filled_orders = set() self.alarms = [] # List to store price alarms self.bot_heartbeat_seconds = getattr(Config, 'BOT_HEARTBEAT_SECONDS', 10) self.external_trade_timestamps = set() # Track external trade timestamps to avoid duplicates self.last_position_check = {} # Track last position state for comparison self.position_tracker = {} # For enhanced position tracking self.stats = None self.version = "Unknown" # Will be set by launcher # Bot state persistence file self.bot_state_file = "bot_state.json" # Order monitoring attributes self.monitoring_active = False self.last_known_orders = set() # Track order IDs we've seen self.last_known_positions = {} # Track position sizes for P&L calculation # External trade monitoring self.last_processed_trade_time = None # Track last processed external trade # Deposit/Withdrawal monitoring self.last_deposit_withdrawal_check = None # Track last deposit/withdrawal check self.deposit_withdrawal_check_interval = 3600 # Check every hour (3600 seconds) # Alarm management self.alarm_manager = AlarmManager() # Pending stop loss storage self.pending_stop_losses = {} # Format: {order_id: {'token': str, 'stop_price': float, 'side': str, 'amount': float, 'order_type': str}} # Load bot state first, then initialize stats self._load_bot_state() self._initialize_stats() def _load_bot_state(self): """Load bot state from disk.""" try: if os.path.exists(self.bot_state_file): with open(self.bot_state_file, 'r') as f: state_data = json.load(f) # Restore critical state self.pending_stop_losses = state_data.get('pending_stop_losses', {}) self.last_known_orders = set(state_data.get('last_known_orders', [])) self.last_known_positions = state_data.get('last_known_positions', {}) # Restore timestamps (convert from ISO string if present) last_trade_time = state_data.get('last_processed_trade_time') if last_trade_time: try: self.last_processed_trade_time = datetime.fromisoformat(last_trade_time) except (ValueError, TypeError): self.last_processed_trade_time = None last_deposit_check = state_data.get('last_deposit_withdrawal_check') if last_deposit_check: try: self.last_deposit_withdrawal_check = datetime.fromisoformat(last_deposit_check) except (ValueError, TypeError): self.last_deposit_withdrawal_check = None logger.info(f"๐Ÿ”„ Restored bot state: {len(self.pending_stop_losses)} pending stop losses, {len(self.last_known_orders)} tracked orders") # Log details about restored pending stop losses if self.pending_stop_losses: for order_id, stop_loss_info in self.pending_stop_losses.items(): token = stop_loss_info.get('token', 'Unknown') stop_price = stop_loss_info.get('stop_price', 0) order_type = stop_loss_info.get('order_type', 'Unknown') logger.info(f"๐Ÿ“‹ Restored pending stop loss: {order_id} -> {token} {order_type} @ ${stop_price}") except Exception as e: logger.error(f"โŒ Error loading bot state: {e}") # Initialize with defaults self.pending_stop_losses = {} self.last_known_orders = set() self.last_known_positions = {} self.last_processed_trade_time = None self.last_deposit_withdrawal_check = None def _save_bot_state(self): """Save bot state to disk.""" try: state_data = { 'pending_stop_losses': self.pending_stop_losses, 'last_known_orders': list(self.last_known_orders), # Convert set to list for JSON 'last_known_positions': self.last_known_positions, 'last_processed_trade_time': self.last_processed_trade_time.isoformat() if self.last_processed_trade_time else None, 'last_deposit_withdrawal_check': self.last_deposit_withdrawal_check.isoformat() if self.last_deposit_withdrawal_check else None, 'last_updated': datetime.now().isoformat(), 'version': self.version } with open(self.bot_state_file, 'w') as f: json.dump(state_data, f, indent=2, default=str) logger.debug(f"๐Ÿ’พ Saved bot state: {len(self.pending_stop_losses)} pending stop losses") except Exception as e: logger.error(f"โŒ Error saving bot state: {e}") def _initialize_stats(self): """Initialize stats with current balance.""" try: # Initialize TradingStats object first self.stats = TradingStats() # Get current balance and set it as initial balance balance = self.client.get_balance() if balance and balance.get('total'): # Get USDC balance as the main balance usdc_balance = float(balance['total'].get('USDC', 0)) self.stats.set_initial_balance(usdc_balance) except Exception as e: logger.error(f"Could not initialize stats: {e}") def is_authorized(self, chat_id: str) -> bool: """Check if the chat ID is authorized to use the bot.""" return str(chat_id) == str(Config.TELEGRAM_CHAT_ID) async def send_message(self, text: str, parse_mode: str = 'HTML') -> None: """Send a message to the authorized chat.""" if self.application and Config.TELEGRAM_CHAT_ID: try: await self.application.bot.send_message( chat_id=Config.TELEGRAM_CHAT_ID, text=text, parse_mode=parse_mode ) except Exception as e: logger.error(f"Failed to send message: {e}") def _create_custom_keyboard(self) -> Optional[ReplyKeyboardMarkup]: """Create a custom keyboard from configuration.""" if not Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED: return None try: layout = Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT # Parse the layout: "cmd1,cmd2,cmd3|cmd4,cmd5|cmd6,cmd7,cmd8,cmd9" rows = layout.split('|') keyboard = [] for row in rows: commands = [cmd.strip() for cmd in row.split(',') if cmd.strip()] if commands: keyboard.append([KeyboardButton(cmd.lstrip('/').capitalize()) for cmd in commands]) if keyboard: return ReplyKeyboardMarkup( keyboard, resize_keyboard=True, # Resize to fit screen one_time_keyboard=False, # Keep keyboard persistent selective=True # Show only to authorized users ) except Exception as e: logger.error(f"Failed to create custom keyboard: {e}") return None async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /start command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return welcome_text = """ ๐Ÿค– Welcome to Hyperliquid Trading Bot ๐Ÿ“ฑ Quick Actions: โ€ข Trading: /long BTC 100 or /short ETH 50 โ€ข Exit: /exit BTC (closes position) โ€ข Info: /balance, /positions, /orders ๐Ÿ“Š Market Data: โ€ข /market - Detailed market overview โ€ข /price - Quick price check โšก Quick Commands: โ€ข /balance - Account balance โ€ข /positions - Open positions โ€ข /orders - Active orders โ€ข /market - Market data & prices ๐Ÿš€ Trading: โ€ข /long BTC 100 - Long position โ€ข /long BTC 100 45000 - Limit order โ€ข /long BTC 100 sl:44000 - With stop loss โ€ข /short ETH 50 - Short position โ€ข /short ETH 50 3500 sl:3600 - With stop loss โ€ข /exit BTC - Close position โ€ข /coo BTC - Cancel open orders ๐Ÿ›ก๏ธ Risk Management: โ€ข Enabled: {risk_enabled} โ€ข Auto Stop Loss: {stop_loss}% โ€ข Order Stop Loss: Use sl:price parameter โ€ข /sl BTC 44000 - Manual stop loss โ€ข /tp BTC 50000 - Take profit order ๐Ÿ“ˆ Performance & Analytics: โ€ข /stats - Complete trading statistics โ€ข /performance - Token performance ranking & detailed stats โ€ข /daily - Daily performance (last 10 days) โ€ข /weekly - Weekly performance (last 10 weeks) โ€ข /monthly - Monthly performance (last 10 months) โ€ข /risk - Sharpe ratio, drawdown, VaR โ€ข /version - Bot version & system information โ€ข /trades - Recent trade history ๐Ÿ”” Price Alerts: โ€ข /alarm - List all active alarms โ€ข /alarm BTC 50000 - Set alarm for BTC at $50,000 โ€ข /alarm BTC - Show all BTC alarms โ€ข /alarm 3 - Remove alarm ID 3 ๐Ÿ”„ Automatic Monitoring: โ€ข Real-time order fill alerts โ€ข Position opened/closed notifications โ€ข P&L calculations on trade closure โ€ข Price alarm triggers โ€ข External trade detection & sync โ€ข Auto stats synchronization โ€ข Automatic stop loss placement โ€ข {heartbeat}-second monitoring interval ๐Ÿ“Š Universal Trade Tracking: โ€ข Bot trades: Full logging & notifications โ€ข Platform trades: Auto-detected & synced โ€ข Mobile app trades: Monitored & recorded โ€ข API trades: Tracked & included in stats Type /help for detailed command information. ๐Ÿ”„ Order Monitoring: โ€ข /monitoring - View monitoring status โ€ข /logs - View log file statistics and cleanup โš™๏ธ Configuration: โ€ข Symbol: {symbol} โ€ข Default Token: {symbol} โ€ข Network: {network} ๐Ÿ›ก๏ธ Safety Features: โ€ข All trades logged automatically โ€ข Comprehensive performance tracking โ€ข Real-time balance monitoring โ€ข Risk metrics calculation โ€ข Automatic stop loss protection ๐Ÿ“ฑ Mobile Optimized: โ€ข Quick action buttons โ€ข Instant notifications โ€ข Clean, readable layout โ€ข One-tap commands ๐Ÿ’ก Quick Access: โ€ข /commands or /c - One-tap button menu for all commands โ€ข Buttons below for instant access to key functions For support, contact your bot administrator. """.format( symbol=Config.DEFAULT_TRADING_TOKEN, network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet", risk_enabled=Config.RISK_MANAGEMENT_ENABLED, stop_loss=Config.STOP_LOSS_PERCENTAGE, heartbeat=Config.BOT_HEARTBEAT_SECONDS ) keyboard = [ [ InlineKeyboardButton("๐Ÿ’ฐ Balance", callback_data="balance"), InlineKeyboardButton("๐Ÿ“Š Stats", callback_data="stats") ], [ InlineKeyboardButton("๐Ÿ“ˆ Positions", callback_data="positions"), InlineKeyboardButton("๐Ÿ“‹ Orders", callback_data="orders") ], [ InlineKeyboardButton("๐Ÿ’ต Price", callback_data="price"), InlineKeyboardButton("๐Ÿ“Š Market", callback_data="market") ], [ InlineKeyboardButton("๐Ÿ”„ Recent Trades", callback_data="trades"), InlineKeyboardButton("โš™๏ธ Help", callback_data="help") ] ] reply_markup = InlineKeyboardMarkup(keyboard) # Create custom keyboard for persistent buttons custom_keyboard = self._create_custom_keyboard() # Send message with inline keyboard await update.message.reply_text( welcome_text, parse_mode='HTML', reply_markup=reply_markup ) # If custom keyboard is enabled, send a follow-up message to set the custom keyboard if custom_keyboard: await update.message.reply_text( "โŒจ๏ธ Custom keyboard enabled!\n\nUse the buttons below for quick access to commands:", parse_mode='HTML', reply_markup=custom_keyboard ) async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /help command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return help_text = """ ๐Ÿ”ง Hyperliquid Trading Bot - Complete Guide ๐Ÿ’ผ Account Management: โ€ข /balance - Show account balance โ€ข /positions - Show open positions โ€ข /orders - Show open orders โ€ข /balance_adjustments - View deposit/withdrawal history ๐Ÿ“Š Market Data: โ€ข /market - Detailed market data (default token) โ€ข /market BTC - Market data for specific token โ€ข /price - Quick price check (default token) โ€ข /price SOL - Price for specific token ๐Ÿš€ Perps Trading: โ€ข /long BTC 100 - Long BTC with $100 USDC (Market Order) โ€ข /long BTC 100 45000 - Long BTC with $100 USDC at $45,000 (Limit Order) โ€ข /long BTC 100 sl:44000 - Market order with automatic stop loss โ€ข /long BTC 100 45000 sl:44000 - Limit order with automatic stop loss โ€ข /short ETH 50 - Short ETH with $50 USDC (Market Order) โ€ข /short ETH 50 3500 - Short ETH with $50 USDC at $3,500 (Limit Order) โ€ข /short ETH 50 sl:3600 - Market order with automatic stop loss โ€ข /short ETH 50 3500 sl:3600 - Limit order with automatic stop loss โ€ข /exit BTC - Close BTC position with Market Order ๐Ÿ›ก๏ธ Risk Management: โ€ข /sl BTC 44000 - Set stop loss for BTC at $44,000 โ€ข /tp BTC 50000 - Set take profit for BTC at $50,000 ๐Ÿšจ Automatic Stop Loss: โ€ข Enabled: {risk_enabled} โ€ข Stop Loss: {stop_loss}% (automatic execution) โ€ข Monitoring: Every {heartbeat} seconds โ€ข Order-based: Use sl:price parameter for automatic placement ๐Ÿ“‹ Order Management: โ€ข /orders - Show all open orders โ€ข /orders BTC - Show open orders for BTC only โ€ข /coo BTC - Cancel all open orders for BTC ๐Ÿ“ˆ Statistics & Analytics: โ€ข /stats - Complete trading statistics โ€ข /performance - Win rate, profit factor, etc. โ€ข /risk - Sharpe ratio, drawdown, VaR โ€ข /version - Bot version & system information โ€ข /trades - Recent trade history ๐Ÿ”” Price Alerts: โ€ข /alarm - List all active alarms โ€ข /alarm BTC 50000 - Set alarm for BTC at $50,000 โ€ข /alarm BTC - Show all BTC alarms โ€ข /alarm 3 - Remove alarm ID 3 ๐Ÿ”„ Order Monitoring: โ€ข /monitoring - View monitoring status โ€ข /logs - View log file statistics and cleanup โš™๏ธ Configuration: โ€ข Symbol: {symbol} โ€ข Default Token: {symbol} โ€ข Network: {network} ๐Ÿ›ก๏ธ Safety Features: โ€ข All trades logged automatically โ€ข Comprehensive performance tracking โ€ข Real-time balance monitoring โ€ข Deposit/withdrawal tracking (hourly) โ€ข Risk metrics calculation โ€ข Automatic stop loss placement ๐Ÿ“ฑ Mobile Optimized: โ€ข Quick action buttons โ€ข Instant notifications โ€ข Clean, readable layout โ€ข One-tap commands For support, contact your bot administrator. """.format( symbol=Config.DEFAULT_TRADING_TOKEN, network="Testnet" if Config.HYPERLIQUID_TESTNET else "Mainnet", risk_enabled=Config.RISK_MANAGEMENT_ENABLED, stop_loss=Config.STOP_LOSS_PERCENTAGE, heartbeat=Config.BOT_HEARTBEAT_SECONDS ) await update.message.reply_text(help_text, parse_mode='HTML') async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /commands and /c command with quick action buttons.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return commands_text = """ ๐Ÿ“ฑ Quick Commands Tap any button below for instant access to bot functions: ๐Ÿ’ก Pro Tip: These buttons work the same as typing the commands manually, but faster! """ keyboard = [ [ InlineKeyboardButton("๐Ÿ’ฐ Balance", callback_data="balance"), InlineKeyboardButton("๐Ÿ“ˆ Positions", callback_data="positions") ], [ InlineKeyboardButton("๐Ÿ“‹ Orders", callback_data="orders"), InlineKeyboardButton("๐Ÿ“Š Stats", callback_data="stats") ], [ InlineKeyboardButton("๐Ÿ’ต Price", callback_data="price"), InlineKeyboardButton("๐Ÿ“Š Market", callback_data="market") ], [ InlineKeyboardButton("๐Ÿ† Performance", callback_data="performance"), InlineKeyboardButton("๐Ÿ”” Alarms", callback_data="alarm") ], [ InlineKeyboardButton("๐Ÿ“… Daily", callback_data="daily"), InlineKeyboardButton("๐Ÿ“Š Weekly", callback_data="weekly") ], [ InlineKeyboardButton("๐Ÿ“† Monthly", callback_data="monthly"), InlineKeyboardButton("๐Ÿ”„ Trades", callback_data="trades") ], [ InlineKeyboardButton("๐Ÿ”„ Monitoring", callback_data="monitoring"), InlineKeyboardButton("๐Ÿ“ Logs", callback_data="logs") ], [ InlineKeyboardButton("โš™๏ธ Help", callback_data="help") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(commands_text, parse_mode='HTML', reply_markup=reply_markup) async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /stats command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return # Get current balance for stats balance = self.client.get_balance() current_balance = 0 if balance and balance.get('total'): current_balance = float(balance['total'].get('USDC', 0)) stats_message = self.stats.format_stats_message(current_balance) await update.message.reply_text(stats_message, parse_mode='HTML') async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /trades command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return recent_trades = self.stats.get_recent_trades(10) if not recent_trades: await update.message.reply_text("๐Ÿ“ No trades recorded yet.") return trades_text = "๐Ÿ”„ Recent Trades\n\n" for trade in reversed(recent_trades[-5:]): # Show last 5 trades timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M') side_emoji = "๐ŸŸข" if trade['side'] == 'buy' else "๐Ÿ”ด" trades_text += f"{side_emoji} {trade['side'].upper()} {trade['amount']} {trade['symbol']}\n" trades_text += f" ๐Ÿ’ฐ ${trade['price']:,.2f} | ๐Ÿ’ต ${trade['value']:,.2f}\n" trades_text += f" ๐Ÿ“… {timestamp}\n\n" await update.message.reply_text(trades_text, parse_mode='HTML') async def balance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /balance command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return balance = self.client.get_balance() if balance: balance_text = "๐Ÿ’ฐ Account Balance\n\n" # Debug: Show raw balance structure (can be removed after debugging) logger.debug(f"Raw balance data: {balance}") # CCXT balance structure includes 'free', 'used', and 'total' total_balance = balance.get('total', {}) free_balance = balance.get('free', {}) used_balance = balance.get('used', {}) if total_balance: total_value = 0 available_value = 0 # Display individual assets for asset, amount in total_balance.items(): if float(amount) > 0: free_amount = float(free_balance.get(asset, 0)) used_amount = float(used_balance.get(asset, 0)) balance_text += f"๐Ÿ’ต {asset}:\n" balance_text += f" ๐Ÿ“Š Total: {amount}\n" balance_text += f" โœ… Available: {free_amount}\n" if used_amount > 0: balance_text += f" ๐Ÿ”’ In Use: {used_amount}\n" balance_text += "\n" # Calculate totals (convert all to USDC equivalent for summary) if asset == 'USDC': total_value += float(amount) available_value += free_amount else: # For non-USDC assets, add to totals (assuming 1:1 for now, could be enhanced with price conversion) total_value += float(amount) available_value += free_amount # Summary section balance_text += f"๐Ÿ’ผ Portfolio Summary:\n" balance_text += f" ๐Ÿ’ฐ Total Value: ${total_value:,.2f}\n" balance_text += f" ๐Ÿš€ Available for Trading: ${available_value:,.2f}\n" if total_value - available_value > 0: balance_text += f" ๐Ÿ”’ In Active Use: ${total_value - available_value:,.2f}\n" # Add P&L summary basic_stats = self.stats.get_basic_stats() if basic_stats['initial_balance'] > 0: # Use USDC balance for P&L calculation usdc_total = float(total_balance.get('USDC', 0)) pnl = usdc_total - basic_stats['initial_balance'] pnl_percent = (pnl / basic_stats['initial_balance']) * 100 balance_text += f"\n๐Ÿ“Š Performance (USDC):\n" balance_text += f" ๐Ÿ’ต P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)\n" balance_text += f" ๐Ÿ“ˆ Initial: ${basic_stats['initial_balance']:,.2f}" else: balance_text += "๐Ÿ“ญ No balance data available" else: balance_text = "โŒ Could not fetch balance data" await update.message.reply_text(balance_text, parse_mode='HTML') async def positions_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /positions command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return positions = self.client.get_positions() if positions is not None: # Successfully fetched (could be empty list) positions_text = "๐Ÿ“ˆ Open Positions\n\n" # Filter for actual open positions open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0] if open_positions: total_unrealized = 0 total_position_value = 0 for position in open_positions: symbol = position.get('symbol', 'Unknown') contracts = float(position.get('contracts', 0)) unrealized_pnl = float(position.get('unrealizedPnl', 0)) # Use the correct field name for entry price entry_price = float(position.get('entryPrice', 0)) # Changed from 'entryPx' to 'entryPrice' # Try to get position value from notional first, then calculate if not available notional = position.get('notional') if notional is not None: position_value = float(notional) else: # Fallback calculation position_value = abs(contracts) * entry_price # Calculate P&L percentage based on position value pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0 pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด" # Extract token name for cleaner display token = symbol.split('/')[0] if '/' in symbol else symbol position_type = "LONG" if contracts > 0 else "SHORT" positions_text += f"๐Ÿ“Š {token} ({position_type})\n" positions_text += f" ๐Ÿ“ Size: {abs(contracts):.6f} {token}\n" positions_text += f" ๐Ÿ’ฐ Entry: ${entry_price:,.2f}\n" positions_text += f" ๐Ÿ’ต Value: ${position_value:,.2f}\n" positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n\n" total_unrealized += unrealized_pnl total_position_value += position_value # Calculate overall P&L percentage total_pnl_percentage = (total_unrealized / total_position_value * 100) if total_position_value > 0 else 0 total_pnl_emoji = "๐ŸŸข" if total_unrealized >= 0 else "๐Ÿ”ด" positions_text += f"๐Ÿ’ผ Total Portfolio:\n" positions_text += f" ๐Ÿ’ต Total Value: ${total_position_value:,.2f}\n" positions_text += f" {total_pnl_emoji} Total P&L: ${total_unrealized:,.2f} ({total_pnl_percentage:+.2f}%)" else: positions_text += "๐Ÿ“ญ No open positions currently\n\n" positions_text += "๐Ÿš€ Ready to start trading!\n" positions_text += "Use /long or /short commands to open positions." else: # Actual API error positions_text = "โŒ Could not fetch positions data\n\n" positions_text += "๐Ÿ”„ Please try again in a moment.\n" positions_text += "If the issue persists, check your connection." await update.message.reply_text(positions_text, parse_mode='HTML') async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /orders command with optional token filter.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return # Check if token filter is provided token_filter = None if context.args and len(context.args) >= 1: token_filter = context.args[0].upper() orders = self.client.get_open_orders() # Debug: Log what we got from orders logger.debug(f"Raw orders data: {orders}") logger.debug(f"Orders type: {type(orders)}, Length: {len(orders) if orders else 'None'}") if orders is not None: # Successfully fetched (could be empty list) if token_filter: orders_text = f"๐Ÿ“‹ Open Orders - {token_filter}\n\n" # Filter orders for specific token target_symbol = f"{token_filter}/USDC:USDC" filtered_orders = [order for order in orders if order.get('symbol') == target_symbol] else: orders_text = "๐Ÿ“‹ All Open Orders\n\n" filtered_orders = orders if filtered_orders and len(filtered_orders) > 0: for order in filtered_orders: symbol = order.get('symbol', 'Unknown') side = order.get('side', 'Unknown') amount = order.get('amount', 0) price = order.get('price', 0) order_id = order.get('id', 'Unknown') # Extract token from symbol for display token = symbol.split('/')[0] if '/' in symbol else symbol side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" orders_text += f"{side_emoji} {token}\n" orders_text += f" ๐Ÿ“Š {side.upper()} {amount} @ ${price:,.2f}\n" orders_text += f" ๐Ÿ’ต Value: ${float(amount) * float(price):,.2f}\n" orders_text += f" ๐Ÿ”‘ ID: {order_id}\n\n" # Add helpful commands if token_filter: orders_text += f"๐Ÿ’ก Quick Actions:\n" orders_text += f"โ€ข /coo {token_filter} - Cancel all {token_filter} orders\n" orders_text += f"โ€ข /orders - View all orders" else: orders_text += f"๐Ÿ’ก Filter by token: /orders BTC, /orders ETH" else: if token_filter: orders_text += f"๐Ÿ“ญ No open orders for {token_filter}\n\n" orders_text += f"๐Ÿ’ก No pending {token_filter} orders found.\n" orders_text += f"Use /long {token_filter} 100 or /short {token_filter} 100 to create new orders." else: orders_text += "๐Ÿ“ญ No open orders currently\n\n" orders_text += "๐Ÿ’ก All clear! No pending orders.\n" orders_text += "Use /long or /short commands to place new orders." else: # Actual API error orders_text = "โŒ Could not fetch orders data\n\n" orders_text += "๐Ÿ”„ Please try again in a moment.\n" orders_text += "If the issue persists, check your connection." await update.message.reply_text(orders_text, parse_mode='HTML') async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /market command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return # Check if token is provided as argument if context.args and len(context.args) >= 1: token = context.args[0].upper() else: token = Config.DEFAULT_TRADING_TOKEN # Convert token to full symbol format for API symbol = f"{token}/USDC:USDC" market_data = self.client.get_market_data(symbol) if market_data and market_data.get('ticker'): try: ticker = market_data['ticker'] orderbook = market_data.get('orderbook', {}) # Safely extract ticker data with fallbacks current_price = float(ticker.get('last') or 0) high_24h = float(ticker.get('high') or 0) low_24h = float(ticker.get('low') or 0) volume_24h = ticker.get('baseVolume') or ticker.get('volume') or 'N/A' market_text = f"๐Ÿ“Š Market Data - {token}\n\n" if current_price > 0: market_text += f"๐Ÿ’ต Current Price: ${current_price:,.2f}\n" else: market_text += f"๐Ÿ’ต Current Price: N/A\n" if high_24h > 0: market_text += f"๐Ÿ“ˆ 24h High: ${high_24h:,.2f}\n" else: market_text += f"๐Ÿ“ˆ 24h High: N/A\n" if low_24h > 0: market_text += f"๐Ÿ“‰ 24h Low: ${low_24h:,.2f}\n" else: market_text += f"๐Ÿ“‰ 24h Low: N/A\n" market_text += f"๐Ÿ“Š 24h Volume: {volume_24h}\n\n" # Handle orderbook data safely if orderbook and orderbook.get('bids') and orderbook.get('asks'): try: bids = orderbook.get('bids', []) asks = orderbook.get('asks', []) if bids and asks and len(bids) > 0 and len(asks) > 0: best_bid = float(bids[0][0]) if bids[0][0] else 0 best_ask = float(asks[0][0]) if asks[0][0] else 0 if best_bid > 0 and best_ask > 0: spread = best_ask - best_bid spread_percent = (spread / best_ask * 100) if best_ask > 0 else 0 market_text += f"๐ŸŸข Best Bid: ${best_bid:,.2f}\n" market_text += f"๐Ÿ”ด Best Ask: ${best_ask:,.2f}\n" market_text += f"๐Ÿ“ Spread: ${spread:.2f} ({spread_percent:.3f}%)\n" else: market_text += f"๐Ÿ“‹ Orderbook: Data unavailable\n" else: market_text += f"๐Ÿ“‹ Orderbook: No orders available\n" except (IndexError, ValueError, TypeError) as e: market_text += f"๐Ÿ“‹ Orderbook: Error parsing data\n" else: market_text += f"๐Ÿ“‹ Orderbook: Not available\n" # Add usage hint market_text += f"\n๐Ÿ’ก Usage: /market {token} or /market for default" except (ValueError, TypeError) as e: market_text = f"โŒ Error parsing market data\n\n" market_text += f"๐Ÿ”ง Raw data received but couldn't parse values.\n" market_text += f"๐Ÿ“ž Please try again or contact support if this persists." else: market_text = f"โŒ Could not fetch market data for {token}\n\n" market_text += f"๐Ÿ”„ Please try again in a moment.\n" market_text += f"๐ŸŒ Check your network connection.\n" market_text += f"๐Ÿ“ก API may be temporarily unavailable.\n\n" market_text += f"๐Ÿ’ก Usage: /market BTC, /market ETH, /market SOL, etc." await update.message.reply_text(market_text, parse_mode='HTML') async def price_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /price command.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return # Check if token is provided as argument if context.args and len(context.args) >= 1: token = context.args[0].upper() else: token = Config.DEFAULT_TRADING_TOKEN # Convert token to full symbol format for API symbol = f"{token}/USDC:USDC" market_data = self.client.get_market_data(symbol) if market_data and market_data.get('ticker'): try: ticker = market_data['ticker'] price_value = ticker.get('last') if price_value is not None: price = float(price_value) price_text = f"๐Ÿ’ต {token}: ${price:,.2f}" # Add timestamp timestamp = datetime.now().strftime('%H:%M:%S') price_text += f"\nโฐ Updated: {timestamp}" # Add usage hint price_text += f"\n๐Ÿ’ก Usage: /price {symbol} or /price for default" else: price_text = f"๐Ÿ’ต {symbol}: Price not available\nโš ๏ธ Data temporarily unavailable" except (ValueError, TypeError) as e: price_text = f"โŒ Error parsing price for {symbol}\n๐Ÿ”ง Please try again" else: price_text = f"โŒ Could not fetch price for {symbol}\n๐Ÿ”„ Please try again in a moment\n\n" price_text += f"๐Ÿ’ก Usage: /price BTC, /price ETH, /price SOL, etc." await update.message.reply_text(price_text, parse_mode='HTML') async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle inline keyboard button presses.""" query = update.callback_query await query.answer() if not self.is_authorized(query.message.chat_id): await query.edit_message_text("โŒ Unauthorized access.") return callback_data = query.data # Handle trading confirmations if callback_data.startswith('confirm_long_'): parts = callback_data.split('_') token = parts[2] usdc_amount = float(parts[3]) try: price = float(parts[4]) except (ValueError, TypeError): price = None # Will be handled in execute_long_order is_limit = len(parts) > 5 and parts[5] == 'limit' # Parse stop loss if present stop_loss_price = None if len(parts) > 6 and parts[6] == 'sl': try: stop_loss_price = float(parts[7]) except (ValueError, TypeError): stop_loss_price = None elif len(parts) > 5 and parts[5] == 'sl': try: stop_loss_price = float(parts[6]) except (ValueError, TypeError): stop_loss_price = None await self._execute_long_order(query, token, usdc_amount, price, is_limit, stop_loss_price) return elif callback_data.startswith('confirm_short_'): parts = callback_data.split('_') token = parts[2] usdc_amount = float(parts[3]) try: price = float(parts[4]) except (ValueError, TypeError): price = None # Will be handled in execute_short_order is_limit = len(parts) > 5 and parts[5] == 'limit' # Parse stop loss if present stop_loss_price = None if len(parts) > 6 and parts[6] == 'sl': try: stop_loss_price = float(parts[7]) except (ValueError, TypeError): stop_loss_price = None elif len(parts) > 5 and parts[5] == 'sl': try: stop_loss_price = float(parts[6]) except (ValueError, TypeError): stop_loss_price = None await self._execute_short_order(query, token, usdc_amount, price, is_limit, stop_loss_price) return elif callback_data.startswith('confirm_exit_'): parts = callback_data.split('_') token = parts[2] exit_side = parts[3] contracts = float(parts[4]) price = float(parts[5]) await self._execute_exit_order(query, token, exit_side, contracts, price) return elif callback_data.startswith('confirm_coo_'): parts = callback_data.split('_') token = parts[2] await self._execute_coo(query, token) return elif callback_data.startswith('confirm_sl_'): parts = callback_data.split('_') token = parts[2] exit_side = parts[3] contracts = float(parts[4]) price = float(parts[5]) await self._execute_sl_order(query, token, exit_side, contracts, price) return elif callback_data.startswith('confirm_tp_'): parts = callback_data.split('_') token = parts[2] exit_side = parts[3] contracts = float(parts[4]) price = float(parts[5]) await self._execute_tp_order(query, token, exit_side, contracts, price) return elif callback_data == 'cancel_order': await query.edit_message_text("โŒ Order cancelled.") return # Create a fake update object for reusing command handlers fake_update = Update( update_id=update.update_id, message=query.message, callback_query=query ) # Handle regular button callbacks if callback_data == "balance": await self.balance_command(fake_update, context) elif callback_data == "stats": await self.stats_command(fake_update, context) elif callback_data == "positions": await self.positions_command(fake_update, context) elif callback_data == "orders": await self.orders_command(fake_update, context) elif callback_data == "market": await self.market_command(fake_update, context) elif callback_data == "price": await self.price_command(fake_update, context) elif callback_data == "trades": await self.trades_command(fake_update, context) elif callback_data == "help": await self.help_command(fake_update, context) elif callback_data == "performance": await self.performance_command(fake_update, context) elif callback_data == "alarm": await self.alarm_command(fake_update, context) elif callback_data == "daily": await self.daily_command(fake_update, context) elif callback_data == "weekly": await self.weekly_command(fake_update, context) elif callback_data == "monthly": await self.monthly_command(fake_update, context) elif callback_data == "monitoring": await self.monitoring_command(fake_update, context) elif callback_data == "logs": await self.logs_command(fake_update, context) async def _execute_long_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool, stop_loss_price: float = None): """Execute a long order.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Opening long position...") # Validate price if price is None or price <= 0: # Try to get current market price market_data = self.client.get_market_data(symbol) if market_data and market_data.get('ticker'): price = float(market_data['ticker'].get('last', 0)) if price <= 0: await query.edit_message_text("โŒ Unable to get valid market price. Please try again.") return else: await query.edit_message_text("โŒ Unable to fetch market price. Please try again.") return # Calculate token amount based on USDC value and price token_amount = usdc_amount / price # Place order (limit or market) if is_limit: order = self.client.place_limit_order(symbol, 'buy', token_amount, price) else: order = self.client.place_market_order(symbol, 'buy', token_amount) if order: # Record the trade in stats order_id = order.get('id', 'N/A') actual_price = order.get('average', price) # Use actual fill price if available if actual_price is None or actual_price <= 0: # This should not happen due to our price validation above, but extra safety actual_price = price action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'buy', token_amount, actual_price, order_id, "bot") # Save pending stop loss if provided if stop_loss_price is not None: self.pending_stop_losses[order_id] = { 'token': token, 'symbol': symbol, 'stop_price': stop_loss_price, 'side': 'sell', # For long position, stop loss is a sell order 'amount': token_amount, 'order_type': 'long', 'original_order_id': order_id, 'is_limit': is_limit } self._save_bot_state() # Save state after adding pending stop loss logger.info(f"๐Ÿ’พ Saved pending stop loss for order {order_id}: sell {token_amount:.6f} {token} @ ${stop_loss_price}") success_message = f""" โœ… Long Position {'Placed' if is_limit else 'Opened'} Successfully! ๐Ÿ“Š Order Details: โ€ข Token: {token} โ€ข Direction: LONG (Buy) โ€ข Amount: {token_amount:.6f} {token} โ€ข Price: ${price:,.2f} โ€ข USDC Value: ~${usdc_amount:,.2f} โ€ข Order Type: {'Limit' if is_limit else 'Market'} Order โ€ข Order ID: {order_id} """ # Add stop loss confirmation if provided if stop_loss_price is not None: success_message += f""" ๐Ÿ›‘ Stop Loss Saved: โ€ข Stop Price: ${stop_loss_price:,.2f} โ€ข Will be placed automatically when order fills โ€ข Status: PENDING โณ """ success_message += f"\n๐Ÿš€ Your {'limit order has been placed' if is_limit else 'long position is now active'}!" await query.edit_message_text(success_message, parse_mode='HTML') logger.info(f"Long {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})") else: await query.edit_message_text(f"โŒ Failed to {'place limit order' if is_limit else 'open long position'}. Please try again.") except Exception as e: error_message = f"โŒ Error {'placing limit order' if is_limit else 'opening long position'}: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error in long order: {e}") async def _execute_short_order(self, query, token: str, usdc_amount: float, price: float, is_limit: bool, stop_loss_price: float = None): """Execute a short order.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Opening short position...") # Validate price if price is None or price <= 0: # Try to get current market price market_data = self.client.get_market_data(symbol) if market_data and market_data.get('ticker'): price = float(market_data['ticker'].get('last', 0)) if price <= 0: await query.edit_message_text("โŒ Unable to get valid market price. Please try again.") return else: await query.edit_message_text("โŒ Unable to fetch market price. Please try again.") return # Calculate token amount based on USDC value and price token_amount = usdc_amount / price # Place order (limit or market) if is_limit: order = self.client.place_limit_order(symbol, 'sell', token_amount, price) else: order = self.client.place_market_order(symbol, 'sell', token_amount) if order: # Record the trade in stats order_id = order.get('id', 'N/A') actual_price = order.get('average', price) # Use actual fill price if available if actual_price is None or actual_price <= 0: # This should not happen due to our price validation above, but extra safety actual_price = price action_type = self.stats.record_trade_with_enhanced_tracking(symbol, 'sell', token_amount, actual_price, order_id, "bot") # Save pending stop loss if provided if stop_loss_price is not None: self.pending_stop_losses[order_id] = { 'token': token, 'symbol': symbol, 'stop_price': stop_loss_price, 'side': 'buy', # For short position, stop loss is a buy order 'amount': token_amount, 'order_type': 'short', 'original_order_id': order_id, 'is_limit': is_limit } self._save_bot_state() # Save state after adding pending stop loss logger.info(f"๐Ÿ’พ Saved pending stop loss for order {order_id}: buy {token_amount:.6f} {token} @ ${stop_loss_price}") success_message = f""" โœ… Short Position {'Placed' if is_limit else 'Opened'} Successfully! ๐Ÿ“Š Order Details: โ€ข Token: {token} โ€ข Direction: SHORT (Sell) โ€ข Amount: {token_amount:.6f} {token} โ€ข Price: ${price:,.2f} โ€ข USDC Value: ~${usdc_amount:,.2f} โ€ข Order Type: {'Limit' if is_limit else 'Market'} Order โ€ข Order ID: {order_id} """ # Add stop loss confirmation if provided if stop_loss_price is not None: success_message += f""" ๐Ÿ›‘ Stop Loss Saved: โ€ข Stop Price: ${stop_loss_price:,.2f} โ€ข Will be placed automatically when order fills โ€ข Status: PENDING โณ """ success_message += f"\n๐Ÿ“‰ Your {'limit order has been placed' if is_limit else 'short position is now active'}!" await query.edit_message_text(success_message, parse_mode='HTML') logger.info(f"Short {'limit order placed' if is_limit else 'position opened'}: {token_amount:.6f} {token} @ ${price} ({'Limit' if is_limit else 'Market'})") else: await query.edit_message_text(f"โŒ Failed to {'place limit order' if is_limit else 'open short position'}. Please try again.") except Exception as e: error_message = f"โŒ Error {'placing limit order' if is_limit else 'opening short position'}: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error in short order: {e}") async def _execute_exit_order(self, query, token: str, exit_side: str, contracts: float, price: float): """Execute an exit order.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Closing position...") # Place market order to close position order = self.client.place_market_order(symbol, exit_side, contracts) if order: # Record the trade in stats order_id = order.get('id', 'N/A') actual_price = order.get('average', price) # Use actual fill price if available if actual_price is None or actual_price <= 0: # Fallback to ensure we have a valid price actual_price = price action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot") position_type = "LONG" if exit_side == "sell" else "SHORT" success_message = f""" โœ… Position Closed Successfully! ๐Ÿ“Š Exit Details: โ€ข Token: {token} โ€ข Position Closed: {position_type} โ€ข Exit Side: {exit_side.upper()} โ€ข Amount: {contracts} {token} โ€ข Est. Price: ~${price:,.2f} โ€ข Order Type: Market Order โ€ข Order ID: {order_id} ๐ŸŽฏ Position Summary: โ€ข Status: CLOSED โ€ข Exit Value: ~${contracts * price:,.2f} ๐Ÿ“Š Use /stats to see updated performance metrics. """ await query.edit_message_text(success_message, parse_mode='HTML') logger.info(f"Position closed: {exit_side} {contracts} {token} @ ~${price}") else: await query.edit_message_text("โŒ Failed to close position. Please try again.") except Exception as e: error_message = f"โŒ Error closing position: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error closing position: {e}") async def _execute_coo(self, query, token: str): """Execute cancel open orders for a specific token.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Cancelling all orders...") # Get current orders for this token all_orders = self.client.get_open_orders() if all_orders is None: await query.edit_message_text(f"โŒ Could not fetch orders to cancel {token} orders") return # Filter orders for the specific token token_orders = [order for order in all_orders if order.get('symbol') == symbol] if not token_orders: await query.edit_message_text(f"๐Ÿ“ญ No open orders found for {token}") return # Cancel each order cancelled_orders = [] failed_orders = [] for order in token_orders: order_id = order.get('id') if order_id: try: success = self.client.cancel_order(order_id, symbol) if success: cancelled_orders.append(order) else: failed_orders.append(order) except Exception as e: logger.error(f"Failed to cancel order {order_id}: {e}") failed_orders.append(order) # Create result message result_message = f""" โœ… Cancel Orders Results ๐Ÿ“Š Summary: โ€ข Token: {token} โ€ข Cancelled: {len(cancelled_orders)} orders โ€ข Failed: {len(failed_orders)} orders โ€ข Total Attempted: {len(token_orders)} orders """ if cancelled_orders: result_message += f"\n๐Ÿ—‘๏ธ Successfully Cancelled:\n" for order in cancelled_orders: side = order.get('side', 'Unknown') amount = order.get('amount', 0) price = order.get('price', 0) side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f}\n" if failed_orders: result_message += f"\nโŒ Failed to Cancel:\n" for order in failed_orders: side = order.get('side', 'Unknown') amount = order.get('amount', 0) price = order.get('price', 0) order_id = order.get('id', 'Unknown') side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" result_message += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (ID: {order_id})\n" if len(cancelled_orders) == len(token_orders): result_message += f"\n๐ŸŽ‰ All {token} orders successfully cancelled!" elif len(cancelled_orders) > 0: result_message += f"\nโš ๏ธ Some orders cancelled. Check failed orders above." else: result_message += f"\nโŒ Could not cancel any {token} orders." await query.edit_message_text(result_message, parse_mode='HTML') logger.info(f"COO executed for {token}: {len(cancelled_orders)}/{len(token_orders)} orders cancelled") except Exception as e: error_message = f"โŒ Error cancelling {token} orders: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error in COO execution: {e}") async def _execute_sl_order(self, query, token: str, exit_side: str, contracts: float, price: float): """Execute a stop loss order.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Setting stop loss...") # Place stop loss order order = self.client.place_stop_loss_order(symbol, exit_side, contracts, price) if order: # Record the trade in stats order_id = order.get('id', 'N/A') actual_price = order.get('average', price) # Use actual fill price if available action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot") position_type = "LONG" if exit_side == "sell" else "SHORT" success_message = f""" โœ… Stop Loss Order Set Successfully! ๐Ÿ“Š Stop Loss Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Size: {contracts} contracts โ€ข Stop Price: ${price:,.2f} โ€ข Action: {exit_side.upper()} (Close {position_type}) โ€ข Amount: {contracts} {token} โ€ข Order Type: Limit Order โ€ข Order ID: {order_id} ๐ŸŽฏ Stop Loss Execution: โ€ข Status: SET โ€ข Exit Value: ~${contracts * price:,.2f} ๐Ÿ“Š Use /stats to see updated performance metrics. """ await query.edit_message_text(success_message, parse_mode='HTML') logger.info(f"Stop loss set: {exit_side} {contracts} {token} @ ${price}") else: await query.edit_message_text("โŒ Failed to set stop loss. Please try again.") except Exception as e: error_message = f"โŒ Error setting stop loss: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error setting stop loss: {e}") async def _execute_tp_order(self, query, token: str, exit_side: str, contracts: float, price: float): """Execute a take profit order.""" symbol = f"{token}/USDC:USDC" try: await query.edit_message_text("โณ Setting take profit...") # Place take profit order order = self.client.place_take_profit_order(symbol, exit_side, contracts, price) if order: # Record the trade in stats order_id = order.get('id', 'N/A') actual_price = order.get('average', price) # Use actual fill price if available if actual_price is None or actual_price <= 0: # Fallback to ensure we have a valid price actual_price = price action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts, actual_price, order_id, "bot") position_type = "LONG" if exit_side == "sell" else "SHORT" success_message = f""" โœ… Take Profit Order Set Successfully! ๐Ÿ“Š Take Profit Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Size: {contracts} contracts โ€ข Target Price: ${price:,.2f} โ€ข Action: {exit_side.upper()} (Close {position_type}) โ€ข Amount: {contracts} {token} โ€ข Order Type: Limit Order โ€ข Order ID: {order_id} ๐ŸŽฏ Take Profit Execution: โ€ข Status: SET โ€ข Exit Value: ~${contracts * price:,.2f} ๐Ÿ“Š Use /stats to see updated performance metrics. """ await query.edit_message_text(success_message, parse_mode='HTML') logger.info(f"Take profit set: {exit_side} {contracts} {token} @ ${price}") else: await query.edit_message_text("โŒ Failed to set take profit. Please try again.") except Exception as e: error_message = f"โŒ Error setting take profit: {str(e)}" await query.edit_message_text(error_message) logger.error(f"Error setting take profit: {e}") async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle messages from custom keyboard buttons (without /).""" if not self.is_authorized(update.effective_chat.id): return message_text = update.message.text.lower() # Map clean button text to command handlers command_handlers = { 'daily': self.daily_command, 'performance': self.performance_command, 'balance': self.balance_command, 'stats': self.stats_command, 'positions': self.positions_command, 'orders': self.orders_command, 'price': self.price_command, 'market': self.market_command, 'help': self.help_command, 'commands': self.commands_command } # Execute the corresponding command handler if message_text in command_handlers: await command_handlers[message_text](update, context) async def unknown_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle unknown commands.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return await update.message.reply_text( "โ“ Unknown command. Use /help to see available commands or tap the buttons in /start." ) def setup_handlers(self): """Set up command handlers for the bot.""" if not self.application: return # Command handlers self.application.add_handler(CommandHandler("start", self.start_command)) self.application.add_handler(CommandHandler("help", self.help_command)) self.application.add_handler(CommandHandler("commands", self.commands_command)) self.application.add_handler(CommandHandler("c", self.commands_command)) self.application.add_handler(CommandHandler("balance", self.balance_command)) self.application.add_handler(CommandHandler("positions", self.positions_command)) self.application.add_handler(CommandHandler("orders", self.orders_command)) self.application.add_handler(CommandHandler("market", self.market_command)) self.application.add_handler(CommandHandler("price", self.price_command)) self.application.add_handler(CommandHandler("stats", self.stats_command)) self.application.add_handler(CommandHandler("trades", self.trades_command)) self.application.add_handler(CommandHandler("long", self.long_command)) self.application.add_handler(CommandHandler("short", self.short_command)) self.application.add_handler(CommandHandler("exit", self.exit_command)) self.application.add_handler(CommandHandler("coo", self.coo_command)) self.application.add_handler(CommandHandler("sl", self.sl_command)) self.application.add_handler(CommandHandler("tp", self.tp_command)) self.application.add_handler(CommandHandler("monitoring", self.monitoring_command)) self.application.add_handler(CommandHandler("alarm", self.alarm_command)) self.application.add_handler(CommandHandler("logs", self.logs_command)) self.application.add_handler(CommandHandler("performance", self.performance_command)) self.application.add_handler(CommandHandler("daily", self.daily_command)) self.application.add_handler(CommandHandler("weekly", self.weekly_command)) self.application.add_handler(CommandHandler("monthly", self.monthly_command)) self.application.add_handler(CommandHandler("risk", self.risk_command)) self.application.add_handler(CommandHandler("version", self.version_command)) self.application.add_handler(CommandHandler("balance_adjustments", self.balance_adjustments_command)) self.application.add_handler(CommandHandler("keyboard", self.keyboard_command)) # Callback query handler for inline keyboards self.application.add_handler(CallbackQueryHandler(self.button_callback)) # Handle clean keyboard button messages (without /) self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_keyboard_message)) # Handle unknown commands self.application.add_handler(MessageHandler(filters.COMMAND, self.unknown_command)) async def run(self): """Run the Telegram bot.""" if not Config.TELEGRAM_BOT_TOKEN: logger.error("โŒ TELEGRAM_BOT_TOKEN not configured") return if not Config.TELEGRAM_CHAT_ID: logger.error("โŒ TELEGRAM_CHAT_ID not configured") return try: # Create application self.application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build() # Set up handlers self.setup_handlers() logger.info("๐Ÿš€ Starting Telegram trading bot...") # Initialize the application await self.application.initialize() # Send startup notification await self.send_message( f"๐Ÿค– Manual Trading Bot v{self.version} Started\n\n" f"โœ… Connected to Hyperliquid {'Testnet' if Config.HYPERLIQUID_TESTNET else 'Mainnet'}\n" f"๐Ÿ“Š Default Symbol: {Config.DEFAULT_TRADING_TOKEN}\n" f"๐Ÿ“ฑ Manual trading ready!\n" f"๐Ÿ”„ Order monitoring: Active ({Config.BOT_HEARTBEAT_SECONDS}s interval)\n" f"๐Ÿ”„ External trade monitoring: Active\n" f"๐Ÿ”” Price alarms: Active\n" f"๐Ÿ“Š Auto stats sync: Enabled\n" f"๐Ÿ“ Logging: {'File + Console' if Config.LOG_TO_FILE else 'Console only'}\n" f"โฐ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" "Use /start for quick actions or /help for all commands." ) # Perform initial log cleanup try: cleanup_logs(days_to_keep=30) logger.info("๐Ÿงน Initial log cleanup completed") except Exception as e: logger.warning(f"โš ๏ธ Initial log cleanup failed: {e}") # Start the application await self.application.start() # Start order monitoring await self.start_order_monitoring() # Start polling for updates manually logger.info("๐Ÿ”„ Starting update polling...") # Get updates in a loop last_update_id = 0 while True: try: # Get updates from Telegram updates = await self.application.bot.get_updates( offset=last_update_id + 1, timeout=30, allowed_updates=None ) # Process each update for update in updates: last_update_id = update.update_id # Process the update through the application await self.application.process_update(update) except Exception as e: logger.error(f"Error processing updates: {e}") await asyncio.sleep(5) # Wait before retrying except asyncio.CancelledError: logger.info("๐Ÿ›‘ Bot polling cancelled") raise except Exception as e: logger.error(f"โŒ Error in telegram bot: {e}") raise finally: # Clean shutdown try: await self.stop_order_monitoring() if self.application: await self.application.stop() await self.application.shutdown() except Exception as e: logger.error(f"Error during shutdown: {e}") async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /long command for opening long positions.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await update.message.reply_text( "โŒ Usage: /long [token] [USDC amount] [price (optional)] [sl:price (optional)]\n" "Examples:\n" "โ€ข /long BTC 100 - Market order\n" "โ€ข /long BTC 100 45000 - Limit order at $45,000\n" "โ€ข /long BTC 100 sl:44000 - Market order with stop loss at $44,000\n" "โ€ข /long BTC 100 45000 sl:44000 - Limit order at $45,000 with stop loss at $44,000" ) return token = context.args[0].upper() usdc_amount = float(context.args[1]) # Parse arguments for price and stop loss limit_price = None stop_loss_price = None # Parse remaining arguments for i, arg in enumerate(context.args[2:], 2): if arg.startswith('sl:'): # Stop loss parameter try: stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix except ValueError: await update.message.reply_text("โŒ Invalid stop loss price format. Use sl:price (e.g., sl:44000)") return elif limit_price is None: # First non-sl parameter is the limit price try: limit_price = float(arg) except ValueError: await update.message.reply_text("โŒ Invalid limit price format. Please use numbers only.") return # Determine order type if limit_price: order_type = "Limit" order_description = f"at ${limit_price:,.2f}" else: order_type = "Market" order_description = "at current market price" # Convert token to full symbol format for Hyperliquid symbol = f"{token}/USDC:USDC" # Get current market price to calculate amount and for display market_data = self.client.get_market_data(symbol) if not market_data: await update.message.reply_text(f"โŒ Could not fetch price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await update.message.reply_text(f"โŒ Invalid price for {token}") return # Validate stop loss price for long positions if stop_loss_price is not None: entry_price = limit_price if limit_price else current_price if stop_loss_price >= entry_price: await update.message.reply_text( f"โŒ Stop loss price should be BELOW entry price for long positions\n\n" f"๐Ÿ“Š Entry Price: ${entry_price:,.2f}\n" f"๐Ÿ›‘ Stop Loss: ${stop_loss_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a lower price like: /long {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 0.95:.0f}" ) return # Calculate token amount based on price (market or limit) calculation_price = limit_price if limit_price else current_price token_amount = usdc_amount / calculation_price # Create confirmation message confirmation_text = f""" ๐ŸŸข Long Position Confirmation ๐Ÿ“Š Order Details: โ€ข Token: {token} โ€ข Direction: LONG (Buy) โ€ข USDC Value: ${usdc_amount:,.2f} โ€ข Current Price: ${current_price:,.2f} โ€ข Order Type: {order_type} Order โ€ข Token Amount: {token_amount:.6f} {token} ๐ŸŽฏ Execution: โ€ข Will buy {token_amount:.6f} {token} {order_description} โ€ข Est. Value: ${token_amount * calculation_price:,.2f} """ # Add stop loss information if provided if stop_loss_price is not None: confirmation_text += f""" ๐Ÿ›‘ Stop Loss: โ€ข Stop Price: ${stop_loss_price:,.2f} โ€ข Will be placed automatically after order fills โ€ข Protection Level: {((calculation_price - stop_loss_price) / calculation_price * 100):.1f}% below entry """ confirmation_text += "\nโš ๏ธ Are you sure you want to open this long position?" # Use limit_price for callback if provided, otherwise current_price callback_price = limit_price if limit_price else current_price callback_data = f"confirm_long_{token}_{usdc_amount}_{callback_price}" if limit_price: callback_data += "_limit" if stop_loss_price is not None: callback_data += f"_sl_{stop_loss_price}" keyboard = [ [ InlineKeyboardButton("โœ… Confirm Long", callback_data=callback_data), InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid USDC amount or price. Please use numbers only.") except Exception as e: await update.message.reply_text(f"โŒ Error processing long command: {e}") async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /short command for opening short positions.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await update.message.reply_text( "โŒ Usage: /short [token] [USDC amount] [price (optional)] [sl:price (optional)]\n" "Examples:\n" "โ€ข /short BTC 100 - Market order\n" "โ€ข /short BTC 100 46000 - Limit order at $46,000\n" "โ€ข /short BTC 100 sl:47000 - Market order with stop loss at $47,000\n" "โ€ข /short BTC 100 46000 sl:47000 - Limit order at $46,000 with stop loss at $47,000" ) return token = context.args[0].upper() usdc_amount = float(context.args[1]) # Parse arguments for price and stop loss limit_price = None stop_loss_price = None # Parse remaining arguments for i, arg in enumerate(context.args[2:], 2): if arg.startswith('sl:'): # Stop loss parameter try: stop_loss_price = float(arg[3:]) # Remove 'sl:' prefix except ValueError: await update.message.reply_text("โŒ Invalid stop loss price format. Use sl:price (e.g., sl:47000)") return elif limit_price is None: # First non-sl parameter is the limit price try: limit_price = float(arg) except ValueError: await update.message.reply_text("โŒ Invalid limit price format. Please use numbers only.") return # Determine order type if limit_price: order_type = "Limit" order_description = f"at ${limit_price:,.2f}" else: order_type = "Market" order_description = "at current market price" # Convert token to full symbol format for Hyperliquid symbol = f"{token}/USDC:USDC" # Get current market price to calculate amount and for display market_data = self.client.get_market_data(symbol) if not market_data: await update.message.reply_text(f"โŒ Could not fetch price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await update.message.reply_text(f"โŒ Invalid price for {token}") return # Validate stop loss price for short positions if stop_loss_price is not None: entry_price = limit_price if limit_price else current_price if stop_loss_price <= entry_price: await update.message.reply_text( f"โŒ Stop loss price should be ABOVE entry price for short positions\n\n" f"๐Ÿ“Š Entry Price: ${entry_price:,.2f}\n" f"๐Ÿ›‘ Stop Loss: ${stop_loss_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a higher price like: /short {token} {usdc_amount} {f'{limit_price} ' if limit_price else ''}sl:{entry_price * 1.05:.0f}" ) return # Calculate token amount based on price (market or limit) calculation_price = limit_price if limit_price else current_price token_amount = usdc_amount / calculation_price # Create confirmation message confirmation_text = f""" ๐Ÿ”ด Short Position Confirmation ๐Ÿ“Š Order Details: โ€ข Token: {token} โ€ข Direction: SHORT (Sell) โ€ข USDC Value: ${usdc_amount:,.2f} โ€ข Current Price: ${current_price:,.2f} โ€ข Order Type: {order_type} Order โ€ข Token Amount: {token_amount:.6f} {token} ๐ŸŽฏ Execution: โ€ข Will sell {token_amount:.6f} {token} {order_description} โ€ข Est. Value: ${token_amount * calculation_price:,.2f} """ # Add stop loss information if provided if stop_loss_price is not None: confirmation_text += f""" ๐Ÿ›‘ Stop Loss: โ€ข Stop Price: ${stop_loss_price:,.2f} โ€ข Will be placed automatically after order fills โ€ข Protection Level: {((stop_loss_price - calculation_price) / calculation_price * 100):.1f}% above entry """ confirmation_text += "\nโš ๏ธ Are you sure you want to open this short position?" # Use limit_price for callback if provided, otherwise current_price callback_price = limit_price if limit_price else current_price callback_data = f"confirm_short_{token}_{usdc_amount}_{callback_price}" if limit_price: callback_data += "_limit" if stop_loss_price is not None: callback_data += f"_sl_{stop_loss_price}" keyboard = [ [ InlineKeyboardButton("โœ… Confirm Short", callback_data=callback_data), InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid USDC amount or price. Please use numbers only.") except Exception as e: await update.message.reply_text(f"โŒ Error processing short command: {e}") async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /exit command for closing positions.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 1: await update.message.reply_text( "โŒ Usage: /exit [token]\n" "Example: /exit BTC" ) return token = context.args[0].upper() symbol = f"{token}/USDC:USDC" # Get current positions to find the position for this token positions = self.client.get_positions() if positions is None: await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position") return # Find the position for this token current_position = None for position in positions: if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0: current_position = position break if not current_position: await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}") return # Extract position details contracts = float(current_position.get('contracts', 0)) entry_price = float(current_position.get('entryPx', 0)) unrealized_pnl = float(current_position.get('unrealizedPnl', 0)) # Determine position direction and exit details if contracts > 0: position_type = "LONG" exit_side = "sell" exit_emoji = "๐Ÿ”ด" else: position_type = "SHORT" exit_side = "buy" exit_emoji = "๐ŸŸข" contracts = abs(contracts) # Make positive for display # Get current market price market_data = self.client.get_market_data(symbol) if not market_data: await update.message.reply_text(f"โŒ Could not fetch current price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await update.message.reply_text(f"โŒ Invalid current price for {token}") return # Calculate estimated exit value exit_value = contracts * current_price # Create confirmation message pnl_emoji = "๐ŸŸข" if unrealized_pnl >= 0 else "๐Ÿ”ด" confirmation_text = f""" {exit_emoji} Exit Position Confirmation ๐Ÿ“Š Position Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Size: {contracts} contracts โ€ข Entry Price: ${entry_price:,.2f} โ€ข Current Price: ${current_price:,.2f} โ€ข {pnl_emoji} Unrealized P&L: ${unrealized_pnl:,.2f} ๐ŸŽฏ Exit Order: โ€ข Action: {exit_side.upper()} (Close {position_type}) โ€ข Amount: {contracts} {token} โ€ข Est. Value: ~${exit_value:,.2f} โ€ข Order Type: Market Order โš ๏ธ Are you sure you want to close this {position_type} position? This will place a market {exit_side} order to close your entire {token} position. """ keyboard = [ [ InlineKeyboardButton(f"โœ… Close {position_type}", callback_data=f"confirm_exit_{token}_{exit_side}_{contracts}_{current_price}"), InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid token format. Please use token symbols like BTC, ETH, etc.") except Exception as e: await update.message.reply_text(f"โŒ Error processing exit command: {e}") async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /coo (cancel open orders) command for a specific token.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 1: await update.message.reply_text( "โŒ Usage: /coo [token]\n" "Example: /coo BTC\n\n" "This command cancels ALL open orders for the specified token." ) return token = context.args[0].upper() symbol = f"{token}/USDC:USDC" # Get current orders for this token all_orders = self.client.get_open_orders() if all_orders is None: await update.message.reply_text(f"โŒ Could not fetch orders to cancel {token} orders") return # Filter orders for the specific token token_orders = [order for order in all_orders if order.get('symbol') == symbol] if not token_orders: await update.message.reply_text(f"๐Ÿ“ญ No open orders found for {token}") return # Create confirmation message with order details confirmation_text = f""" โš ๏ธ Cancel All {token} Orders ๐Ÿ“‹ Orders to Cancel: """ total_value = 0 for order in token_orders: side = order.get('side', 'Unknown') amount = order.get('amount', 0) price = order.get('price', 0) order_id = order.get('id', 'Unknown') side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" order_value = float(amount) * float(price) total_value += order_value confirmation_text += f"{side_emoji} {side.upper()} {amount} @ ${price:,.2f} (${order_value:,.2f})\n" confirmation_text += f""" ๐Ÿ’ฐ Total Value: ${total_value:,.2f} ๐Ÿ”ข Orders Count: {len(token_orders)} โš ๏ธ Are you sure you want to cancel ALL {token} orders? This action cannot be undone. """ keyboard = [ [ InlineKeyboardButton(f"โœ… Cancel All {token}", callback_data=f"confirm_coo_{token}"), InlineKeyboardButton("โŒ Keep Orders", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid token format. Please use token symbols like BTC, ETH, etc.") except Exception as e: await update.message.reply_text(f"โŒ Error processing cancel orders command: {e}") async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /sl (stop loss) command for setting stop loss orders.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await update.message.reply_text( "โŒ Usage: /sl [token] [price]\n" "Example: /sl BTC 44000\n\n" "This creates a stop loss order at the specified price." ) return token = context.args[0].upper() stop_price = float(context.args[1]) symbol = f"{token}/USDC:USDC" # Get current positions to find the position for this token positions = self.client.get_positions() if positions is None: await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position") return # Find the position for this token current_position = None for position in positions: if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0: current_position = position break if not current_position: await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}\n\nYou need an open position to set a stop loss.") return # Extract position details contracts = float(current_position.get('contracts', 0)) entry_price = float(current_position.get('entryPx', 0)) unrealized_pnl = float(current_position.get('unrealizedPnl', 0)) # Determine position direction and validate stop loss price if contracts > 0: # Long position - stop loss should be below entry price position_type = "LONG" exit_side = "sell" exit_emoji = "๐Ÿ”ด" contracts_abs = contracts if stop_price >= entry_price: await update.message.reply_text( f"โš ๏ธ Stop loss price should be BELOW entry price for long positions\n\n" f"๐Ÿ“Š Your {token} LONG position:\n" f"โ€ข Entry Price: ${entry_price:,.2f}\n" f"โ€ข Stop Price: ${stop_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a lower price like: /sl {token} {entry_price * 0.95:.0f}" ) return else: # Short position - stop loss should be above entry price position_type = "SHORT" exit_side = "buy" exit_emoji = "๐ŸŸข" contracts_abs = abs(contracts) if stop_price <= entry_price: await update.message.reply_text( f"โš ๏ธ Stop loss price should be ABOVE entry price for short positions\n\n" f"๐Ÿ“Š Your {token} SHORT position:\n" f"โ€ข Entry Price: ${entry_price:,.2f}\n" f"โ€ข Stop Price: ${stop_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a higher price like: /sl {token} {entry_price * 1.05:.0f}" ) return # Get current market price for reference market_data = self.client.get_market_data(symbol) current_price = 0 if market_data: current_price = float(market_data['ticker'].get('last', 0)) # Calculate estimated P&L at stop loss if contracts > 0: # Long position pnl_at_stop = (stop_price - entry_price) * contracts_abs else: # Short position pnl_at_stop = (entry_price - stop_price) * contracts_abs # Create confirmation message pnl_emoji = "๐ŸŸข" if pnl_at_stop >= 0 else "๐Ÿ”ด" confirmation_text = f""" ๐Ÿ›‘ Stop Loss Order Confirmation ๐Ÿ“Š Position Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Size: {contracts_abs} contracts โ€ข Entry Price: ${entry_price:,.2f} โ€ข Current Price: ${current_price:,.2f} ๐ŸŽฏ Stop Loss Order: โ€ข Stop Price: ${stop_price:,.2f} โ€ข Action: {exit_side.upper()} (Close {position_type}) โ€ข Amount: {contracts_abs} {token} โ€ข Order Type: Limit Order โ€ข {pnl_emoji} Est. P&L: ${pnl_at_stop:,.2f} โš ๏ธ Are you sure you want to set this stop loss? This will place a limit {exit_side} order at ${stop_price:,.2f} to protect your {position_type} position. """ keyboard = [ [ InlineKeyboardButton(f"โœ… Set Stop Loss", callback_data=f"confirm_sl_{token}_{exit_side}_{contracts_abs}_{stop_price}"), InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid price format. Please use numbers only.") except Exception as e: await update.message.reply_text(f"โŒ Error processing stop loss command: {e}") async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /tp (take profit) command for setting take profit orders.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await update.message.reply_text( "โŒ Usage: /tp [token] [price]\n" "Example: /tp BTC 50000\n\n" "This creates a take profit order at the specified price." ) return token = context.args[0].upper() profit_price = float(context.args[1]) symbol = f"{token}/USDC:USDC" # Get current positions to find the position for this token positions = self.client.get_positions() if positions is None: await update.message.reply_text(f"โŒ Could not fetch positions to check {token} position") return # Find the position for this token current_position = None for position in positions: if position.get('symbol') == symbol and float(position.get('contracts', 0)) != 0: current_position = position break if not current_position: await update.message.reply_text(f"๐Ÿ“ญ No open position found for {token}\n\nYou need an open position to set a take profit.") return # Extract position details contracts = float(current_position.get('contracts', 0)) entry_price = float(current_position.get('entryPx', 0)) unrealized_pnl = float(current_position.get('unrealizedPnl', 0)) # Determine position direction and validate take profit price if contracts > 0: # Long position - take profit should be above entry price position_type = "LONG" exit_side = "sell" exit_emoji = "๐Ÿ”ด" contracts_abs = contracts if profit_price <= entry_price: await update.message.reply_text( f"โš ๏ธ Take profit price should be ABOVE entry price for long positions\n\n" f"๐Ÿ“Š Your {token} LONG position:\n" f"โ€ข Entry Price: ${entry_price:,.2f}\n" f"โ€ข Take Profit: ${profit_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a higher price like: /tp {token} {entry_price * 1.05:.0f}" ) return else: # Short position - take profit should be below entry price position_type = "SHORT" exit_side = "buy" exit_emoji = "๐ŸŸข" contracts_abs = abs(contracts) if profit_price >= entry_price: await update.message.reply_text( f"โš ๏ธ Take profit price should be BELOW entry price for short positions\n\n" f"๐Ÿ“Š Your {token} SHORT position:\n" f"โ€ข Entry Price: ${entry_price:,.2f}\n" f"โ€ข Take Profit: ${profit_price:,.2f} โŒ\n\n" f"๐Ÿ’ก Try a lower price like: /tp {token} {entry_price * 0.95:.0f}" ) return # Get current market price for reference market_data = self.client.get_market_data(symbol) current_price = 0 if market_data: current_price = float(market_data['ticker'].get('last', 0)) # Calculate estimated P&L at take profit if contracts > 0: # Long position pnl_at_tp = (profit_price - entry_price) * contracts_abs else: # Short position pnl_at_tp = (entry_price - profit_price) * contracts_abs # Create confirmation message pnl_emoji = "๐ŸŸข" if pnl_at_tp >= 0 else "๐Ÿ”ด" confirmation_text = f""" ๐ŸŽฏ Take Profit Order Confirmation ๐Ÿ“Š Position Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Size: {contracts_abs} contracts โ€ข Entry Price: ${entry_price:,.2f} โ€ข Current Price: ${current_price:,.2f} ๐Ÿ’ฐ Take Profit Order: โ€ข Target Price: ${profit_price:,.2f} โ€ข Action: {exit_side.upper()} (Close {position_type}) โ€ข Amount: {contracts_abs} {token} โ€ข Order Type: Limit Order โ€ข {pnl_emoji} Est. P&L: ${pnl_at_tp:,.2f} โš ๏ธ Are you sure you want to set this take profit? This will place a limit {exit_side} order at ${profit_price:,.2f} to capture profits from your {position_type} position. """ keyboard = [ [ InlineKeyboardButton(f"โœ… Set Take Profit", callback_data=f"confirm_tp_{token}_{exit_side}_{contracts_abs}_{profit_price}"), InlineKeyboardButton("โŒ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await update.message.reply_text("โŒ Invalid price format. Please use numbers only.") except Exception as e: await update.message.reply_text(f"โŒ Error processing take profit command: {e}") async def start_order_monitoring(self): """Start the order monitoring background task.""" # Safety check in case this is called before initialization is complete if not hasattr(self, 'monitoring_active'): self.monitoring_active = False if self.monitoring_active: return self.monitoring_active = True logger.info("๐Ÿ”„ Starting order monitoring...") # Initialize tracking data await self._initialize_order_tracking() # Start monitoring loop asyncio.create_task(self._order_monitoring_loop()) async def stop_order_monitoring(self): """Stop the order monitoring background task.""" # Safety check in case this is called before initialization is complete if hasattr(self, 'monitoring_active'): self.monitoring_active = False logger.info("โน๏ธ Stopping order monitoring...") async def _initialize_order_tracking(self): """Initialize order and position tracking.""" try: # Get current open orders to initialize tracking orders = self.client.get_open_orders() if orders: self.last_known_orders = {order.get('id') for order in orders if order.get('id')} logger.info(f"๐Ÿ“‹ Initialized tracking with {len(self.last_known_orders)} open orders") # Get current positions for P&L tracking positions = self.client.get_positions() if positions: for position in positions: symbol = position.get('symbol') contracts = float(position.get('contracts', 0)) entry_price = float(position.get('entryPx', 0)) if symbol and contracts != 0: self.last_known_positions[symbol] = { 'contracts': contracts, 'entry_price': entry_price } logger.info(f"๐Ÿ“Š Initialized tracking with {len(self.last_known_positions)} positions") except Exception as e: logger.error(f"โŒ Error initializing order tracking: {e}") async def _order_monitoring_loop(self): """Main monitoring loop that runs every Config.BOT_HEARTBEAT_SECONDS seconds.""" while getattr(self, 'monitoring_active', False): try: await self._check_order_fills() await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Use configurable interval except asyncio.CancelledError: logger.info("๐Ÿ›‘ Order monitoring cancelled") break except Exception as e: logger.error(f"โŒ Error in order monitoring loop: {e}") await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS) # Continue monitoring even if there's an error async def _check_order_fills(self): """Check for filled orders and send notifications.""" try: # Get current orders and positions current_orders = self.client.get_open_orders() or [] current_positions = self.client.get_positions() or [] # Get current order IDs current_order_ids = {order.get('id') for order in current_orders if order.get('id')} # Find filled orders (orders that were in last_known_orders but not in current_orders) filled_order_ids = self.last_known_orders - current_order_ids if filled_order_ids: logger.info(f"๐ŸŽฏ Detected {len(filled_order_ids)} filled orders") await self._process_filled_orders(filled_order_ids, current_positions) # Process pending stop losses for filled orders await self._process_pending_stop_losses(filled_order_ids) # Update tracking data self.last_known_orders = current_order_ids await self._update_position_tracking(current_positions) # Save state after updating tracking data self._save_bot_state() # Check price alarms await self._check_price_alarms() # Check external trades (trades made outside the bot) await self._check_external_trades() # Check stop losses (if risk management is enabled) if Config.RISK_MANAGEMENT_ENABLED: await self._check_stop_losses(current_positions) # Check deposits/withdrawals (hourly) await self._check_deposits_withdrawals() # Clean up cancelled orders from pending stop losses await self._cleanup_cancelled_stop_losses(current_order_ids) except Exception as e: logger.error(f"โŒ Error checking order fills: {e}") async def _process_pending_stop_losses(self, filled_order_ids: set): """Process pending stop losses for filled orders.""" try: processed_any = False for order_id in filled_order_ids: if order_id in self.pending_stop_losses: stop_loss_info = self.pending_stop_losses[order_id] # Place the stop loss order await self._place_pending_stop_loss(order_id, stop_loss_info) # Remove from pending after processing del self.pending_stop_losses[order_id] processed_any = True logger.info(f"๐Ÿ—‘๏ธ Removed processed stop loss for order {order_id}") # Save state if any stop losses were processed if processed_any: self._save_bot_state() except Exception as e: logger.error(f"โŒ Error processing pending stop losses: {e}") async def _place_pending_stop_loss(self, original_order_id: str, stop_loss_info: Dict[str, Any]): """Place a pending stop loss order.""" try: token = stop_loss_info['token'] symbol = stop_loss_info['symbol'] stop_price = stop_loss_info['stop_price'] side = stop_loss_info['side'] amount = stop_loss_info['amount'] order_type = stop_loss_info['order_type'] logger.info(f"๐Ÿ›‘ Placing automatic stop loss: {side} {amount:.6f} {token} @ ${stop_price}") # Place the stop loss order as a limit order order = self.client.place_limit_order(symbol, side, amount, stop_price) if order: order_id = order.get('id', 'N/A') # Send notification await self._send_stop_loss_placed_notification(token, order_type, stop_price, amount, order_id, original_order_id) logger.info(f"โœ… Successfully placed automatic stop loss for {token}: Order ID {order_id}") else: # Send failure notification await self._send_stop_loss_failed_notification(token, order_type, stop_price, amount, original_order_id) logger.error(f"โŒ Failed to place automatic stop loss for {token}") except Exception as e: logger.error(f"โŒ Error placing pending stop loss: {e}") await self._send_stop_loss_failed_notification( stop_loss_info.get('token', 'Unknown'), stop_loss_info.get('order_type', 'Unknown'), stop_loss_info.get('stop_price', 0), stop_loss_info.get('amount', 0), original_order_id, str(e) ) async def _cleanup_cancelled_stop_losses(self, current_order_ids: set): """Remove pending stop losses for cancelled orders.""" try: # Find orders that are no longer active but were not filled orders_to_remove = [] for order_id, stop_loss_info in self.pending_stop_losses.items(): if order_id not in current_order_ids: # Order is no longer in open orders, check if it was cancelled (not filled) # We assume if it's not in current_order_ids and we haven't processed it as filled, # then it was likely cancelled orders_to_remove.append(order_id) # Remove cancelled orders from pending stop losses for order_id in orders_to_remove: stop_loss_info = self.pending_stop_losses[order_id] token = stop_loss_info['token'] # Send notification about cancelled stop loss await self._send_stop_loss_cancelled_notification(token, stop_loss_info, order_id) # Remove from pending del self.pending_stop_losses[order_id] logger.info(f"๐Ÿ—‘๏ธ Removed pending stop loss for cancelled order {order_id}") # Save state if any stop losses were removed if orders_to_remove: self._save_bot_state() except Exception as e: logger.error(f"โŒ Error cleaning up cancelled stop losses: {e}") async def _send_stop_loss_placed_notification(self, token: str, order_type: str, stop_price: float, amount: float, stop_order_id: str, original_order_id: str): """Send notification when stop loss is successfully placed.""" try: position_type = order_type.upper() message = f""" ๐Ÿ›‘ Stop Loss Placed Automatically โœ… Stop Loss Active ๐Ÿ“Š Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Stop Price: ${stop_price:,.2f} โ€ข Amount: {amount:.6f} {token} โ€ข Stop Loss Order ID: {stop_order_id} โ€ข Original Order ID: {original_order_id} ๐ŸŽฏ Protection: โ€ข Status: ACTIVE โœ… โ€ข Will execute if price reaches ${stop_price:,.2f} โ€ข Order Type: Limit Order ๐Ÿ’ก Your {position_type} position is now protected with automatic stop loss! """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Sent stop loss placed notification: {token} @ ${stop_price}") except Exception as e: logger.error(f"โŒ Error sending stop loss placed notification: {e}") async def _send_stop_loss_failed_notification(self, token: str, order_type: str, stop_price: float, amount: float, original_order_id: str, error: str = None): """Send notification when stop loss placement fails.""" try: position_type = order_type.upper() message = f""" โš ๏ธ Stop Loss Placement Failed โŒ Automatic Stop Loss Failed ๐Ÿ“Š Details: โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Intended Stop Price: ${stop_price:,.2f} โ€ข Amount: {amount:.6f} {token} โ€ข Original Order ID: {original_order_id} ๐Ÿšจ Action Required: โ€ข Your position is NOT protected โ€ข Consider manually setting stop loss: /sl {token} {stop_price:.0f} โ€ข Monitor your position closely {f'๐Ÿ”ง Error: {error}' if error else ''} ๐Ÿ’ก Use /sl command to manually set stop loss protection. """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Sent stop loss failed notification: {token}") except Exception as e: logger.error(f"โŒ Error sending stop loss failed notification: {e}") async def _send_stop_loss_cancelled_notification(self, token: str, stop_loss_info: Dict[str, Any], order_id: str): """Send notification when stop loss is cancelled due to order cancellation.""" try: position_type = stop_loss_info['order_type'].upper() stop_price = stop_loss_info['stop_price'] message = f""" ๐Ÿšซ Stop Loss Cancelled ๐Ÿ“Š Original Order Cancelled โ€ข Token: {token} โ€ข Position: {position_type} โ€ข Cancelled Stop Price: ${stop_price:,.2f} โ€ข Original Order ID: {order_id} ๐Ÿ’ก Status: โ€ข Pending stop loss automatically cancelled โ€ข No position protection was placed โ€ข Order was cancelled before execution ๐Ÿ”„ If you still want to trade {token}, place a new order with stop loss protection. """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Sent stop loss cancelled notification: {token}") except Exception as e: logger.error(f"โŒ Error sending stop loss cancelled notification: {e}") async def _check_price_alarms(self): """Check all active price alarms.""" try: # Get all active alarms active_alarms = self.alarm_manager.get_all_active_alarms() if not active_alarms: return # Get unique tokens from alarms tokens_to_check = list(set(alarm['token'] for alarm in active_alarms)) # Fetch current prices for all tokens price_data = {} for token in tokens_to_check: symbol = f"{token}/USDC:USDC" market_data = self.client.get_market_data(symbol) if market_data and market_data.get('ticker'): current_price = market_data['ticker'].get('last') if current_price is not None: price_data[token] = float(current_price) # Check alarms against current prices triggered_alarms = self.alarm_manager.check_alarms(price_data) # Send notifications for triggered alarms for alarm in triggered_alarms: await self._send_alarm_notification(alarm) except Exception as e: logger.error(f"โŒ Error checking price alarms: {e}") async def _send_alarm_notification(self, alarm: Dict[str, Any]): """Send notification for triggered alarm.""" try: message = self.alarm_manager.format_triggered_alarm(alarm) await self.send_message(message) logger.info(f"๐Ÿ“ข Sent alarm notification: {alarm['token']} ID {alarm['id']} @ ${alarm['triggered_price']}") except Exception as e: logger.error(f"โŒ Error sending alarm notification: {e}") async def _check_external_trades(self): """Check for trades made outside the Telegram bot and update stats.""" try: # Get recent fills from Hyperliquid recent_fills = self.client.get_recent_fills() if not recent_fills: return # Initialize last processed time if first run if self.last_processed_trade_time is None: # Set to current time minus 1 hour to catch recent activity self.last_processed_trade_time = (datetime.now() - timedelta(hours=1)).isoformat() # Filter for new trades since last check new_trades = [] latest_trade_time = self.last_processed_trade_time for fill in recent_fills: fill_time = fill.get('timestamp') if fill_time: # Convert timestamps to comparable format try: # Convert fill_time to string if it's not already if isinstance(fill_time, (int, float)): # Assume it's a unix timestamp fill_time_str = datetime.fromtimestamp(fill_time / 1000 if fill_time > 1e10 else fill_time).isoformat() else: fill_time_str = str(fill_time) # Compare as strings if fill_time_str > self.last_processed_trade_time: new_trades.append(fill) if fill_time_str > latest_trade_time: latest_trade_time = fill_time_str except Exception as timestamp_error: logger.warning(f"โš ๏ธ Error processing timestamp {fill_time}: {timestamp_error}") continue if not new_trades: return # Process new trades for trade in new_trades: await self._process_external_trade(trade) # Update last processed time self.last_processed_trade_time = latest_trade_time # Save state after updating last processed time self._save_bot_state() if new_trades: logger.info(f"๐Ÿ“Š Processed {len(new_trades)} external trades") except Exception as e: logger.error(f"โŒ Error checking external trades: {e}") async def _check_deposits_withdrawals(self): """Check for deposits and withdrawals to maintain accurate P&L tracking.""" try: # Check if it's time to run (hourly check) current_time = datetime.now() if self.last_deposit_withdrawal_check is not None: time_since_last_check = (current_time - self.last_deposit_withdrawal_check).total_seconds() if time_since_last_check < self.deposit_withdrawal_check_interval: return # Not time to check yet logger.info("๐Ÿ” Checking for deposits and withdrawals...") # Initialize last check time if first run if self.last_deposit_withdrawal_check is None: # Set to 24 hours ago to catch recent activity self.last_deposit_withdrawal_check = current_time - timedelta(hours=24) # Calculate timestamp for API calls (last check time) since_timestamp = int(self.last_deposit_withdrawal_check.timestamp() * 1000) # Hyperliquid expects milliseconds # Track new deposits/withdrawals new_deposits = 0 new_withdrawals = 0 # Check if sync_client is available if not hasattr(self.client, 'sync_client') or not self.client.sync_client: logger.warning("โš ๏ธ CCXT sync_client not available for deposit/withdrawal checking") self.last_deposit_withdrawal_check = current_time return # Set up user parameter for Hyperliquid API calls params = {} if Config.HYPERLIQUID_WALLET_ADDRESS: wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address else: logger.warning("โš ๏ธ No wallet address configured for deposit/withdrawal checking") self.last_deposit_withdrawal_check = current_time return # Check for deposits try: deposits = self.client.sync_client.fetch_deposits(code='USDC', since=since_timestamp, params=params) if deposits: for deposit in deposits: amount = float(deposit.get('amount', 0)) timestamp = deposit.get('datetime', datetime.now().isoformat()) deposit_id = deposit.get('id', 'unknown') # Record in stats to adjust P&L calculations self.stats.record_deposit(amount, timestamp, deposit_id) new_deposits += 1 # Send notification await self._send_deposit_notification(amount, timestamp) except Exception as e: logger.warning(f"โš ๏ธ Error fetching deposits: {e}") # Check for withdrawals try: withdrawals = self.client.sync_client.fetch_withdrawals(code='USDC', since=since_timestamp, params=params) if withdrawals: for withdrawal in withdrawals: amount = float(withdrawal.get('amount', 0)) timestamp = withdrawal.get('datetime', datetime.now().isoformat()) withdrawal_id = withdrawal.get('id', 'unknown') # Record in stats to adjust P&L calculations self.stats.record_withdrawal(amount, timestamp, withdrawal_id) new_withdrawals += 1 # Send notification await self._send_withdrawal_notification(amount, timestamp) except Exception as e: logger.warning(f"โš ๏ธ Error fetching withdrawals: {e}") # Update last check time self.last_deposit_withdrawal_check = current_time # Save state after updating last check time self._save_bot_state() if new_deposits > 0 or new_withdrawals > 0: logger.info(f"๐Ÿ’ฐ Processed {new_deposits} deposits and {new_withdrawals} withdrawals") # Get updated balance adjustments summary adjustments = self.stats.get_balance_adjustments_summary() logger.info(f"๐Ÿ“Š Total adjustments: ${adjustments['net_adjustment']:,.2f} net ({adjustments['adjustment_count']} total)") except Exception as e: logger.error(f"โŒ Error checking deposits/withdrawals: {e}") async def _send_deposit_notification(self, amount: float, timestamp: str): """Send notification for detected deposit.""" try: time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S') message = f""" ๐Ÿ’ฐ Deposit Detected ๐Ÿ’ต Amount: ${amount:,.2f} USDC โฐ Time: {time_str} ๐Ÿ“Š P&L Impact: โ€ข Initial balance adjusted to maintain accurate P&L โ€ข Trading statistics unaffected by balance change โ€ข This deposit will not show as trading profit โœ… Balance tracking updated automatically """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ฑ Sent deposit notification: ${amount:,.2f}") except Exception as e: logger.error(f"โŒ Error sending deposit notification: {e}") async def _send_withdrawal_notification(self, amount: float, timestamp: str): """Send notification for detected withdrawal.""" try: time_str = datetime.fromisoformat(timestamp.replace('Z', '+00:00')).strftime('%H:%M:%S') message = f""" ๐Ÿ’ธ Withdrawal Detected ๐Ÿ’ต Amount: ${amount:,.2f} USDC โฐ Time: {time_str} ๐Ÿ“Š P&L Impact: โ€ข Initial balance adjusted to maintain accurate P&L โ€ข Trading statistics unaffected by balance change โ€ข This withdrawal will not show as trading loss โœ… Balance tracking updated automatically """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ฑ Sent withdrawal notification: ${amount:,.2f}") except Exception as e: logger.error(f"โŒ Error sending withdrawal notification: {e}") async def _process_external_trade(self, trade: Dict[str, Any]): """Process an individual external trade and determine if it's opening or closing a position.""" try: # Extract trade information symbol = trade.get('symbol', '') side = trade.get('side', '') amount = float(trade.get('amount', 0)) price = float(trade.get('price', 0)) trade_id = trade.get('id', 'external') timestamp = trade.get('timestamp', '') if not all([symbol, side, amount, price]): return # Record trade in stats and get action type using enhanced tracking action_type = self.stats.record_trade_with_enhanced_tracking(symbol, side, amount, price, trade_id, "external") # Send enhanced notification based on action type await self._send_enhanced_trade_notification(symbol, side, amount, price, action_type, timestamp) logger.info(f"๐Ÿ“‹ Processed external trade: {side} {amount} {symbol} @ ${price} ({action_type})") except Exception as e: logger.error(f"โŒ Error processing external trade: {e}") async def _send_enhanced_trade_notification(self, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str = None): """Send enhanced trade notification based on position action type.""" try: token = symbol.split('/')[0] if '/' in symbol else symbol position = self.stats.get_enhanced_position_state(symbol) if timestamp is None: time_str = datetime.now().strftime('%H:%M:%S') else: try: time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) time_str = time_obj.strftime('%H:%M:%S') except: time_str = "Unknown" # Handle different action types if action_type in ['long_opened', 'short_opened']: await self._send_position_opened_notification(token, side, amount, price, action_type, time_str) elif action_type in ['long_increased', 'short_increased']: await self._send_position_increased_notification(token, side, amount, price, position, action_type, time_str) elif action_type in ['long_reduced', 'short_reduced']: pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price) await self._send_position_reduced_notification(token, side, amount, price, position, pnl_data, action_type, time_str) elif action_type in ['long_closed', 'short_closed']: pnl_data = self.stats.calculate_enhanced_position_pnl(symbol, amount, price) await self._send_position_closed_notification(token, side, amount, price, position, pnl_data, action_type, time_str) elif action_type in ['long_closed_and_short_opened', 'short_closed_and_long_opened']: await self._send_position_flipped_notification(token, side, amount, price, action_type, time_str) else: # Fallback to generic notification await self._send_external_trade_notification({ 'symbol': symbol, 'side': side, 'amount': amount, 'price': price, 'timestamp': timestamp or datetime.now().isoformat() }) except Exception as e: logger.error(f"โŒ Error sending enhanced trade notification: {e}") async def _send_position_opened_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str): """Send notification for newly opened position.""" position_type = "LONG" if action_type == 'long_opened' else "SHORT" side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" trade_value = amount * price message = f""" ๐Ÿš€ Position Opened ๐Ÿ“Š New {position_type} Position: โ€ข Token: {token} โ€ข Direction: {position_type} โ€ข Entry Size: {amount} {token} โ€ข Entry Price: ${price:,.2f} โ€ข Position Value: ${trade_value:,.2f} {side_emoji} Trade Details: โ€ข Side: {side.upper()} โ€ข Order Type: Market/Limit โ€ข Status: OPENED โœ… โฐ Time: {time_str} ๐Ÿ“ˆ Note: New {position_type} position established ๐Ÿ“Š Use /positions to view current holdings """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Position opened: {token} {position_type} {amount} @ ${price}") async def _send_position_increased_notification(self, token: str, side: str, amount: float, price: float, position: Dict, action_type: str, time_str: str): """Send notification for position increase (additional entry).""" position_type = "LONG" if action_type == 'long_increased' else "SHORT" side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" total_size = abs(position['contracts']) avg_entry = position['avg_entry_price'] entry_count = position['entry_count'] total_value = total_size * avg_entry message = f""" ๐Ÿ“ˆ Position Increased ๐Ÿ“Š {position_type} Position Updated: โ€ข Token: {token} โ€ข Direction: {position_type} โ€ข Added Size: {amount} {token} @ ${price:,.2f} โ€ข New Total Size: {total_size} {token} โ€ข Average Entry: ${avg_entry:,.2f} {side_emoji} Position Summary: โ€ข Total Value: ${total_value:,.2f} โ€ข Entry Points: {entry_count} โ€ข Last Entry: ${price:,.2f} โ€ข Status: INCREASED โฌ†๏ธ โฐ Time: {time_str} ๐Ÿ’ก Strategy: Multiple entry averaging ๐Ÿ“Š Use /positions for complete position details """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Position increased: {token} {position_type} +{amount} @ ${price} (total: {total_size})") async def _send_position_reduced_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str): """Send notification for partial position close.""" position_type = "LONG" if action_type == 'long_reduced' else "SHORT" remaining_size = abs(position['contracts']) avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price']) pnl = pnl_data['pnl'] pnl_percent = pnl_data['pnl_percent'] pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด" partial_value = amount * price message = f""" ๐Ÿ“‰ Position Partially Closed ๐Ÿ“Š {position_type} Partial Exit: โ€ข Token: {token} โ€ข Direction: {position_type} โ€ข Closed Size: {amount} {token} โ€ข Exit Price: ${price:,.2f} โ€ข Remaining Size: {remaining_size} {token} {pnl_emoji} Partial P&L: โ€ข Entry Price: ${avg_entry:,.2f} โ€ข Exit Value: ${partial_value:,.2f} โ€ข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%) โ€ข Result: {"PROFIT" if pnl >= 0 else "LOSS"} ๐Ÿ’ฐ Position Status: โ€ข Status: PARTIALLY CLOSED ๐Ÿ“‰ โ€ข Take Profit Strategy: Active โฐ Time: {time_str} ๐Ÿ“Š Use /positions to view remaining position """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Position reduced: {token} {position_type} -{amount} @ ${price} P&L: ${pnl:.2f}") async def _send_position_closed_notification(self, token: str, side: str, amount: float, price: float, position: Dict, pnl_data: Dict, action_type: str, time_str: str): """Send notification for fully closed position.""" position_type = "LONG" if action_type == 'long_closed' else "SHORT" avg_entry = pnl_data.get('avg_entry_price', position['avg_entry_price']) pnl = pnl_data['pnl'] pnl_percent = pnl_data['pnl_percent'] pnl_emoji = "๐ŸŸข" if pnl >= 0 else "๐Ÿ”ด" entry_count = position.get('entry_count', 1) exit_value = amount * price message = f""" ๐ŸŽฏ Position Fully Closed ๐Ÿ“Š {position_type} Position Summary: โ€ข Token: {token} โ€ข Direction: {position_type} โ€ข Total Size: {amount} {token} โ€ข Average Entry: ${avg_entry:,.2f} โ€ข Exit Price: ${price:,.2f} โ€ข Exit Value: ${exit_value:,.2f} {pnl_emoji} Total P&L: โ€ข P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%) โ€ข Result: {"PROFIT" if pnl >= 0 else "LOSS"} โ€ข Entry Points Used: {entry_count} โœ… Trade Complete: โ€ข Status: FULLY CLOSED ๐ŸŽฏ โ€ข Position: FLAT โฐ Time: {time_str} ๐Ÿ“Š Use /stats to view updated performance """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Position closed: {token} {position_type} {amount} @ ${price} Total P&L: ${pnl:.2f}") async def _send_position_flipped_notification(self, token: str, side: str, amount: float, price: float, action_type: str, time_str: str): """Send notification for position flip (close and reverse).""" if action_type == 'long_closed_and_short_opened': old_type = "LONG" new_type = "SHORT" else: old_type = "SHORT" new_type = "LONG" message = f""" ๐Ÿ”„ Position Flipped ๐Ÿ“Š Direction Change: โ€ข Token: {token} โ€ข Previous: {old_type} position โ€ข New: {new_type} position โ€ข Size: {amount} {token} โ€ข Price: ${price:,.2f} ๐ŸŽฏ Trade Summary: โ€ข {old_type} position: CLOSED โœ… โ€ข {new_type} position: OPENED ๐Ÿš€ โ€ข Flip Price: ${price:,.2f} โ€ข Status: POSITION REVERSED โฐ Time: {time_str} ๐Ÿ’ก Strategy: Directional change ๐Ÿ“Š Use /positions to view new position """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Position flipped: {token} {old_type} -> {new_type} @ ${price}") async def monitoring_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /monitoring command to show monitoring status.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return # Get alarm statistics alarm_stats = self.alarm_manager.get_statistics() # Get balance adjustments info adjustments_summary = self.stats.get_balance_adjustments_summary() last_deposit_check = "Never" next_deposit_check = "Unknown" if hasattr(self, 'last_deposit_withdrawal_check') and self.last_deposit_withdrawal_check: last_deposit_check = self.last_deposit_withdrawal_check.strftime('%H:%M:%S') next_check_time = self.last_deposit_withdrawal_check + timedelta(seconds=self.deposit_withdrawal_check_interval) next_deposit_check = next_check_time.strftime('%H:%M:%S') # Safety checks for monitoring attributes monitoring_active = getattr(self, 'monitoring_active', False) last_known_orders = getattr(self, 'last_known_orders', set()) last_known_positions = getattr(self, 'last_known_positions', {}) deposit_withdrawal_check_interval = getattr(self, 'deposit_withdrawal_check_interval', 3600) status_text = f""" ๐Ÿ”„ System Monitoring Status ๐Ÿ“Š Order Monitoring: โ€ข Active: {'โœ… Yes' if monitoring_active else 'โŒ No'} โ€ข Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds โ€ข Orders Tracked: {len(last_known_orders)} โ€ข Positions Tracked: {len(last_known_positions)} โ€ข Pending Stop Losses: {len(getattr(self, 'pending_stop_losses', {}))} ๐Ÿ’ฐ Deposit/Withdrawal Monitoring: โ€ข Check Interval: {deposit_withdrawal_check_interval // 3600} hour(s) โ€ข Last Check: {last_deposit_check} โ€ข Next Check: {next_deposit_check} โ€ข Total Adjustments: {adjustments_summary['adjustment_count']} โ€ข Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f} ๐Ÿ”” 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: โ€ข Last Check: {self.last_processed_trade_time or 'Not started'} โ€ข Auto Stats Update: โœ… Enabled โ€ข External Notifications: โœ… Enabled ๐Ÿ›ก๏ธ Risk Management: โ€ข Automatic Stop Loss: {'โœ… Enabled' if Config.RISK_MANAGEMENT_ENABLED else 'โŒ Disabled'} โ€ข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}% โ€ข Position Monitoring: {'โœ… Active' if Config.RISK_MANAGEMENT_ENABLED else 'โŒ Inactive'} โ€ข Order-based Stop Loss: โœ… Enabled ๐Ÿ“ˆ Notifications: โ€ข ๐Ÿš€ Position Opened/Increased โ€ข ๐Ÿ“‰ Position Partially/Fully Closed โ€ข ๐ŸŽฏ P&L Calculations โ€ข ๐Ÿ”” Price Alarm Triggers โ€ข ๐Ÿ”„ External Trade Detection โ€ข ๐Ÿ’ฐ Deposit/Withdrawal Detection โ€ข ๐Ÿ›‘ Automatic Stop Loss Triggers โ€ข ๐Ÿ›‘ Order-based Stop Loss Placement ๐Ÿ’พ Bot State Persistence: โ€ข Pending Stop Losses: Saved to disk โ€ข Order Tracking: Saved to disk โ€ข External Trade Times: Saved to disk โ€ข Deposit Check Times: Saved to disk โ€ข State File: bot_state.json โ€ข Survives Bot 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 โ€ข Deposit/withdrawal tracking โ€ข 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 update.message.reply_text(status_text.strip(), parse_mode='HTML') async def alarm_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /alarm command for price alerts.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_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 update.message.reply_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 update.message.reply_text(f"โœ… Alarm ID {alarm_id} has been removed.") else: await update.message.reply_text(f"โŒ Alarm ID {alarm_id} not found.") return except ValueError: # Not a number, treat as token token = arg.upper() alarms = self.alarm_manager.get_alarms_by_token(token) message = self.alarm_manager.format_alarm_list(alarms, f"{token} Price Alarms") await update.message.reply_text(message, parse_mode='HTML') return elif len(context.args) == 2: # Set new alarm: /alarm TOKEN PRICE token = context.args[0].upper() target_price = float(context.args[1]) # Get current market price symbol = f"{token}/USDC:USDC" market_data = self.client.get_market_data(symbol) if not market_data or not market_data.get('ticker'): await update.message.reply_text(f"โŒ Could not fetch current price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await update.message.reply_text(f"โŒ Invalid current price for {token}") return # Create the alarm alarm = self.alarm_manager.create_alarm(token, target_price, current_price) # Format confirmation message direction_emoji = "๐Ÿ“ˆ" if alarm['direction'] == 'above' else "๐Ÿ“‰" price_diff = abs(target_price - current_price) price_diff_percent = (price_diff / current_price) * 100 message = f""" โœ… Price Alarm Created ๐Ÿ“Š Alarm Details: โ€ข Alarm ID: {alarm['id']} โ€ข Token: {token} โ€ข Target Price: ${target_price:,.2f} โ€ข Current Price: ${current_price:,.2f} โ€ข Direction: {alarm['direction'].upper()} {direction_emoji} Alert Condition: Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f} ๐Ÿ’ฐ Price Difference: โ€ข Distance: ${price_diff:,.2f} ({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 update.message.reply_text(message.strip(), parse_mode='HTML') else: # Too many arguments await update.message.reply_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 update.message.reply_text("โŒ Invalid price format. Please use numbers only.") except Exception as e: error_message = f"โŒ Error processing alarm command: {str(e)}" await update.message.reply_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 to show log file statistics and cleanup options.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: # Check for cleanup argument if context.args and len(context.args) >= 1: if context.args[0].lower() == 'cleanup': # Get days parameter (default 30) days_to_keep = 30 if len(context.args) >= 2: try: days_to_keep = int(context.args[1]) except ValueError: await update.message.reply_text("โŒ Invalid number of days. Using default (30).") # Perform cleanup await update.message.reply_text(f"๐Ÿงน Cleaning up log files older than {days_to_keep} days...") cleanup_logs(days_to_keep) await update.message.reply_text(f"โœ… Log cleanup completed!") return # Show log statistics log_stats_text = format_log_stats() # Add additional info status_text = f""" ๐Ÿ“Š System Logging Status {log_stats_text} ๐Ÿ“ˆ Log Configuration: โ€ข Log Level: {Config.LOG_LEVEL} โ€ข Heartbeat Interval: {Config.BOT_HEARTBEAT_SECONDS}s โ€ข Bot Uptime: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ๐Ÿ’ก Log Management: โ€ข /logs cleanup - Clean old logs (30 days) โ€ข /logs cleanup 7 - Clean logs older than 7 days โ€ข Log rotation happens automatically โ€ข Old backups are removed automatically ๐Ÿ”ง Configuration: โ€ข Rotation Type: {Config.LOG_ROTATION_TYPE} โ€ข Max Size: {Config.LOG_MAX_SIZE_MB}MB (size rotation) โ€ข Backup Count: {Config.LOG_BACKUP_COUNT} """ await update.message.reply_text(status_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing logs command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in logs command: {e}") async def performance_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /performance command to show token performance ranking or detailed stats.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: # Check if specific token is requested if context.args and len(context.args) >= 1: # Detailed performance for specific token token = context.args[0].upper() await self._show_token_performance(update, token) else: # Show token performance ranking await self._show_performance_ranking(update) except Exception as e: error_message = f"โŒ Error processing performance command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in performance command: {e}") async def _show_performance_ranking(self, update: Update): """Show token performance ranking (compressed view).""" token_performance = self.stats.get_token_performance() if not token_performance: await update.message.reply_text( "๐Ÿ“Š Token Performance\n\n" "๐Ÿ“ญ No trading data available yet.\n\n" "๐Ÿ’ก Performance tracking starts after your first completed trades.\n" "Use /long or /short to start trading!", parse_mode='HTML' ) return # Sort tokens by total P&L (best to worst) sorted_tokens = sorted( token_performance.items(), key=lambda x: x[1]['total_pnl'], reverse=True ) performance_text = "๐Ÿ† Token Performance Ranking\n\n" # Add ranking with emojis for i, (token, stats) in enumerate(sorted_tokens, 1): # Ranking emoji if i == 1: rank_emoji = "๐Ÿฅ‡" elif i == 2: rank_emoji = "๐Ÿฅˆ" elif i == 3: rank_emoji = "๐Ÿฅ‰" else: rank_emoji = f"#{i}" # P&L emoji pnl_emoji = "๐ŸŸข" if stats['total_pnl'] >= 0 else "๐Ÿ”ด" # Format the line performance_text += f"{rank_emoji} {token}\n" performance_text += f" {pnl_emoji} P&L: ${stats['total_pnl']:,.2f} ({stats['pnl_percentage']:+.1f}%)\n" performance_text += f" ๐Ÿ“Š Trades: {stats['completed_trades']}" # Add win rate if there are completed trades if stats['completed_trades'] > 0: performance_text += f" | Win: {stats['win_rate']:.0f}%" performance_text += "\n\n" # Add summary total_pnl = sum(stats['total_pnl'] for stats in token_performance.values()) total_trades = sum(stats['completed_trades'] for stats in token_performance.values()) total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด" performance_text += f"๐Ÿ’ผ Portfolio Summary:\n" performance_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" performance_text += f" ๐Ÿ“ˆ Tokens Traded: {len(token_performance)}\n" performance_text += f" ๐Ÿ”„ Completed Trades: {total_trades}\n\n" performance_text += f"๐Ÿ’ก Usage: /performance BTC for detailed {Config.DEFAULT_TRADING_TOKEN} stats" await update.message.reply_text(performance_text.strip(), parse_mode='HTML') async def _show_token_performance(self, update: Update, token: str): """Show detailed performance for a specific token.""" token_stats = self.stats.get_token_detailed_stats(token) # Check if token has any data if token_stats.get('total_trades', 0) == 0: await update.message.reply_text( f"๐Ÿ“Š {token} Performance\n\n" f"๐Ÿ“ญ No trading history found for {token}.\n\n" f"๐Ÿ’ก Start trading {token} with:\n" f"โ€ข /long {token} 100\n" f"โ€ข /short {token} 100\n\n" f"๐Ÿ”„ Use /performance to see all token rankings.", parse_mode='HTML' ) return # Check if there's a message (no completed trades) if 'message' in token_stats and token_stats.get('completed_trades', 0) == 0: await update.message.reply_text( f"๐Ÿ“Š {token} Performance\n\n" f"{token_stats['message']}\n\n" f"๐Ÿ“ˆ Current Activity:\n" f"โ€ข Total Trades: {token_stats['total_trades']}\n" f"โ€ข Buy Orders: {token_stats.get('buy_trades', 0)}\n" f"โ€ข Sell Orders: {token_stats.get('sell_trades', 0)}\n" f"โ€ข Volume: ${token_stats.get('total_volume', 0):,.2f}\n\n" f"๐Ÿ’ก Complete some trades to see P&L statistics!\n" f"๐Ÿ”„ Use /performance to see all token rankings.", parse_mode='HTML' ) return # Detailed stats display pnl_emoji = "๐ŸŸข" if token_stats['total_pnl'] >= 0 else "๐Ÿ”ด" performance_text = f""" ๐Ÿ“Š {token} Detailed Performance ๐Ÿ’ฐ P&L Summary: โ€ข {pnl_emoji} Total P&L: ${token_stats['total_pnl']:,.2f} ({token_stats['pnl_percentage']:+.2f}%) โ€ข ๐Ÿ’ต Total Volume: ${token_stats['completed_volume']:,.2f} โ€ข ๐Ÿ“ˆ Expectancy: ${token_stats['expectancy']:,.2f} ๐Ÿ“Š Trading Activity: โ€ข Total Trades: {token_stats['total_trades']} โ€ข Completed: {token_stats['completed_trades']} โ€ข Buy Orders: {token_stats['buy_trades']} โ€ข Sell Orders: {token_stats['sell_trades']} ๐Ÿ† Performance Metrics: โ€ข Win Rate: {token_stats['win_rate']:.1f}% โ€ข Profit Factor: {token_stats['profit_factor']:.2f} โ€ข Wins: {token_stats['total_wins']} | Losses: {token_stats['total_losses']} ๐Ÿ’ก Best/Worst: โ€ข Largest Win: ${token_stats['largest_win']:,.2f} โ€ข Largest Loss: ${token_stats['largest_loss']:,.2f} โ€ข Avg Win: ${token_stats['avg_win']:,.2f} โ€ข Avg Loss: ${token_stats['avg_loss']:,.2f} """ # Add recent trades if available if token_stats.get('recent_trades'): performance_text += f"\n๐Ÿ”„ Recent Trades:\n" for trade in token_stats['recent_trades'][-3:]: # Last 3 trades trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M') side_emoji = "๐ŸŸข" if trade['side'] == 'buy' else "๐Ÿ”ด" pnl_display = f" | P&L: ${trade.get('pnl', 0):.2f}" if trade.get('pnl', 0) != 0 else "" performance_text += f"โ€ข {side_emoji} {trade['side'].upper()} ${trade['value']:,.0f} @ {trade_time}{pnl_display}\n" performance_text += f"\n๐Ÿ”„ Use /performance to see all token rankings" await update.message.reply_text(performance_text.strip(), parse_mode='HTML') async def daily_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /daily command to show daily performance stats.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: daily_stats = self.stats.get_daily_stats(10) if not daily_stats: await update.message.reply_text( "๐Ÿ“… Daily Performance\n\n" "๐Ÿ“ญ No daily performance data available yet.\n\n" "๐Ÿ’ก Daily stats are calculated from completed trades.\n" "Start trading to see daily performance!", parse_mode='HTML' ) return daily_text = "๐Ÿ“… Daily Performance (Last 10 Days)\n\n" total_pnl = 0 total_trades = 0 trading_days = 0 for day_stats in daily_stats: if day_stats['has_trades']: # Day with completed trades pnl_emoji = "๐ŸŸข" if day_stats['pnl'] >= 0 else "๐Ÿ”ด" daily_text += f"๐Ÿ“Š {day_stats['date_formatted']}\n" daily_text += f" {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n" daily_text += f" ๐Ÿ”„ Trades: {day_stats['trades']}\n\n" total_pnl += day_stats['pnl'] total_trades += day_stats['trades'] trading_days += 1 else: # Day with no trades daily_text += f"๐Ÿ“Š {day_stats['date_formatted']}\n" daily_text += f" ๐Ÿ“ญ No completed trades\n\n" # Add summary if trading_days > 0: total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด" daily_text += f"๐Ÿ’ผ 10-Day Summary:\n" daily_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" daily_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n" daily_text += f" ๐Ÿ“ˆ Trading Days: {trading_days}/10\n" daily_text += f" ๐Ÿ“Š Avg per Trading Day: ${total_pnl/trading_days:,.2f}" else: daily_text += f"๐Ÿ’ผ 10-Day Summary:\n" daily_text += f" ๐Ÿ“ญ No completed trades in the last 10 days\n" daily_text += f" ๐Ÿ’ก Start trading to see daily performance!" await update.message.reply_text(daily_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing daily command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in daily command: {e}") async def weekly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /weekly command to show weekly performance stats.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: weekly_stats = self.stats.get_weekly_stats(10) if not weekly_stats: await update.message.reply_text( "๐Ÿ“Š Weekly Performance\n\n" "๐Ÿ“ญ No weekly performance data available yet.\n\n" "๐Ÿ’ก Weekly stats are calculated from completed trades.\n" "Start trading to see weekly performance!", parse_mode='HTML' ) return weekly_text = "๐Ÿ“Š Weekly Performance (Last 10 Weeks)\n\n" total_pnl = 0 total_trades = 0 trading_weeks = 0 for week_stats in weekly_stats: if week_stats['has_trades']: # Week with completed trades pnl_emoji = "๐ŸŸข" if week_stats['pnl'] >= 0 else "๐Ÿ”ด" weekly_text += f"๐Ÿ“ˆ {week_stats['week_formatted']}\n" weekly_text += f" {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n" weekly_text += f" ๐Ÿ”„ Trades: {week_stats['trades']}\n\n" total_pnl += week_stats['pnl'] total_trades += week_stats['trades'] trading_weeks += 1 else: # Week with no trades weekly_text += f"๐Ÿ“ˆ {week_stats['week_formatted']}\n" weekly_text += f" ๐Ÿ“ญ No completed trades\n\n" # Add summary if trading_weeks > 0: total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด" weekly_text += f"๐Ÿ’ผ 10-Week Summary:\n" weekly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" weekly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n" weekly_text += f" ๐Ÿ“ˆ Trading Weeks: {trading_weeks}/10\n" weekly_text += f" ๐Ÿ“Š Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}" else: weekly_text += f"๐Ÿ’ผ 10-Week Summary:\n" weekly_text += f" ๐Ÿ“ญ No completed trades in the last 10 weeks\n" weekly_text += f" ๐Ÿ’ก Start trading to see weekly performance!" await update.message.reply_text(weekly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing weekly command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in weekly command: {e}") async def monthly_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /monthly command to show monthly performance stats.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: monthly_stats = self.stats.get_monthly_stats(10) if not monthly_stats: await update.message.reply_text( "๐Ÿ“† Monthly Performance\n\n" "๐Ÿ“ญ No monthly performance data available yet.\n\n" "๐Ÿ’ก Monthly stats are calculated from completed trades.\n" "Start trading to see monthly performance!", parse_mode='HTML' ) return monthly_text = "๐Ÿ“† Monthly Performance (Last 10 Months)\n\n" total_pnl = 0 total_trades = 0 trading_months = 0 for month_stats in monthly_stats: if month_stats['has_trades']: # Month with completed trades pnl_emoji = "๐ŸŸข" if month_stats['pnl'] >= 0 else "๐Ÿ”ด" monthly_text += f"๐Ÿ“… {month_stats['month_formatted']}\n" monthly_text += f" {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n" monthly_text += f" ๐Ÿ”„ Trades: {month_stats['trades']}\n\n" total_pnl += month_stats['pnl'] total_trades += month_stats['trades'] trading_months += 1 else: # Month with no trades monthly_text += f"๐Ÿ“… {month_stats['month_formatted']}\n" monthly_text += f" ๐Ÿ“ญ No completed trades\n\n" # Add summary if trading_months > 0: total_pnl_emoji = "๐ŸŸข" if total_pnl >= 0 else "๐Ÿ”ด" monthly_text += f"๐Ÿ’ผ 10-Month Summary:\n" monthly_text += f" {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n" monthly_text += f" ๐Ÿ”„ Total Trades: {total_trades}\n" monthly_text += f" ๐Ÿ“ˆ Trading Months: {trading_months}/10\n" monthly_text += f" ๐Ÿ“Š Avg per Trading Month: ${total_pnl/trading_months:,.2f}" else: monthly_text += f"๐Ÿ’ผ 10-Month Summary:\n" monthly_text += f" ๐Ÿ“ญ No completed trades in the last 10 months\n" monthly_text += f" ๐Ÿ’ก Start trading to see monthly performance!" await update.message.reply_text(monthly_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing monthly command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in monthly command: {e}") async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /risk command to show advanced risk metrics.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: # Get current balance for context balance = self.client.get_balance() current_balance = 0 if balance and balance.get('total'): current_balance = float(balance['total'].get('USDC', 0)) # Get risk metrics and basic stats risk_metrics = self.stats.get_risk_metrics() basic_stats = self.stats.get_basic_stats() # Check if we have enough data for risk calculations if basic_stats['completed_trades'] < 2: await update.message.reply_text( "๐Ÿ“Š Risk Analysis\n\n" "๐Ÿ“ญ Insufficient Data\n\n" f"โ€ข Current completed trades: {basic_stats['completed_trades']}\n" f"โ€ข Required for risk analysis: 2+ trades\n" f"โ€ข Daily balance snapshots: {len(self.stats.data.get('daily_balances', []))}\n\n" "๐Ÿ’ก To enable risk analysis:\n" "โ€ข Complete more trades to generate returns data\n" "โ€ข Bot automatically records daily balance snapshots\n" "โ€ข Risk metrics will be available after sufficient trading history\n\n" "๐Ÿ“ˆ Use /stats for current performance metrics", parse_mode='HTML' ) return # Format the risk analysis message risk_text = f""" ๐Ÿ“Š Risk Analysis & Advanced Metrics ๐ŸŽฏ Risk-Adjusted Performance: โ€ข Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f} โ€ข Sortino Ratio: {risk_metrics['sortino_ratio']:.3f} โ€ข Annual Volatility: {risk_metrics['volatility']:.2f}% ๐Ÿ“‰ Drawdown Analysis: โ€ข Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}% โ€ข Value at Risk (95%): {risk_metrics['var_95']:.2f}% ๐Ÿ’ฐ Portfolio Context: โ€ข Current Balance: ${current_balance:,.2f} โ€ข Initial Balance: ${basic_stats['initial_balance']:,.2f} โ€ข Total P&L: ${basic_stats['total_pnl']:,.2f} โ€ข Days Active: {basic_stats['days_active']} ๐Ÿ“Š Risk Interpretation: """ # Add interpretive guidance sharpe = risk_metrics['sharpe_ratio'] if sharpe > 2.0: risk_text += "โ€ข ๐ŸŸข Excellent risk-adjusted returns (Sharpe > 2.0)\n" elif sharpe > 1.0: risk_text += "โ€ข ๐ŸŸก Good risk-adjusted returns (Sharpe > 1.0)\n" elif sharpe > 0.5: risk_text += "โ€ข ๐ŸŸ  Moderate risk-adjusted returns (Sharpe > 0.5)\n" elif sharpe > 0: risk_text += "โ€ข ๐Ÿ”ด Poor risk-adjusted returns (Sharpe > 0)\n" else: risk_text += "โ€ข โšซ Negative risk-adjusted returns (Sharpe < 0)\n" max_dd = risk_metrics['max_drawdown'] if max_dd < 5: risk_text += "โ€ข ๐ŸŸข Low maximum drawdown (< 5%)\n" elif max_dd < 15: risk_text += "โ€ข ๐ŸŸก Moderate maximum drawdown (< 15%)\n" elif max_dd < 30: risk_text += "โ€ข ๐ŸŸ  High maximum drawdown (< 30%)\n" else: risk_text += "โ€ข ๐Ÿ”ด Very High maximum drawdown (> 30%)\n" volatility = risk_metrics['volatility'] if volatility < 10: risk_text += "โ€ข ๐ŸŸข Low portfolio volatility (< 10%)\n" elif volatility < 25: risk_text += "โ€ข ๐ŸŸก Moderate portfolio volatility (< 25%)\n" elif volatility < 50: risk_text += "โ€ข ๐ŸŸ  High portfolio volatility (< 50%)\n" else: risk_text += "โ€ข ๐Ÿ”ด Very High portfolio volatility (> 50%)\n" risk_text += f""" ๐Ÿ’ก Risk Definitions: โ€ข Sharpe Ratio: Risk-adjusted return (excess return / volatility) โ€ข Sortino Ratio: Return / downside volatility (focuses on bad volatility) โ€ข Max Drawdown: Largest peak-to-trough decline โ€ข VaR 95%: Maximum expected loss 95% of the time โ€ข Volatility: Annualized standard deviation of returns ๐Ÿ“ˆ Data Based On: โ€ข Completed Trades: {basic_stats['completed_trades']} โ€ข Daily Balance Records: {len(self.stats.data.get('daily_balances', []))} โ€ข Trading Period: {basic_stats['days_active']} days ๐Ÿ”„ Use /stats for trading performance metrics """ await update.message.reply_text(risk_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing risk command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in risk command: {e}") async def version_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /version command to show bot version and system info.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: # Get system info import platform import sys from datetime import datetime uptime_info = "Unknown" try: # Try to get process uptime if available 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: # psutil not available, skip uptime pass # Get stats info basic_stats = self.stats.get_basic_stats() # Safety checks for monitoring attributes order_monitoring_task = getattr(self, 'order_monitoring_task', None) alarms = getattr(self, 'alarms', []) version_text = f""" ๐Ÿค– Trading Bot Version & System Info ๐Ÿ“ฑ Bot Information: โ€ข Version: {self.version} โ€ข 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: โ€ข Order Monitoring: {'โœ… Active' if order_monitoring_task and not order_monitoring_task.done() else 'โŒ Inactive'} โ€ข External Trades: โœ… Active โ€ข Price Alarms: โœ… Active ({len(alarms)} active) โ€ข Risk Management: {'โœ… Enabled' if Config.RISK_MANAGEMENT_ENABLED else 'โŒ Disabled'} โฐ Current Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} """ await update.message.reply_text(version_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing version command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in version command: {e}") async def balance_adjustments_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /balance_adjustments command to show deposit/withdrawal history.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return try: # Get balance adjustments summary adjustments_summary = self.stats.get_balance_adjustments_summary() # Get detailed adjustments all_adjustments = self.stats.data.get('balance_adjustments', []) if not all_adjustments: await update.message.reply_text( "๐Ÿ’ฐ Balance Adjustments\n\n" "๐Ÿ“ญ No deposits or withdrawals detected yet.\n\n" "๐Ÿ’ก The bot automatically monitors for deposits and withdrawals\n" "every hour to maintain accurate P&L calculations.", parse_mode='HTML' ) return # Format the message adjustments_text = f""" ๐Ÿ’ฐ Balance Adjustments History ๐Ÿ“Š Summary: โ€ข Total Deposits: ${adjustments_summary['total_deposits']:,.2f} โ€ข Total Withdrawals: ${adjustments_summary['total_withdrawals']:,.2f} โ€ข Net Adjustment: ${adjustments_summary['net_adjustment']:,.2f} โ€ข Total Transactions: {adjustments_summary['adjustment_count']} ๐Ÿ“… Recent Adjustments: """ # Show last 10 adjustments recent_adjustments = sorted(all_adjustments, key=lambda x: x['timestamp'], reverse=True)[:10] for adj in recent_adjustments: try: # Format timestamp adj_time = datetime.fromisoformat(adj['timestamp']).strftime('%m/%d %H:%M') # Format type and amount if adj['type'] == 'deposit': emoji = "๐Ÿ’ฐ" amount_str = f"+${adj['amount']:,.2f}" else: # withdrawal emoji = "๐Ÿ’ธ" amount_str = f"-${abs(adj['amount']):,.2f}" adjustments_text += f"โ€ข {emoji} {adj_time}: {amount_str}\n" except Exception as adj_error: logger.warning(f"Error formatting adjustment: {adj_error}") continue adjustments_text += f""" ๐Ÿ’ก How it Works: โ€ข Bot checks for deposits/withdrawals every hour โ€ข Adjustments maintain accurate P&L calculations โ€ข Non-trading balance changes don't affect performance metrics โ€ข Trading statistics remain pure and accurate โฐ Last Check: {adjustments_summary['last_adjustment'][:16] if adjustments_summary['last_adjustment'] else 'Never'} """ await update.message.reply_text(adjustments_text.strip(), parse_mode='HTML') except Exception as e: error_message = f"โŒ Error processing balance adjustments command: {str(e)}" await update.message.reply_text(error_message) logger.error(f"Error in balance_adjustments command: {e}") def _get_position_state(self, symbol: str) -> Dict[str, Any]: """Get current position state for a symbol.""" if symbol not in self.position_tracker: self.position_tracker[symbol] = { 'contracts': 0.0, 'avg_entry_price': 0.0, 'total_cost_basis': 0.0, 'entry_count': 0, 'entry_history': [], # List of {price, amount, timestamp} 'last_update': datetime.now().isoformat() } return self.position_tracker[symbol] def _update_position_state(self, symbol: str, side: str, amount: float, price: float, timestamp: str = None): """Update position state with a new trade.""" if timestamp is None: timestamp = datetime.now().isoformat() position = self._get_position_state(symbol) if side.lower() == 'buy': # Adding to long position or reducing short position if position['contracts'] >= 0: # Opening/adding to long position new_cost = amount * price old_cost = position['total_cost_basis'] old_contracts = position['contracts'] position['contracts'] += amount position['total_cost_basis'] += new_cost position['avg_entry_price'] = position['total_cost_basis'] / position['contracts'] if position['contracts'] > 0 else 0 position['entry_count'] += 1 position['entry_history'].append({ 'price': price, 'amount': amount, 'timestamp': timestamp, 'side': 'buy' }) logger.info(f"๐Ÿ“ˆ Position updated: {symbol} LONG {position['contracts']:.6f} @ avg ${position['avg_entry_price']:.2f}") return 'long_opened' if old_contracts == 0 else 'long_increased' else: # Reducing short position reduction = min(amount, abs(position['contracts'])) position['contracts'] += reduction if position['contracts'] >= 0: # Short position fully closed or flipped to long if position['contracts'] == 0: self._reset_position_state(symbol) return 'short_closed' else: # Flipped to long - need to track new long position remaining_amount = amount - reduction position['contracts'] = remaining_amount position['total_cost_basis'] = remaining_amount * price position['avg_entry_price'] = price return 'short_closed_and_long_opened' else: return 'short_reduced' elif side.lower() == 'sell': # Adding to short position or reducing long position if position['contracts'] <= 0: # Opening/adding to short position position['contracts'] -= amount position['entry_count'] += 1 position['entry_history'].append({ 'price': price, 'amount': amount, 'timestamp': timestamp, 'side': 'sell' }) logger.info(f"๐Ÿ“‰ Position updated: {symbol} SHORT {abs(position['contracts']):.6f} @ ${price:.2f}") return 'short_opened' if position['contracts'] == -amount else 'short_increased' else: # Reducing long position reduction = min(amount, position['contracts']) position['contracts'] -= reduction # Adjust cost basis proportionally if position['contracts'] > 0: reduction_ratio = reduction / (position['contracts'] + reduction) position['total_cost_basis'] *= (1 - reduction_ratio) return 'long_reduced' else: # Long position fully closed if position['contracts'] == 0: self._reset_position_state(symbol) return 'long_closed' else: # Flipped to short remaining_amount = amount - reduction position['contracts'] = -remaining_amount return 'long_closed_and_short_opened' position['last_update'] = timestamp return 'unknown' def _reset_position_state(self, symbol: str): """Reset position state when position is fully closed.""" if symbol in self.position_tracker: del self.position_tracker[symbol] def _calculate_position_pnl(self, symbol: str, exit_amount: float, exit_price: float) -> Dict[str, float]: """Calculate P&L for a position exit.""" position = self._get_position_state(symbol) if position['contracts'] == 0: return {'pnl': 0.0, 'pnl_percent': 0.0} avg_entry = position['avg_entry_price'] if position['contracts'] > 0: # Long position pnl = exit_amount * (exit_price - avg_entry) else: # Short position pnl = exit_amount * (avg_entry - exit_price) cost_basis = exit_amount * avg_entry pnl_percent = (pnl / cost_basis * 100) if cost_basis > 0 else 0 return { 'pnl': pnl, 'pnl_percent': pnl_percent, 'avg_entry_price': avg_entry } async def _send_external_trade_notification(self, trade: Dict[str, Any]): """Send generic notification for external trades (fallback).""" try: symbol = trade.get('symbol', '') side = trade.get('side', '') amount = float(trade.get('amount', 0)) price = float(trade.get('price', 0)) timestamp = trade.get('timestamp', '') # Extract token from symbol token = symbol.split('/')[0] if '/' in symbol else symbol # Format timestamp try: trade_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) time_str = trade_time.strftime('%H:%M:%S') except: time_str = "Unknown" # Determine trade type and emoji side_emoji = "๐ŸŸข" if side.lower() == 'buy' else "๐Ÿ”ด" trade_value = amount * price message = f""" ๐Ÿ”„ External Trade Detected ๐Ÿ“Š Trade Details: โ€ข Token: {token} โ€ข Side: {side.upper()} โ€ข Amount: {amount} {token} โ€ข Price: ${price:,.2f} โ€ข Value: ${trade_value:,.2f} {side_emoji} Source: External Platform Trade โฐ Time: {time_str} ๐Ÿ“ˆ Note: This trade was executed outside the Telegram bot ๐Ÿ“Š Stats have been automatically updated """ await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Sent generic external trade notification: {side} {amount} {token}") except Exception as e: logger.error(f"โŒ Error sending external trade notification: {e}") async def _check_stop_losses(self, current_positions: list): """Check all positions for stop loss triggers and execute automatic exits.""" try: if not current_positions: return stop_loss_triggers = [] for position in current_positions: symbol = position.get('symbol') contracts = float(position.get('contracts', 0)) entry_price = float(position.get('entryPx', 0)) if not symbol or contracts == 0 or entry_price == 0: continue # Get current market price market_data = self.client.get_market_data(symbol) if not market_data or not market_data.get('ticker'): continue current_price = float(market_data['ticker'].get('last', 0)) if current_price == 0: continue # Calculate current P&L percentage if contracts > 0: # Long position pnl_percent = ((current_price - entry_price) / entry_price) * 100 else: # Short position pnl_percent = ((entry_price - current_price) / entry_price) * 100 # Check if stop loss should trigger if pnl_percent <= -Config.STOP_LOSS_PERCENTAGE: token = symbol.split('/')[0] if '/' in symbol else symbol stop_loss_triggers.append({ 'symbol': symbol, 'token': token, 'contracts': contracts, 'entry_price': entry_price, 'current_price': current_price, 'pnl_percent': pnl_percent }) # Execute stop losses for trigger in stop_loss_triggers: await self._execute_automatic_stop_loss(trigger) except Exception as e: logger.error(f"โŒ Error checking stop losses: {e}") async def _execute_automatic_stop_loss(self, trigger: Dict[str, Any]): """Execute an automatic stop loss order.""" try: symbol = trigger['symbol'] token = trigger['token'] contracts = trigger['contracts'] entry_price = trigger['entry_price'] current_price = trigger['current_price'] pnl_percent = trigger['pnl_percent'] # Determine the exit side (opposite of position) exit_side = 'sell' if contracts > 0 else 'buy' contracts_abs = abs(contracts) # Send notification before executing await self._send_stop_loss_notification(trigger, "triggered") # Execute the stop loss order (market order for immediate execution) try: if exit_side == 'sell': order = self.client.create_market_sell_order(symbol, contracts_abs) else: order = self.client.create_market_buy_order(symbol, contracts_abs) if order: logger.info(f"๐Ÿ›‘ Stop loss executed: {token} {exit_side} {contracts_abs} @ ${current_price}") # Record the trade in stats and update position tracking action_type = self.stats.record_trade_with_enhanced_tracking(symbol, exit_side, contracts_abs, current_price, order.get('id', 'stop_loss'), "auto_stop_loss") # Send success notification await self._send_stop_loss_notification(trigger, "executed", order) else: logger.error(f"โŒ Stop loss order failed for {token}") await self._send_stop_loss_notification(trigger, "failed") except Exception as order_error: logger.error(f"โŒ Stop loss order execution failed for {token}: {order_error}") await self._send_stop_loss_notification(trigger, "failed", error=str(order_error)) except Exception as e: logger.error(f"โŒ Error executing automatic stop loss: {e}") async def _send_stop_loss_notification(self, trigger: Dict[str, Any], status: str, order: Dict = None, error: str = None): """Send notification for stop loss events.""" try: token = trigger['token'] contracts = trigger['contracts'] entry_price = trigger['entry_price'] current_price = trigger['current_price'] pnl_percent = trigger['pnl_percent'] position_type = "LONG" if contracts > 0 else "SHORT" contracts_abs = abs(contracts) if status == "triggered": title = "๐Ÿ›‘ Stop Loss Triggered" status_text = f"Stop loss triggered at {Config.STOP_LOSS_PERCENTAGE}% loss" emoji = "๐Ÿšจ" elif status == "executed": title = "โœ… Stop Loss Executed" status_text = "Position closed automatically" emoji = "๐Ÿ›‘" elif status == "failed": title = "โŒ Stop Loss Failed" status_text = f"Stop loss execution failed{': ' + error if error else ''}" emoji = "โš ๏ธ" else: return # Calculate loss loss_value = contracts_abs * abs(current_price - entry_price) message = f""" {title} {emoji} Risk Management Alert ๐Ÿ“Š Position Details: โ€ข Token: {token} โ€ข Direction: {position_type} โ€ข Size: {contracts_abs} contracts โ€ข Entry Price: ${entry_price:,.2f} โ€ข Current Price: ${current_price:,.2f} ๐Ÿ”ด Loss Details: โ€ข Loss: ${loss_value:,.2f} ({pnl_percent:.2f}%) โ€ข Stop Loss Threshold: {Config.STOP_LOSS_PERCENTAGE}% ๐Ÿ“‹ Action: {status_text} โฐ Time: {datetime.now().strftime('%H:%M:%S')} """ if order and status == "executed": order_id = order.get('id', 'N/A') message += f"\n๐Ÿ†” Order ID: {order_id}" await self.send_message(message.strip()) logger.info(f"๐Ÿ“ข Sent stop loss notification: {token} {status}") except Exception as e: logger.error(f"โŒ Error sending stop loss notification: {e}") async def _process_filled_orders(self, filled_order_ids: set, current_positions: list): """Process filled orders using enhanced position tracking.""" try: # For bot-initiated orders, we'll detect changes in position size # and send appropriate notifications using the enhanced system # This method will be triggered when orders placed through the bot are filled # The external trade monitoring will handle trades made outside the bot # Update position tracking based on current positions await self._update_position_tracking(current_positions) except Exception as e: logger.error(f"โŒ Error processing filled orders: {e}") async def _update_position_tracking(self, current_positions: list): """Update the legacy position tracking data for compatibility.""" new_position_map = {} for position in current_positions: symbol = position.get('symbol') contracts = float(position.get('contracts', 0)) entry_price = float(position.get('entryPx', 0)) if symbol and contracts != 0: new_position_map[symbol] = { 'contracts': contracts, 'entry_price': entry_price } # Also update our enhanced position tracker if not already present if symbol not in self.position_tracker: self._get_position_state(symbol) self.position_tracker[symbol]['contracts'] = contracts self.position_tracker[symbol]['avg_entry_price'] = entry_price self.position_tracker[symbol]['total_cost_basis'] = contracts * entry_price self.last_known_positions = new_position_map async def keyboard_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /keyboard command to enable/show custom keyboard.""" if not self.is_authorized(update.effective_chat.id): await update.message.reply_text("โŒ Unauthorized access.") return custom_keyboard = self._create_custom_keyboard() if custom_keyboard: await update.message.reply_text( "โŒจ๏ธ Custom Keyboard Activated!\n\n" "๐ŸŽฏ Your quick buttons are now ready:\n" "โ€ข Daily - Daily performance\n" "โ€ข Performance - Performance stats\n" "โ€ข Balance - Account balance\n" "โ€ข Stats - Trading statistics\n" "โ€ข Positions - Open positions\n" "โ€ข Orders - Active orders\n" "โ€ข Price - Quick price check\n" "โ€ข Market - Market overview\n" "โ€ข Help - Help guide\n" "โ€ข Commands - Command menu\n\n" "๐Ÿ’ก How to use:\n" "Tap any button below instead of typing the command manually!\n\n" "๐Ÿ”ง These buttons will stay at the bottom of your chat.", parse_mode='HTML', reply_markup=custom_keyboard ) else: await update.message.reply_text( "โŒ Custom Keyboard Disabled\n\n" "๐Ÿ”ง To enable:\n" "โ€ข Set TELEGRAM_CUSTOM_KEYBOARD_ENABLED=true in your .env file\n" "โ€ข Restart the bot\n" "โ€ข Run /keyboard again\n\n" f"๐Ÿ“‹ Current config:\n" f"โ€ข Enabled: {Config.TELEGRAM_CUSTOM_KEYBOARD_ENABLED}\n" f"โ€ข Layout: {Config.TELEGRAM_CUSTOM_KEYBOARD_LAYOUT}", parse_mode='HTML' ) async def handle_keyboard_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle messages from custom keyboard buttons (without /).""" if not self.is_authorized(update.effective_chat.id): return message_text = update.message.text.lower() # Map clean button text to command handlers command_handlers = { 'daily': self.daily_command, 'performance': self.performance_command, 'balance': self.balance_command, 'stats': self.stats_command, 'positions': self.positions_command, 'orders': self.orders_command, 'price': self.price_command, 'market': self.market_command, 'help': self.help_command, 'commands': self.commands_command } # Execute the corresponding command handler if message_text in command_handlers: await command_handlers[message_text](update, context) async def main_async(): """Async main entry point for the Telegram bot.""" try: # Validate configuration if not Config.validate(): logger.error("โŒ Configuration validation failed!") return if not Config.TELEGRAM_ENABLED: logger.error("โŒ Telegram is not enabled in configuration") return # Create and run the bot bot = TelegramTradingBot() await bot.run() except KeyboardInterrupt: logger.info("๐Ÿ‘‹ Bot stopped by user") except Exception as e: logger.error(f"โŒ Unexpected error: {e}") raise def main(): """Main entry point for the Telegram bot.""" try: # Check if we're already in an asyncio context try: loop = asyncio.get_running_loop() # If we get here, we're already in an asyncio context logger.error("โŒ Cannot run main() from within an asyncio context. Use main_async() instead.") return except RuntimeError: # No running loop, safe to use asyncio.run() pass # Run the async main function asyncio.run(main_async()) except Exception as e: logger.error(f"โŒ Failed to start telegram bot: {e}") raise if __name__ == "__main__": main()