|
@@ -10,7 +10,7 @@ class OrdersCommands(InfoCommandsBase):
|
|
"""Handles all order-related commands."""
|
|
"""Handles all order-related commands."""
|
|
|
|
|
|
async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
- """Handle the /orders command."""
|
|
|
|
|
|
+ """Handle the /orders command with exchange-first approach."""
|
|
try:
|
|
try:
|
|
if not self._is_authorized(update):
|
|
if not self._is_authorized(update):
|
|
await self._reply(update, "❌ Unauthorized access.")
|
|
await self._reply(update, "❌ Unauthorized access.")
|
|
@@ -21,82 +21,307 @@ class OrdersCommands(InfoCommandsBase):
|
|
await self._reply(update, "❌ Trading stats not available.")
|
|
await self._reply(update, "❌ Trading stats not available.")
|
|
return
|
|
return
|
|
|
|
|
|
- # Get open orders from database (much faster than exchange query)
|
|
|
|
- db_orders = stats.get_orders_by_status('open', limit=50)
|
|
|
|
- db_orders.extend(stats.get_orders_by_status('submitted', limit=50))
|
|
|
|
- db_orders.extend(stats.get_orders_by_status('pending_trigger', limit=50))
|
|
|
|
-
|
|
|
|
- # Get pending stop loss orders from the database
|
|
|
|
- pending_sl_lifecycles = stats.get_pending_stop_losses_from_lifecycles()
|
|
|
|
|
|
+ # Get exchange orders (primary source of truth)
|
|
|
|
+ exchange_orders = self.trading_engine.get_orders() or []
|
|
|
|
+
|
|
|
|
+ # Get pending stop loss orders (bot-internal only)
|
|
|
|
+ pending_sl_orders = stats.get_pending_stop_losses_from_lifecycles()
|
|
|
|
|
|
- # Combine both lists
|
|
|
|
- all_orders = db_orders + pending_sl_lifecycles
|
|
|
|
|
|
+ # Clean up stale pending SL orders
|
|
|
|
+ await self._cleanup_stale_pending_sl_orders(stats, exchange_orders)
|
|
|
|
+
|
|
|
|
+ if not exchange_orders and not pending_sl_orders:
|
|
|
|
+ await self._reply(update, "📭 No open orders found")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ # Format exchange-first orders message
|
|
|
|
+ orders_text = await self._format_exchange_first_orders_message(exchange_orders, pending_sl_orders)
|
|
|
|
+ await self._reply(update, orders_text.strip())
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error in orders command: {e}", exc_info=True)
|
|
|
|
+ await self._reply(update, "❌ Error retrieving order information.")
|
|
|
|
|
|
- if not all_orders:
|
|
|
|
- await self._reply(update, "📭 No open or pending orders")
|
|
|
|
|
|
+ async def _cleanup_stale_pending_sl_orders(self, stats, exchange_orders: List[Dict]):
|
|
|
|
+ """Clean up pending SL orders when no more limit orders or positions exist for that token."""
|
|
|
|
+ try:
|
|
|
|
+ pending_sl_orders = stats.get_pending_stop_losses_from_lifecycles()
|
|
|
|
+ if not pending_sl_orders:
|
|
return
|
|
return
|
|
|
|
+
|
|
|
|
+ # Get current positions
|
|
|
|
+ positions = self.trading_engine.get_positions() or []
|
|
|
|
+ position_tokens = {pos.get('symbol', '').split('/')[0] for pos in positions if pos.get('symbol')}
|
|
|
|
+
|
|
|
|
+ # Get tokens with open limit orders on exchange
|
|
|
|
+ limit_order_tokens = set()
|
|
|
|
+ for order in exchange_orders:
|
|
|
|
+ if order.get('type') == 'limit' and not order.get('reduceOnly', False):
|
|
|
|
+ symbol = order.get('symbol', '')
|
|
|
|
+ token = symbol.split('/')[0] if symbol else ''
|
|
|
|
+ if token:
|
|
|
|
+ limit_order_tokens.add(token)
|
|
|
|
+
|
|
|
|
+ # Find stale pending SL orders
|
|
|
|
+ stale_lifecycles = []
|
|
|
|
+ for pending_sl in pending_sl_orders:
|
|
|
|
+ symbol = pending_sl.get('symbol', '')
|
|
|
|
+ token = symbol.split('/')[0] if symbol else ''
|
|
|
|
+
|
|
|
|
+ # If no position and no limit orders for this token, mark as stale
|
|
|
|
+ if token and token not in position_tokens and token not in limit_order_tokens:
|
|
|
|
+ lifecycle_id = pending_sl.get('trade_lifecycle_id')
|
|
|
|
+ if lifecycle_id:
|
|
|
|
+ stale_lifecycles.append(lifecycle_id)
|
|
|
|
+ logger.info(f"🧹 Marking pending SL for {token} as stale (no position/limit orders)")
|
|
|
|
+
|
|
|
|
+ # Clean up stale pending SL orders
|
|
|
|
+ for lifecycle_id in stale_lifecycles:
|
|
|
|
+ stats.cancel_pending_stop_loss(lifecycle_id)
|
|
|
|
+ logger.info(f"✅ Cleaned up stale pending SL: {lifecycle_id}")
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error cleaning up stale pending SL orders: {e}")
|
|
|
|
|
|
- # Format orders text
|
|
|
|
- orders_text = "📋 <b>Open & Pending Orders</b>\n\n"
|
|
|
|
|
|
+ def _categorize_exchange_orders(self, exchange_orders: List[Dict]) -> Dict[str, List[Dict]]:
|
|
|
|
+ """Categorize exchange orders by type for better display."""
|
|
|
|
+ categories = {
|
|
|
|
+ 'stop_loss': [], # Stop loss orders (reduce-only with trigger)
|
|
|
|
+ 'take_profit': [], # Take profit orders
|
|
|
|
+ 'limit_orders': [], # Regular limit orders
|
|
|
|
+ 'market_orders': [], # Market orders
|
|
|
|
+ 'other_exit': [], # Other reduce-only orders
|
|
|
|
+ 'zero_size': [] # Zero-size position placeholders
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ for order in exchange_orders:
|
|
|
|
+ amount = float(order.get('amount', 0))
|
|
|
|
+ order_type = order.get('type', '').lower()
|
|
|
|
+ is_reduce_only = order.get('reduceOnly', False) or order.get('info', {}).get('reduceOnly', False)
|
|
|
|
+ order_info = order.get('info', {})
|
|
|
|
+ has_trigger = 'triggerPrice' in order_info or order_info.get('isTrigger', False)
|
|
|
|
+ tpsl_type = order_info.get('tpsl') # 'tp' or 'sl'
|
|
|
|
+ is_position_tpsl = order_info.get('isPositionTpsl', False)
|
|
|
|
+
|
|
|
|
+ if amount == 0:
|
|
|
|
+ categories['zero_size'].append(order)
|
|
|
|
+ elif is_reduce_only and has_trigger and (tpsl_type == 'sl' or is_position_tpsl):
|
|
|
|
+ categories['stop_loss'].append(order)
|
|
|
|
+ elif is_reduce_only and has_trigger and tpsl_type == 'tp':
|
|
|
|
+ categories['take_profit'].append(order)
|
|
|
|
+ elif is_reduce_only:
|
|
|
|
+ categories['other_exit'].append(order)
|
|
|
|
+ elif order_type == 'limit':
|
|
|
|
+ categories['limit_orders'].append(order)
|
|
|
|
+ elif order_type == 'market':
|
|
|
|
+ categories['market_orders'].append(order)
|
|
|
|
+ else:
|
|
|
|
+ categories['other_exit'].append(order) # Fallback
|
|
|
|
+
|
|
|
|
+ return categories
|
|
|
|
|
|
- for order in all_orders:
|
|
|
|
- try:
|
|
|
|
- is_pending_sl = 'trade_lifecycle_id' in order and order.get('stop_loss_price') is not None
|
|
|
|
-
|
|
|
|
- symbol = order.get('symbol', 'unknown')
|
|
|
|
- base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
|
-
|
|
|
|
- if is_pending_sl:
|
|
|
|
- # This is a pending SL from a trade lifecycle
|
|
|
|
- entry_side = order.get('side', 'unknown').upper()
|
|
|
|
- order_type = "STOP (PENDING)"
|
|
|
|
- side = "SELL" if entry_side == "BUY" else "BUY"
|
|
|
|
- price = float(order.get('stop_loss_price') or 0)
|
|
|
|
- amount = float(order.get('amount') or 0) # Amount from the entry order
|
|
|
|
- status = f"Awaiting {order.get('status', '').upper()} Entry" # e.g. Awaiting PENDING Entry
|
|
|
|
- order_id = order.get('trade_lifecycle_id', 'unknown')
|
|
|
|
- else:
|
|
|
|
- # This is a regular database order
|
|
|
|
- order_type = order.get('type', 'unknown').upper()
|
|
|
|
- side = order.get('side', 'unknown').upper()
|
|
|
|
- price = float(order.get('price') or 0)
|
|
|
|
- amount_requested = float(order.get('amount_requested') or 0)
|
|
|
|
- amount_filled = float(order.get('amount_filled') or 0)
|
|
|
|
- amount = amount_requested - amount_filled # Show remaining amount
|
|
|
|
- status = order.get('status', 'unknown').upper()
|
|
|
|
- order_id = order.get('exchange_order_id') or order.get('bot_order_ref_id', 'unknown')
|
|
|
|
|
|
+ async def _format_exchange_first_orders_message(self, exchange_orders: List[Dict], pending_sl_orders: List[Dict]) -> str:
|
|
|
|
+ """Format orders message with exchange orders as primary."""
|
|
|
|
+
|
|
|
|
+ formatter = self._get_formatter()
|
|
|
|
+ message_parts = []
|
|
|
|
+
|
|
|
|
+ # Header
|
|
|
|
+ total_exchange = len(exchange_orders)
|
|
|
|
+ total_pending = len(pending_sl_orders)
|
|
|
|
+ message_parts.append(f"📋 <b>All Orders ({total_exchange + total_pending} total)</b>\n")
|
|
|
|
+
|
|
|
|
+ if exchange_orders:
|
|
|
|
+ # Categorize exchange orders
|
|
|
|
+ categories = self._categorize_exchange_orders(exchange_orders)
|
|
|
|
+
|
|
|
|
+ # Regular Limit Orders
|
|
|
|
+ if categories['limit_orders']:
|
|
|
|
+ message_parts.append("📈 <b>Limit Orders:</b>")
|
|
|
|
+ for order in categories['limit_orders']:
|
|
|
|
+ order_line = await self._format_exchange_order(order, formatter, show_type=False)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Market Orders
|
|
|
|
+ if categories['market_orders']:
|
|
|
|
+ message_parts.append("⚡ <b>Market Orders:</b>")
|
|
|
|
+ for order in categories['market_orders']:
|
|
|
|
+ order_line = await self._format_exchange_order(order, formatter, show_type=False)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Stop Loss Orders
|
|
|
|
+ if categories['stop_loss']:
|
|
|
|
+ message_parts.append("🛑 <b>Stop Loss Orders:</b>")
|
|
|
|
+ for order in categories['stop_loss']:
|
|
|
|
+ order_line = await self._format_stop_loss_order(order, formatter)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Take Profit Orders
|
|
|
|
+ if categories['take_profit']:
|
|
|
|
+ message_parts.append("🎯 <b>Take Profit Orders:</b>")
|
|
|
|
+ for order in categories['take_profit']:
|
|
|
|
+ order_line = await self._format_take_profit_order(order, formatter)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Other Exit Orders
|
|
|
|
+ if categories['other_exit']:
|
|
|
|
+ message_parts.append("🔄 <b>Other Exit Orders:</b>")
|
|
|
|
+ for order in categories['other_exit']:
|
|
|
|
+ order_line = await self._format_exchange_order(order, formatter, show_type=True)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Zero-size orders (position placeholders)
|
|
|
|
+ if categories['zero_size']:
|
|
|
|
+ message_parts.append("⚪ <b>Position Placeholders (Zero Size):</b>")
|
|
|
|
+ for order in categories['zero_size']:
|
|
|
|
+ order_line = await self._format_zero_size_order(order, formatter)
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Pending Stop Loss Orders (bot-internal only)
|
|
|
|
+ if pending_sl_orders:
|
|
|
|
+ message_parts.append("⏳ <b>Pending Stop Loss Orders (Bot Internal):</b>")
|
|
|
|
+ message_parts.append(" <i>Will be placed when entry orders fill</i>")
|
|
|
|
+ for order in pending_sl_orders:
|
|
|
|
+ order_line = await self._format_pending_sl_order(order, formatter)
|
|
|
|
+ if order_line:
|
|
|
|
+ message_parts.append(order_line)
|
|
|
|
+ message_parts.append("")
|
|
|
|
+
|
|
|
|
+ # Footer
|
|
|
|
+ from src.config.config import Config
|
|
|
|
+ message_parts.append("💡 <b>About Orders:</b>")
|
|
|
|
+ message_parts.append("• All orders above are live on the exchange")
|
|
|
|
+ message_parts.append("• Pending SL orders are bot-internal (not yet on exchange)")
|
|
|
|
+ message_parts.append("• Use /coo [token] to cancel all orders for a token")
|
|
|
|
+ message_parts.append(f"• Data refreshed every {Config.BOT_HEARTBEAT_SECONDS}s")
|
|
|
|
+
|
|
|
|
+ return "\n".join(message_parts)
|
|
|
|
|
|
- # Skip fully filled orders
|
|
|
|
- if amount <= 0 and not is_pending_sl:
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- # Format order details
|
|
|
|
- formatter = self._get_formatter()
|
|
|
|
- price_str = await formatter.format_price_with_symbol(price, base_asset)
|
|
|
|
- amount_str = await formatter.format_amount(amount, base_asset) if amount > 0 else "N/A"
|
|
|
|
|
|
+ async def _format_exchange_order(self, order: Dict, formatter, show_type: bool = True) -> str:
|
|
|
|
+ """Format a regular exchange order."""
|
|
|
|
+ try:
|
|
|
|
+ symbol = order.get('symbol', 'unknown')
|
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
|
+ side = order.get('side', '').upper()
|
|
|
|
+ amount = float(order.get('amount', 0))
|
|
|
|
+ price = float(order.get('price', 0)) if order.get('price') else 0
|
|
|
|
+ order_type = order.get('type', '').upper()
|
|
|
|
+
|
|
|
|
+ price_str = await formatter.format_price_with_symbol(price, base_asset) if price else "MARKET"
|
|
|
|
+ amount_str = await formatter.format_amount(amount, base_asset)
|
|
|
|
+
|
|
|
|
+ side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
|
+
|
|
|
|
+ if show_type:
|
|
|
|
+ return f" {side_emoji} {base_asset} {side} {order_type} - {amount_str} @ {price_str}"
|
|
|
|
+ else:
|
|
|
|
+ return f" {side_emoji} {base_asset} {side} - {amount_str} @ {price_str}"
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error formatting exchange order: {e}")
|
|
|
|
+ return f" ❌ Error formatting order: {order.get('symbol', 'unknown')}"
|
|
|
|
|
|
- # Order header
|
|
|
|
- side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
|
- orders_text += f"{side_emoji} <b>{base_asset} {side} {order_type}</b>\n"
|
|
|
|
- orders_text += f" Status: {status.replace('_', ' ')}\n"
|
|
|
|
- orders_text += f" 📏 Amount: {amount_str}\n"
|
|
|
|
- orders_text += f" 💰 {'Trigger Price' if 'STOP' in order_type else 'Price'}: {price_str}\n"
|
|
|
|
-
|
|
|
|
- # Add order ID
|
|
|
|
- id_label = "Lifecycle ID" if is_pending_sl else "Order ID"
|
|
|
|
- orders_text += f" 🆔 {id_label}: {str(order_id)[:12]}\n\n"
|
|
|
|
|
|
+ async def _format_stop_loss_order(self, order: Dict, formatter) -> str:
|
|
|
|
+ """Format a stop loss order with trigger information."""
|
|
|
|
+ try:
|
|
|
|
+ symbol = order.get('symbol', 'unknown')
|
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
|
+ side = order.get('side', '').upper()
|
|
|
|
+
|
|
|
|
+ # Get trigger price and amount info
|
|
|
|
+ order_info = order.get('info', {})
|
|
|
|
+ trigger_price = order_info.get('triggerPx', order_info.get('triggerPrice'))
|
|
|
|
+ trigger_condition = order_info.get('triggerCondition', '')
|
|
|
|
+ amount = float(order.get('amount', 0))
|
|
|
|
+
|
|
|
|
+ trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
|
|
|
|
+
|
|
|
|
+ # For zero-size position stop losses
|
|
|
|
+ if amount == 0:
|
|
|
|
+ amount_str = "Full Position"
|
|
|
|
+ else:
|
|
|
|
+ amount_str = await formatter.format_amount(amount, base_asset)
|
|
|
|
+
|
|
|
|
+ side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
|
+
|
|
|
|
+ # Add trigger condition if available
|
|
|
|
+ condition_text = f" ({trigger_condition})" if trigger_condition and trigger_condition != "N/A" else ""
|
|
|
|
+
|
|
|
|
+ return f" {side_emoji} {base_asset} {side} {amount_str} - Trigger @ {trigger_price_str}{condition_text}"
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error formatting stop loss order: {e}")
|
|
|
|
+ return f" ❌ Error formatting SL order: {order.get('symbol', 'unknown')}"
|
|
|
|
|
|
- except Exception as e:
|
|
|
|
- logger.error(f"Error processing order {order.get('symbol', 'unknown')}: {e}", exc_info=True)
|
|
|
|
- continue
|
|
|
|
-
|
|
|
|
- # Add footer
|
|
|
|
- orders_text += "💡 Pending SL orders are activated when the entry order fills.\n"
|
|
|
|
- from src.config.config import Config
|
|
|
|
- orders_text += f"🔄 Data updated every {Config.BOT_HEARTBEAT_SECONDS}s via monitoring system"
|
|
|
|
|
|
+ async def _format_take_profit_order(self, order: Dict, formatter) -> str:
|
|
|
|
+ """Format a take profit order."""
|
|
|
|
+ try:
|
|
|
|
+ symbol = order.get('symbol', 'unknown')
|
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
|
+ side = order.get('side', '').upper()
|
|
|
|
+ amount = float(order.get('amount', 0))
|
|
|
|
+
|
|
|
|
+ order_info = order.get('info', {})
|
|
|
|
+ trigger_price = order_info.get('triggerPx', order_info.get('triggerPrice'))
|
|
|
|
+ trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
|
|
|
|
+ amount_str = await formatter.format_amount(amount, base_asset)
|
|
|
|
+
|
|
|
|
+ side_emoji = "🟢" if side == "BUY" else "🔴"
|
|
|
|
+
|
|
|
|
+ return f" {side_emoji} {base_asset} {side} {amount_str} - Target @ {trigger_price_str}"
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error formatting take profit order: {e}")
|
|
|
|
+ return f" ❌ Error formatting TP order: {order.get('symbol', 'unknown')}"
|
|
|
|
|
|
- await self._reply(update, orders_text.strip())
|
|
|
|
|
|
+ async def _format_zero_size_order(self, order: Dict, formatter) -> str:
|
|
|
|
+ """Format a zero-size order (position placeholder)."""
|
|
|
|
+ try:
|
|
|
|
+ symbol = order.get('symbol', 'unknown')
|
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
|
+ side = order.get('side', '').upper()
|
|
|
|
+ order_info = order.get('info', {})
|
|
|
|
+
|
|
|
|
+ # These are usually stop losses for positions
|
|
|
|
+ if order_info.get('isPositionTpsl', False):
|
|
|
|
+ trigger_price = order_info.get('triggerPx')
|
|
|
|
+ trigger_condition = order_info.get('triggerCondition', '')
|
|
|
|
+ trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
|
|
|
|
+
|
|
|
|
+ side_emoji = "🛑" if side == "BUY" else "🛑" # Red for stop loss
|
|
|
|
+ return f" {side_emoji} {base_asset} Position Stop Loss - Trigger @ {trigger_price_str}"
|
|
|
|
+ else:
|
|
|
|
+ side_emoji = "⚪"
|
|
|
|
+ return f" {side_emoji} {base_asset} {side} - Zero size placeholder"
|
|
|
|
+
|
|
|
|
+ except Exception as e:
|
|
|
|
+ logger.error(f"Error formatting zero size order: {e}")
|
|
|
|
+ return f" ❌ Error formatting zero-size order: {order.get('symbol', 'unknown')}"
|
|
|
|
|
|
|
|
+ async def _format_pending_sl_order(self, order: Dict, formatter) -> Optional[str]:
|
|
|
|
+ """Format a pending stop loss order (bot-internal)."""
|
|
|
|
+ try:
|
|
|
|
+ symbol = order.get('symbol', 'unknown')
|
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
|
+
|
|
|
|
+ entry_side = order.get('side', 'unknown').upper()
|
|
|
|
+ exit_side = "SELL" if entry_side == "BUY" else "BUY"
|
|
|
|
+ price = float(order.get('stop_loss_price') or 0)
|
|
|
|
+ amount = float(order.get('amount') or 0)
|
|
|
|
+ status = order.get('status', '').upper()
|
|
|
|
+
|
|
|
|
+ price_str = await formatter.format_price_with_symbol(price, base_asset)
|
|
|
|
+ amount_str = await formatter.format_amount(amount, base_asset)
|
|
|
|
+
|
|
|
|
+ side_emoji = "⏳"
|
|
|
|
+ return f" {side_emoji} {base_asset} {exit_side} {amount_str} @ {price_str} (Entry: {status})"
|
|
|
|
+
|
|
except Exception as e:
|
|
except Exception as e:
|
|
- logger.error(f"Error in orders command: {e}", exc_info=True)
|
|
|
|
- await self._reply(update, "❌ Error retrieving order information.")
|
|
|
|
|
|
+ logger.error(f"Error formatting pending SL order: {e}")
|
|
|
|
+ return None
|