#!/usr/bin/env python3 """ Trading Commands - Handles all trading-related Telegram commands. """ import logging from typing import Optional from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes from src.config.config import Config from src.utils.token_display_formatter import get_formatter logger = logging.getLogger(__name__) def _normalize_token_case(token: str) -> str: """ Normalize token case: if any characters are already uppercase, keep as-is. Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK. """ # Check if any character is already uppercase if any(c.isupper() for c in token): return token # Keep original case for mixed-case tokens else: return token.upper() # Convert to uppercase for all-lowercase input class TradingCommands: """Handles all trading-related Telegram commands.""" def __init__(self, trading_engine, notification_manager, info_commands_handler=None, management_commands_handler=None): """Initialize with trading engine, notification manager, and other command handlers.""" self.trading_engine = trading_engine self.notification_manager = notification_manager self.info_commands_handler = info_commands_handler self.management_commands_handler = management_commands_handler def _is_authorized(self, chat_id: str) -> bool: """Check if the chat ID is authorized.""" return str(chat_id) == str(Config.TELEGRAM_CHAT_ID) async def long_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /long command for opening long positions.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await context.bot.send_message(chat_id=chat_id, 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 = _normalize_token_case(context.args[0]) 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 context.bot.send_message(chat_id=chat_id, 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 context.bot.send_message(chat_id=chat_id, text="❌ Invalid limit price format. Please use numbers only.") return # Get current market price market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC") if not market_data: await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}") return # Determine order type and price if limit_price: order_type = "Limit" price = limit_price token_amount = usdc_amount / price else: order_type = "Market" price = current_price token_amount = usdc_amount / current_price # Validate stop loss for long positions if stop_loss_price and stop_loss_price >= price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n" f"📊 Your order:\n" f"• Entry Price: {formatter.format_price_with_symbol(price, token)}\n" f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)} ❌\n\n" f"💡 Try a lower stop loss like: sl:{formatter.format_price(price * 0.95, token)}" )) return # Create confirmation message formatter = get_formatter() confirmation_text = f""" 🟢 Long Order Confirmation 📊 Order Details: • Token: {token} • USDC Amount: {formatter.format_price_with_symbol(usdc_amount)} • Token Amount: {formatter.format_amount(token_amount, token)} {token} • Order Type: {order_type} • Price: {formatter.format_price_with_symbol(price, token)} • Current Price: {formatter.format_price_with_symbol(current_price, token)} • Est. Value: {formatter.format_price_with_symbol(token_amount * price)}""" if stop_loss_price: confirmation_text += f""" • 🛑 Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}""" confirmation_text += f""" ⚠️ Are you sure you want to open this LONG position? This will {"place a limit buy order" if limit_price else "execute a market buy order"} for {token}.""" if stop_loss_price: confirmation_text += f"\nStop loss will be set automatically when order fills." # Create callback data for confirmation callback_data = f"confirm_long_{token}_{usdc_amount}_{price if limit_price else 'market'}" if stop_loss_price: callback_data += f"_sl_{stop_loss_price}" keyboard = [ [ InlineKeyboardButton("✅ Execute Long", callback_data=callback_data), InlineKeyboardButton("❌ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid input format: {e}") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing long command: {e}") logger.error(f"Error in long command: {e}") async def short_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /short command for opening short positions.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await context.bot.send_message(chat_id=chat_id, text=( "❌ Usage: /short [token] [USDC amount] [price (optional)] [sl:price (optional)]\n" "Examples:\n" "• /short BTC 100 - Market order\n" "• /short BTC 100 45000 - Limit order at $45,000\n" "• /short BTC 100 sl:46000 - Market order with stop loss at $46,000\n" "• /short BTC 100 45000 sl:46000 - Limit order at $45,000 with stop loss at $46,000" )) return token = _normalize_token_case(context.args[0]) usdc_amount = float(context.args[1]) # Parse arguments (similar to long_command) limit_price = None stop_loss_price = None for i, arg in enumerate(context.args[2:], 2): if arg.startswith('sl:'): try: stop_loss_price = float(arg[3:]) except ValueError: await context.bot.send_message(chat_id=chat_id, text="❌ Invalid stop loss price format. Use sl:price (e.g., sl:46000)") return elif limit_price is None: try: limit_price = float(arg) except ValueError: await context.bot.send_message(chat_id=chat_id, text="❌ Invalid limit price format. Please use numbers only.") return # Get current market price market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC") if not market_data: await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch market data for {token}") return current_price = float(market_data['ticker'].get('last', 0)) if current_price <= 0: await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid current price for {token}") return # Determine order type and price if limit_price: order_type = "Limit" price = limit_price token_amount = usdc_amount / price else: order_type = "Market" price = current_price token_amount = usdc_amount / current_price # Validate stop loss for short positions if stop_loss_price and stop_loss_price <= price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n" f"📊 Your order:\n" f"• Entry Price: {formatter.format_price_with_symbol(price, token)}\n" f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)} ❌\n\n" f"💡 Try a higher stop loss like: sl:{formatter.format_price(price * 1.05, token)}" )) return # Create confirmation message formatter = get_formatter() confirmation_text = f""" 🔴 Short Order Confirmation 📊 Order Details: • Token: {token} • USDC Amount: {formatter.format_price_with_symbol(usdc_amount)} • Token Amount: {formatter.format_amount(token_amount, token)} {token} • Order Type: {order_type} • Price: {formatter.format_price_with_symbol(price, token)} • Current Price: {formatter.format_price_with_symbol(current_price, token)} • Est. Value: {formatter.format_price_with_symbol(token_amount * price)}""" if stop_loss_price: confirmation_text += f""" • 🛑 Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}""" confirmation_text += f""" ⚠️ Are you sure you want to open this SHORT position? This will {"place a limit sell order" if limit_price else "execute a market sell order"} for {token}.""" if stop_loss_price: confirmation_text += f"\nStop loss will be set automatically when order fills." # Create callback data for confirmation callback_data = f"confirm_short_{token}_{usdc_amount}_{price if limit_price else 'market'}" if stop_loss_price: callback_data += f"_sl_{stop_loss_price}" keyboard = [ [ InlineKeyboardButton("✅ Execute Short", callback_data=callback_data), InlineKeyboardButton("❌ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Invalid input format: {e}") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing short command: {e}") logger.error(f"Error in short command: {e}") async def exit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /exit command for closing positions.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 1: await context.bot.send_message(chat_id=chat_id, text=( "❌ Usage: /exit [token]\n" "Example: /exit BTC\n\n" "This closes your entire position for the specified token." )) return token = _normalize_token_case(context.args[0]) # Find the position position = self.trading_engine.find_position(token) if not position: await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}") return # Get position details position_type, exit_side, contracts = self.trading_engine.get_position_direction(position) entry_price = float(position.get('entryPx', 0)) unrealized_pnl = float(position.get('unrealizedPnl', 0)) # Get current market price market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC") if not market_data: await context.bot.send_message(chat_id=chat_id, text=f"❌ Could not fetch current price for {token}") return current_price = float(market_data['ticker'].get('last', 0)) exit_value = contracts * current_price # Create confirmation message pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴" exit_emoji = "🔴" if position_type == "LONG" else "🟢" formatter = get_formatter() confirmation_text = f""" {exit_emoji} Exit Position Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {formatter.format_amount(contracts, token)} contracts • Entry Price: {formatter.format_price_with_symbol(entry_price, token)} • Current Price: {formatter.format_price_with_symbol(current_price, token)} • {pnl_emoji} Unrealized P&L: {formatter.format_price_with_symbol(unrealized_pnl)} 🎯 Exit Order: • Action: {exit_side.upper()} (Close {position_type}) • Amount: {formatter.format_amount(contracts, token)} {token} • Est. Value: ~{formatter.format_price_with_symbol(exit_value)} • Order Type: Market Order ⚠️ Are you sure you want to close this {position_type} position? """ keyboard = [ [ InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}"), InlineKeyboardButton("❌ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing exit command: {e}") logger.error(f"Error in exit command: {e}") async def sl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /sl (stop loss) command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await context.bot.send_message(chat_id=chat_id, text=( "❌ Usage: /sl [token] [price]\n" "Example: /sl BTC 44000\n\n" "This sets a stop loss order for your existing position." )) return token = _normalize_token_case(context.args[0]) stop_price = float(context.args[1]) # Find the position position = self.trading_engine.find_position(token) if not position: await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}") return # Get position details position_type, exit_side, contracts = self.trading_engine.get_position_direction(position) entry_price = float(position.get('entryPx', 0)) # Validate stop loss price based on position direction if position_type == "LONG" and stop_price >= entry_price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Stop loss price should be BELOW entry price for long positions\n\n" f"📊 Your {token} LONG position:\n" f"• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}\n" f"• Stop Price: {formatter.format_price_with_symbol(stop_price, token)} ❌\n\n" f"💡 Try a lower price like: /sl {token} {formatter.format_price(entry_price * 0.95, token)}\n" )) return elif position_type == "SHORT" and stop_price <= entry_price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Stop loss price should be ABOVE entry price for short positions\n\n" f"📊 Your {token} SHORT position:\n" f"• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}\n" f"• Stop Price: {formatter.format_price_with_symbol(stop_price, token)} ❌\n\n" f"💡 Try a higher price like: /sl {token} {formatter.format_price(entry_price * 1.05, token)}\n" )) return # Get current market price market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC") current_price = 0 if market_data: current_price = float(market_data['ticker'].get('last', 0)) # Calculate estimated P&L at stop loss if position_type == "LONG": pnl_at_stop = (stop_price - entry_price) * contracts else: # SHORT pnl_at_stop = (entry_price - stop_price) * contracts pnl_emoji = "🟢" if pnl_at_stop >= 0 else "🔴" formatter = get_formatter() confirmation_text = f""" 🛑 Stop Loss Order Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {formatter.format_amount(contracts, token)} contracts • Entry Price: {formatter.format_price_with_symbol(entry_price, token)} • Current Price: {formatter.format_price_with_symbol(current_price, token)} 🎯 Stop Loss Order: • Stop Price: {formatter.format_price_with_symbol(stop_price, token)} • Action: {exit_side.upper()} (Close {position_type}) • Amount: {formatter.format_amount(contracts, token)} {token} • Order Type: Limit Order • {pnl_emoji} Est. P&L: {formatter.format_price_with_symbol(pnl_at_stop)} ⚠️ Are you sure you want to set this stop loss? This will place a limit {exit_side} order at {formatter.format_price_with_symbol(stop_price, token)} to protect your {position_type} position. """ keyboard = [ [ InlineKeyboardButton("✅ Set Stop Loss", callback_data=f"confirm_sl_{token}_{stop_price}"), InlineKeyboardButton("❌ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing stop loss command: {e}") logger.error(f"Error in sl command: {e}") async def tp_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /tp (take profit) command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 2: await context.bot.send_message(chat_id=chat_id, text=( "❌ Usage: /tp [token] [price]\n" "Example: /tp BTC 50000\n\n" "This sets a take profit order for your existing position." )) return token = _normalize_token_case(context.args[0]) tp_price = float(context.args[1]) # Find the position position = self.trading_engine.find_position(token) if not position: await context.bot.send_message(chat_id=chat_id, text=f"📭 No open position found for {token}") return # Get position details position_type, exit_side, contracts = self.trading_engine.get_position_direction(position) entry_price = float(position.get('entryPx', 0)) # Validate take profit price based on position direction if position_type == "LONG" and tp_price <= entry_price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Take profit price should be ABOVE entry price for long positions\n\n" f"📊 Your {token} LONG position:\n" f"• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}\n" f"• Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ❌\n\n" f"💡 Try a higher price like: /tp {token} {formatter.format_price(entry_price * 1.05, token)}\n" )) return elif position_type == "SHORT" and tp_price >= entry_price: formatter = get_formatter() await context.bot.send_message(chat_id=chat_id, text=( f"⚠️ Take profit price should be BELOW entry price for short positions\n\n" f"📊 Your {token} SHORT position:\n" f"• Entry Price: {formatter.format_price_with_symbol(entry_price, token)}\n" f"• Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ❌\n\n" f"💡 Try a lower price like: /tp {token} {formatter.format_price(entry_price * 0.95, token)}\n" )) return # Get current market price market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC") current_price = 0 if market_data: current_price = float(market_data['ticker'].get('last', 0)) # Calculate estimated P&L at take profit if position_type == "LONG": pnl_at_tp = (tp_price - entry_price) * contracts else: # SHORT pnl_at_tp = (entry_price - tp_price) * contracts pnl_emoji = "🟢" if pnl_at_tp >= 0 else "🔴" formatter = get_formatter() confirmation_text = f""" 🎯 Take Profit Order Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {formatter.format_amount(contracts, token)} contracts • Entry Price: {formatter.format_price_with_symbol(entry_price, token)} • Current Price: {formatter.format_price_with_symbol(current_price, token)} 🎯 Take Profit Order: • Target Price: {formatter.format_price_with_symbol(tp_price, token)} • Action: {exit_side.upper()} (Close {position_type}) • Amount: {formatter.format_amount(contracts, token)} {token} • Order Type: Limit Order • {pnl_emoji} Est. P&L: {formatter.format_price_with_symbol(pnl_at_tp)} ⚠️ Are you sure you want to set this take profit? This will place a limit {exit_side} order at {formatter.format_price_with_symbol(tp_price, token)} to secure profit on your {position_type} position. """ keyboard = [ [ InlineKeyboardButton("✅ Set Take Profit", callback_data=f"confirm_tp_{token}_{tp_price}"), InlineKeyboardButton("❌ Cancel", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except ValueError: await context.bot.send_message(chat_id=chat_id, text="❌ Invalid price format. Please use numbers only.") except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing take profit command: {e}") logger.error(f"Error in tp command: {e}") async def coo_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the /coo (cancel all orders) command.""" chat_id = update.effective_chat.id if not self._is_authorized(chat_id): await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.") return try: if not context.args or len(context.args) < 1: await context.bot.send_message(chat_id=chat_id, text=( "❌ Usage: /coo [token]\n" "Example: /coo BTC\n\n" "This cancels all open orders for the specified token." )) return token = _normalize_token_case(context.args[0]) confirmation_text = f""" 🚫 Cancel All Orders Confirmation 📊 Action: Cancel all open orders for {token} ⚠️ Are you sure you want to cancel all {token} orders? This will cancel ALL pending orders for {token}, including: • Limit orders • Stop loss orders • Take profit orders This action cannot be undone. """ keyboard = [ [ InlineKeyboardButton(f"✅ Cancel All {token} Orders", callback_data=f"confirm_coo_{token}"), InlineKeyboardButton("❌ Keep Orders", callback_data="cancel_order") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await context.bot.send_message(chat_id=chat_id, text=confirmation_text, parse_mode='HTML', reply_markup=reply_markup) except Exception as e: await context.bot.send_message(chat_id=chat_id, text=f"❌ Error processing cancel orders command: {e}") logger.error(f"Error in coo command: {e}") async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle button callbacks for trading commands.""" 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 logger.info(f"Button callback triggered with data: {callback_data}") # Define a map for informational and management command callbacks # These commands expect `update` and `context` as if called by a CommandHandler command_action_map = {} if self.info_commands_handler: command_action_map.update({ "balance": self.info_commands_handler.balance_command, "positions": self.info_commands_handler.positions_command, "orders": self.info_commands_handler.orders_command, "stats": self.info_commands_handler.stats_command, "price": self.info_commands_handler.price_command, "market": self.info_commands_handler.market_command, "performance": self.info_commands_handler.performance_command, "daily": self.info_commands_handler.daily_command, "weekly": self.info_commands_handler.weekly_command, "monthly": self.info_commands_handler.monthly_command, "trades": self.info_commands_handler.trades_command, # Note: 'help' is handled separately below as its main handler is in TelegramTradingBot core }) if self.management_commands_handler: command_action_map.update({ "alarm": self.management_commands_handler.alarm_command, "monitoring": self.management_commands_handler.monitoring_command, "logs": self.management_commands_handler.logs_command, # Add other management commands here if they have quick action buttons }) # Prepare key for map lookup, stripping leading '/' if present for general commands mapped_command_key = callback_data if callback_data.startswith('/') and not callback_data.startswith('/confirm_'): # Avoid stripping for confirm actions mapped_command_key = callback_data[1:] # Check if the callback_data matches a mapped informational/management command if mapped_command_key in command_action_map: command_method = command_action_map[mapped_command_key] try: logger.info(f"Executing {mapped_command_key} command (from callback: {callback_data}) via button callback.") # Edit the original message to indicate the action is being processed # await query.edit_message_text(text=f"🔄 Processing {mapped_command_key.capitalize()}...", parse_mode='HTML') # Optional await command_method(update, context) # Call the actual command method # After the command sends its own message(s), we might want to remove or clean up the original message with buttons. # For now, let the command method handle all responses. # Optionally, delete the message that had the buttons: # await query.message.delete() except Exception as e: logger.error(f"Error executing command '{mapped_command_key}' from button: {e}", exc_info=True) try: await query.message.reply_text(f"❌ Error processing {mapped_command_key.capitalize()}: {e}") except Exception as reply_e: logger.error(f"Failed to send error reply for {mapped_command_key} button: {reply_e}") return # Handled # Special handling for 'help' callback from InfoCommands quick menu # This check should use the modified key as well if we want /help to work via this mechanism # However, the 'help' key in command_action_map is 'help', not '/help'. # The current 'help' handling is separate and specific. Let's adjust it for consistency if needed or verify. # The previous change to info_commands.py made help callback_data='/help'. if callback_data == "/help": # Check for the actual callback_data value logger.info("Handling '/help' button callback. Guiding user to /help command.") try: # Remove the inline keyboard from the original message and provide guidance. await query.edit_message_text( text="📖 To view all commands and their descriptions, please type the /help command.", reply_markup=None, # Remove buttons parse_mode='HTML' ) except Exception as e: logger.error(f"Error editing message for 'help' callback: {e}") # Fallback if edit fails (e.g., message too old) await query.message.reply_text("📖 Please type /help for command details.", parse_mode='HTML') return # Handled # Existing trading confirmation logic if callback_data.startswith("confirm_long_"): await self._execute_long_callback(query, callback_data) elif callback_data.startswith("confirm_short_"): await self._execute_short_callback(query, callback_data) elif callback_data.startswith("confirm_exit_"): await self._execute_exit_callback(query, callback_data) elif callback_data.startswith("confirm_sl_"): await self._execute_sl_callback(query, callback_data) elif callback_data.startswith("confirm_tp_"): await self._execute_tp_callback(query, callback_data) elif callback_data.startswith("confirm_coo_"): await self._execute_coo_callback(query, callback_data) elif callback_data == 'cancel_order': await query.edit_message_text("❌ Order cancelled.") async def _execute_long_callback(self, query, callback_data): """Execute long order from callback.""" parts = callback_data.split('_') token = parts[2] usdc_amount = float(parts[3]) price = None if parts[4] == 'market' else float(parts[4]) stop_loss_price = None # Check for stop loss if len(parts) > 5 and parts[5] == 'sl': stop_loss_price = float(parts[6]) await query.edit_message_text("⏳ Executing long order...") result = await self.trading_engine.execute_long_order(token, usdc_amount, price, stop_loss_price) if result["success"]: # Extract data from new response format order_details = result.get("order_placed_details", {}) token_amount = result.get("token_amount", 0) price_used = order_details.get("price_requested") or price trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID # Create a mock order object for backward compatibility with notification method mock_order = { "id": order_details.get("exchange_order_id", "N/A"), "price": price_used } await self.notification_manager.send_long_success_notification( query, token, token_amount, price_used, mock_order, stop_loss_price, trade_lifecycle_id ) else: await query.edit_message_text(f"❌ Long order failed: {result['error']}") async def _execute_short_callback(self, query, callback_data): """Execute short order from callback.""" parts = callback_data.split('_') token = parts[2] usdc_amount = float(parts[3]) price = None if parts[4] == 'market' else float(parts[4]) stop_loss_price = None # Check for stop loss if len(parts) > 5 and parts[5] == 'sl': stop_loss_price = float(parts[6]) await query.edit_message_text("⏳ Executing short order...") result = await self.trading_engine.execute_short_order(token, usdc_amount, price, stop_loss_price) if result["success"]: # Extract data from new response format order_details = result.get("order_placed_details", {}) token_amount = result.get("token_amount", 0) price_used = order_details.get("price_requested") or price trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID # Create a mock order object for backward compatibility with notification method mock_order = { "id": order_details.get("exchange_order_id", "N/A"), "price": price_used } await self.notification_manager.send_short_success_notification( query, token, token_amount, price_used, mock_order, stop_loss_price, trade_lifecycle_id ) else: await query.edit_message_text(f"❌ Short order failed: {result['error']}") async def _execute_exit_callback(self, query, callback_data): """Execute exit order from callback.""" parts = callback_data.split('_') token = parts[2] await query.edit_message_text("⏳ Closing position...") result = await self.trading_engine.execute_exit_order(token) if result["success"]: # Extract data from new response format order_details = result.get("order_placed_details", {}) position_type_closed = result.get("position_type_closed", "UNKNOWN") contracts_intended_to_close = result.get("contracts_intended_to_close", 0) cancelled_stop_losses = result.get("cancelled_stop_losses", 0) trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID # For market orders, we won't have the actual execution price until the fill # We'll use 0 for now since this will be updated by MarketMonitor when the fill occurs estimated_price = 0 # Market order - actual price will be determined by fill estimated_pnl = 0 # PnL will be calculated when fill is processed # Create a mock order object for backward compatibility mock_order = { "id": order_details.get("exchange_order_id", "N/A"), "cancelled_stop_losses": cancelled_stop_losses } await self.notification_manager.send_exit_success_notification( query, token, position_type_closed, contracts_intended_to_close, estimated_price, estimated_pnl, mock_order, trade_lifecycle_id ) else: await query.edit_message_text(f"❌ Exit order failed: {result['error']}") async def _execute_sl_callback(self, query, callback_data): """Execute stop loss order from callback.""" parts = callback_data.split('_') token = parts[2] stop_price = float(parts[3]) await query.edit_message_text("⏳ Setting stop loss...") result = await self.trading_engine.execute_sl_order(token, stop_price) if result["success"]: trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID await self.notification_manager.send_sl_success_notification( query, token, result["position_type_for_sl"], result["contracts_for_sl"], stop_price, result.get("order_placed_details", {}), trade_lifecycle_id ) else: await query.edit_message_text(f"❌ Stop loss failed: {result['error']}") async def _execute_tp_callback(self, query, callback_data): """Execute take profit order from callback.""" parts = callback_data.split('_') token = parts[2] tp_price = float(parts[3]) await query.edit_message_text("⏳ Setting take profit...") result = await self.trading_engine.execute_tp_order(token, tp_price) if result["success"]: trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID await self.notification_manager.send_tp_success_notification( query, token, result["position_type_for_tp"], result["contracts_for_tp"], tp_price, result.get("order_placed_details", {}), trade_lifecycle_id ) else: await query.edit_message_text(f"❌ Take profit failed: {result['error']}") async def _execute_coo_callback(self, query, callback_data): """Execute cancel all orders from callback.""" parts = callback_data.split('_') token = parts[2] await query.edit_message_text("⏳ Cancelling orders...") result = await self.trading_engine.execute_coo_order(token) if result["success"]: cancelled_count = result.get("cancelled_count", 0) failed_count = result.get("failed_count", 0) cancelled_linked_sls = result.get("cancelled_linked_stop_losses", 0) cancelled_orders = result.get("cancelled_orders", []) await self.notification_manager.send_coo_success_notification( query, token, cancelled_count, failed_count, cancelled_linked_sls, cancelled_orders ) else: await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}")