#!/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 logger = logging.getLogger(__name__) class TradingCommands: """Handles all trading-related Telegram commands.""" def __init__(self, trading_engine, notification_manager): """Initialize with trading engine and notification manager.""" self.trading_engine = trading_engine self.notification_manager = notification_manager 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 = 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 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: 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: ${price:,.2f}\n" f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n" f"💡 Try a lower stop loss like: sl:{price * 0.95:.0f}" )) return # Create confirmation message confirmation_text = f""" 🟢 Long Order Confirmation 📊 Order Details: • Token: {token} • USDC Amount: ${usdc_amount:,.2f} • Token Amount: {token_amount:.6f} {token} • Order Type: {order_type} • Price: ${price:,.2f} • Current Price: ${current_price:,.2f} • Est. Value: ${token_amount * price:,.2f} {f"🛑 Stop Loss: ${stop_loss_price:,.2f}" if stop_loss_price else ""} ⚠️ 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}. """ # 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 = context.args[0].upper() 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: 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: ${price:,.2f}\n" f"• Stop Loss: ${stop_loss_price:,.2f} ❌\n\n" f"💡 Try a higher stop loss like: sl:{price * 1.05:.0f}" )) return # Create confirmation message confirmation_text = f""" 🔴 Short Order Confirmation 📊 Order Details: • Token: {token} • USDC Amount: ${usdc_amount:,.2f} • Token Amount: {token_amount:.6f} {token} • Order Type: {order_type} • Price: ${price:,.2f} • Current Price: ${current_price:,.2f} • Est. Value: ${token_amount * price:,.2f} {f"🛑 Stop Loss: ${stop_loss_price:,.2f}" if stop_loss_price else ""} ⚠️ 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}. """ # 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 = context.args[0].upper() # 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 "🟢" confirmation_text = f""" {exit_emoji} Exit Position Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {contracts:.6f} 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:.6f} {token} • Est. Value: ~${exit_value:,.2f} • 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 = context.args[0].upper() 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: 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: ${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 elif position_type == "SHORT" and stop_price <= entry_price: 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: ${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 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 "🔴" confirmation_text = f""" 🛑 Stop Loss Order Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {contracts:.6f} 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:.6f} {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("✅ 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 = context.args[0].upper() 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: 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: ${entry_price:,.2f}\n" f"• Take Profit: ${tp_price:,.2f} ❌\n\n" f"💡 Try a higher price like: /tp {token} {entry_price * 1.05:.0f}" )) return elif position_type == "SHORT" and tp_price >= entry_price: 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: ${entry_price:,.2f}\n" f"• Take Profit: ${tp_price:,.2f} ❌\n\n" f"💡 Try a lower price like: /tp {token} {entry_price * 0.95:.0f}" )) 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 "🔴" confirmation_text = f""" 🎯 Take Profit Order Confirmation 📊 Position Details: • Token: {token} • Position: {position_type} • Size: {contracts:.6f} contracts • Entry Price: ${entry_price:,.2f} • Current Price: ${current_price:,.2f} 🎯 Take Profit Order: • Take Profit Price: ${tp_price:,.2f} • Action: {exit_side.upper()} (Close {position_type}) • Amount: {contracts:.6f} {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 ${tp_price:,.2f} to secure profits from 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 = context.args[0].upper() 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 try: 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.") # Handle info command button callbacks elif callback_data in ['balance', 'positions', 'orders', 'stats', 'trades', 'market', 'price', 'performance', 'daily', 'weekly', 'monthly', 'alarm', 'monitoring', 'logs', 'help']: await query.edit_message_text(f"✅ Please use /{callback_data} command to get the latest data.") except Exception as e: await query.edit_message_text(f"❌ Error processing order: {e}") logger.error(f"Error in button callback: {e}") 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"]: await self.notification_manager.send_long_success_notification( query, token, result["token_amount"], result["actual_price"], result["order"], stop_loss_price ) 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"]: await self.notification_manager.send_short_success_notification( query, token, result["token_amount"], result["actual_price"], result["order"], stop_loss_price ) 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"]: await self.notification_manager.send_exit_success_notification( query, token, result["position_type"], result["contracts"], result["actual_price"], result["pnl"], result["order"] ) 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"]: await self.notification_manager.send_sl_success_notification( query, token, result["position_type"], result["contracts"], stop_price, result["order"] ) 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"]: await self.notification_manager.send_tp_success_notification( query, token, result["position_type"], result["contracts"], tp_price, result["order"] ) 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"]: await self.notification_manager.send_coo_success_notification( query, token, result["cancelled_count"], result["failed_count"] ) else: await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}")