123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880 |
- #!/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"""
- 🟢 <b>Long Order Confirmation</b>
- 📊 <b>Order Details:</b>
- • 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"""
- ⚠️ <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}."""
-
- 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"""
- 🔴 <b>Short Order Confirmation</b>
- 📊 <b>Order Details:</b>
- • 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"""
- ⚠️ <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}."""
-
- 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} <b>Exit Position Confirmation</b>
- 📊 <b>Position Details:</b>
- • 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)}
- 🎯 <b>Exit Order:</b>
- • 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
- ⚠️ <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 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"""
- 🛑 <b>Stop Loss Order Confirmation</b>
- 📊 <b>Position Details:</b>
- • 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)}
- 🎯 <b>Stop Loss Order:</b>
- • 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)}
- ⚠️ <b>Are you sure you want to set this stop loss?</b>
- 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"""
- 🎯 <b>Take Profit Order Confirmation</b>
- 📊 <b>Position Details:</b>
- • 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)}
- 🎯 <b>Take Profit Order:</b>
- • 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)}
- ⚠️ <b>Are you sure you want to set this take profit?</b>
- 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"""
- 🚫 <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 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']}")
|