|
@@ -167,6 +167,10 @@ class InfoCommands:
|
|
|
total_unrealized = 0
|
|
|
total_position_value = 0
|
|
|
|
|
|
+ # Fetch all exchange orders once to use throughout the command if needed by other parts
|
|
|
+ # For this specific change, we'll use it inside the loop, but good practice to fetch once.
|
|
|
+ # self._cached_all_exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
for position_trade in open_positions:
|
|
|
symbol = position_trade['symbol']
|
|
|
# base_asset is the asset being traded, quote_asset is the settlement currency (usually USDC)
|
|
@@ -179,6 +183,33 @@ class InfoCommands:
|
|
|
abs_current_amount = abs(current_amount)
|
|
|
trade_type = position_trade.get('trade_type', 'manual') # Default to manual if not specified
|
|
|
|
|
|
+ # Calculate position duration
|
|
|
+ position_opened_at_str = position_trade.get('position_opened_at')
|
|
|
+ duration_str = "N/A"
|
|
|
+ if position_opened_at_str:
|
|
|
+ try:
|
|
|
+ opened_at_dt = datetime.fromisoformat(position_opened_at_str)
|
|
|
+ if opened_at_dt.tzinfo is None: # Ensure timezone aware
|
|
|
+ opened_at_dt = opened_at_dt.replace(tzinfo=timezone.utc)
|
|
|
+ now_utc = datetime.now(timezone.utc)
|
|
|
+ duration = now_utc - opened_at_dt
|
|
|
+
|
|
|
+ days = duration.days
|
|
|
+ hours, remainder = divmod(duration.seconds, 3600)
|
|
|
+ minutes, _ = divmod(remainder, 60)
|
|
|
+
|
|
|
+ parts = []
|
|
|
+ if days > 0:
|
|
|
+ parts.append(f"{days}d")
|
|
|
+ if hours > 0:
|
|
|
+ parts.append(f"{hours}h")
|
|
|
+ if minutes > 0 or (days == 0 and hours == 0): # Show minutes if primary unit or others are zero
|
|
|
+ parts.append(f"{minutes}m")
|
|
|
+ duration_str = " ".join(parts) if parts else "0m"
|
|
|
+ except ValueError:
|
|
|
+ logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
|
|
|
+ duration_str = "Error"
|
|
|
+
|
|
|
mark_price = position_trade.get('mark_price', entry_price) # Default to entry if not available
|
|
|
|
|
|
# Calculate unrealized PnL
|
|
@@ -256,6 +287,7 @@ class InfoCommands:
|
|
|
positions_text += f"{pos_emoji} <b>{base_asset} ({direction_text}){type_indicator}</b>\n"
|
|
|
positions_text += f" 📏 Size: {size_str} {base_asset}\n" # Use the formatted size_str
|
|
|
positions_text += f" 💰 Entry: {entry_price_str}\n"
|
|
|
+ positions_text += f" ⏳ Duration: {duration_str}\n" # Display position duration
|
|
|
|
|
|
# Display individual position value
|
|
|
positions_text += f" 🏦 Value: ${individual_position_value:,.2f}\n"
|
|
@@ -285,6 +317,61 @@ class InfoCommands:
|
|
|
tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
|
|
|
positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)} ({tp_status})\n"
|
|
|
|
|
|
+ # --- NEW: Display potential unlinked SL/TP orders from exchange ---
|
|
|
+ # Fetch all open orders from the exchange once per /positions call if not already done
|
|
|
+ # For this example, assuming self.all_exchange_orders is populated at the start of positions_command
|
|
|
+ # If not, it should be: all_exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
+ # Ensure all_exchange_orders is fetched (ideally once at the start of the command)
|
|
|
+ # For this isolated edit, let's assume it's passed or fetched.
|
|
|
+ # A better place would be at the start of the `positions_command` method.
|
|
|
+ # e.g., all_exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
+ # The following line is a placeholder for where all_exchange_orders would be fetched
|
|
|
+ # This should be done *once* at the beginning of the `positions_command` method.
|
|
|
+ # all_exchange_orders = getattr(self, '_cached_all_exchange_orders', []) # Example of how it might be cached
|
|
|
+
|
|
|
+ # To make this edit apply, we'll add a temporary fetch here.
|
|
|
+ # THIS IS INEFFICIENT and should be done ONCE at the start of the command.
|
|
|
+ temp_all_exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
+
|
|
|
+ if temp_all_exchange_orders: # Check if fetching was successful
|
|
|
+ unlinked_sl_display = []
|
|
|
+ unlinked_tp_display = []
|
|
|
+
|
|
|
+ # Using entry_price of the current bot position for comparison.
|
|
|
+ # Could also use current_market_price if fetched, but entry_price is simpler here.
|
|
|
+ # current_market_price_for_pos = mark_price # mark_price is already available
|
|
|
+
|
|
|
+ for ex_order in temp_all_exchange_orders:
|
|
|
+ if ex_order.get('symbol') == symbol: # Match symbol
|
|
|
+ ex_order_id = ex_order.get('id')
|
|
|
+ # Skip if this order is already formally linked as SL or TP to the current position_trade
|
|
|
+ if ex_order_id == position_trade.get('stop_loss_order_id') or \
|
|
|
+ ex_order_id == position_trade.get('take_profit_order_id'):
|
|
|
+ continue
|
|
|
+
|
|
|
+ order_price = float(ex_order.get('price', 0))
|
|
|
+ order_side = ex_order.get('side', '').lower()
|
|
|
+
|
|
|
+ if position_side == 'long' and order_side == 'sell':
|
|
|
+ if order_price < entry_price: # Potential SL
|
|
|
+ unlinked_sl_display.append(f"SELL @ {formatter.format_price_with_symbol(order_price, base_asset)}")
|
|
|
+ elif order_price > entry_price: # Potential TP
|
|
|
+ unlinked_tp_display.append(f"SELL @ {formatter.format_price_with_symbol(order_price, base_asset)}")
|
|
|
+ elif position_side == 'short' and order_side == 'buy':
|
|
|
+ if order_price > entry_price: # Potential SL
|
|
|
+ unlinked_sl_display.append(f"BUY @ {formatter.format_price_with_symbol(order_price, base_asset)}")
|
|
|
+ elif order_price < entry_price: # Potential TP
|
|
|
+ unlinked_tp_display.append(f"BUY @ {formatter.format_price_with_symbol(order_price, base_asset)}")
|
|
|
+
|
|
|
+ if unlinked_sl_display:
|
|
|
+ positions_text += f" ⚠️ Unlinked SL(s)?: {', '.join(unlinked_sl_display)}\n"
|
|
|
+ if unlinked_tp_display:
|
|
|
+ positions_text += f" 🎯 Unlinked TP(s)?: {', '.join(unlinked_tp_display)}\n"
|
|
|
+ # --- END: Display potential unlinked SL/TP orders ---
|
|
|
+
|
|
|
positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
|
|
|
|
|
|
# Portfolio summary
|
|
@@ -310,6 +397,10 @@ class InfoCommands:
|
|
|
if not self._is_authorized(update):
|
|
|
return
|
|
|
|
|
|
+ # Fetch all exchange orders once to use throughout the command if needed by other parts
|
|
|
+ # For this specific change, we'll use it inside the loop, but good practice to fetch once.
|
|
|
+ # self._cached_all_exchange_orders = self.trading_engine.get_orders() or []
|
|
|
+
|
|
|
reply_method = None
|
|
|
if update.callback_query:
|
|
|
reply_method = update.callback_query.message.reply_text
|