|
@@ -0,0 +1,719 @@
|
|
|
+#!/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."""
|
|
|
+ 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
|
|
|
+
|
|
|
+ # Get current market price
|
|
|
+ market_data = self.trading_engine.get_market_data(f"{token}/USDC:USDC")
|
|
|
+ if not market_data:
|
|
|
+ await update.message.reply_text(f"❌ Could not fetch market data 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
|
|
|
+
|
|
|
+ # 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 update.message.reply_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"""
|
|
|
+🟢 <b>Long Order Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Order Details:</b>
|
|
|
+• 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 ""}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to open this LONG position?</b>
|
|
|
+
|
|
|
+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 update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
+
|
|
|
+ except ValueError as e:
|
|
|
+ await update.message.reply_text(f"❌ Invalid input format: {e}")
|
|
|
+ except Exception as e:
|
|
|
+ await update.message.reply_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."""
|
|
|
+ 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 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 update.message.reply_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 update.message.reply_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 update.message.reply_text(f"❌ Could not fetch market data 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
|
|
|
+
|
|
|
+ # 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 update.message.reply_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"""
|
|
|
+🔴 <b>Short Order Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Order Details:</b>
|
|
|
+• 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 ""}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to open this SHORT position?</b>
|
|
|
+
|
|
|
+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 update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
+
|
|
|
+ except ValueError as e:
|
|
|
+ await update.message.reply_text(f"❌ Invalid input format: {e}")
|
|
|
+ except Exception as e:
|
|
|
+ await update.message.reply_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."""
|
|
|
+ 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\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 update.message.reply_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 update.message.reply_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} <b>Exit Position Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Position Details:</b>
|
|
|
+• 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}
|
|
|
+
|
|
|
+🎯 <b>Exit Order:</b>
|
|
|
+• Action: {exit_side.upper()} (Close {position_type})
|
|
|
+• Amount: {contracts:.6f} {token}
|
|
|
+• Est. Value: ~${exit_value:,.2f}
|
|
|
+• Order Type: Market Order
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to close this {position_type} position?</b>
|
|
|
+ """
|
|
|
+
|
|
|
+ keyboard = [
|
|
|
+ [
|
|
|
+ InlineKeyboardButton(f"✅ Close {position_type}", callback_data=f"confirm_exit_{token}"),
|
|
|
+ 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 Exception as e:
|
|
|
+ await update.message.reply_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."""
|
|
|
+ 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 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 update.message.reply_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 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
|
|
|
+ elif position_type == "SHORT" and 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
|
|
|
+ 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"""
|
|
|
+🛑 <b>Stop Loss Order Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Position Details:</b>
|
|
|
+• Token: {token}
|
|
|
+• Position: {position_type}
|
|
|
+• Size: {contracts:.6f} contracts
|
|
|
+• Entry Price: ${entry_price:,.2f}
|
|
|
+• Current Price: ${current_price:,.2f}
|
|
|
+
|
|
|
+🎯 <b>Stop Loss Order:</b>
|
|
|
+• 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}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to set this stop loss?</b>
|
|
|
+
|
|
|
+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 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}")
|
|
|
+ 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."""
|
|
|
+ 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 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 update.message.reply_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 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: ${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 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: ${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"""
|
|
|
+🎯 <b>Take Profit Order Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Position Details:</b>
|
|
|
+• Token: {token}
|
|
|
+• Position: {position_type}
|
|
|
+• Size: {contracts:.6f} contracts
|
|
|
+• Entry Price: ${entry_price:,.2f}
|
|
|
+• Current Price: ${current_price:,.2f}
|
|
|
+
|
|
|
+🎯 <b>Take Profit Order:</b>
|
|
|
+• 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}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to set this take profit?</b>
|
|
|
+
|
|
|
+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 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}")
|
|
|
+ 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."""
|
|
|
+ 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 cancels all open orders for the specified token."
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ token = context.args[0].upper()
|
|
|
+
|
|
|
+ confirmation_text = f"""
|
|
|
+🚫 <b>Cancel All Orders Confirmation</b>
|
|
|
+
|
|
|
+📊 <b>Action:</b> Cancel all open orders for {token}
|
|
|
+
|
|
|
+⚠️ <b>Are you sure you want to cancel all {token} orders?</b>
|
|
|
+
|
|
|
+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 update.message.reply_text(confirmation_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ await update.message.reply_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.")
|
|
|
+
|
|
|
+ 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']}")
|