|
@@ -155,29 +155,17 @@ class InfoCommands:
|
|
|
await reply_method(text="❌ Trading statistics not available.", parse_mode='HTML')
|
|
|
return
|
|
|
|
|
|
- # 🆕 AUTO-SYNC logic removed as per user request.
|
|
|
- # Assuming heartbeat updates the DB sufficiently.
|
|
|
- sync_msg = ""
|
|
|
-
|
|
|
# Get open positions from unified trades table
|
|
|
open_positions = stats.get_open_positions()
|
|
|
|
|
|
# Add position count to header
|
|
|
position_count = len(open_positions) if open_positions else 0
|
|
|
- positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n{sync_msg}" # sync_msg will be empty
|
|
|
+ positions_text = f"📈 <b>Open Positions ({position_count})</b>\n\n"
|
|
|
|
|
|
if open_positions:
|
|
|
total_unrealized = 0
|
|
|
total_position_value = 0
|
|
|
- total_margin_used = 0
|
|
|
- total_equity_used = 0 # For ROE calculation
|
|
|
-
|
|
|
- # Fetch exchange data once for the entire command
|
|
|
- exchange_positions_data = self.trading_engine.get_positions() or []
|
|
|
-
|
|
|
- # 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 []
|
|
|
+ total_margin_used = 0
|
|
|
|
|
|
for position_trade in open_positions:
|
|
|
symbol = position_trade['symbol']
|
|
@@ -229,56 +217,18 @@ class InfoCommands:
|
|
|
unrealized_pnl = (entry_price - mark_price) * abs_current_amount
|
|
|
unrealized_pnl = unrealized_pnl or 0.0 # Ensure it's not None for calculations
|
|
|
|
|
|
- # Tiered P&L Percentage Calculation (prioritize exchange percentage)
|
|
|
+ # P&L Percentage Calculation (use database data)
|
|
|
pnl_percentage = 0.0
|
|
|
- exchange_pnl_percentage = position_trade.get('unrealized_pnl_percentage') # From exchange, e.g., 50.5 for 50.5%
|
|
|
+ db_pnl_percentage = position_trade.get('unrealized_pnl_percentage')
|
|
|
margin_used = position_trade.get('margin_used')
|
|
|
|
|
|
- if exchange_pnl_percentage is not None:
|
|
|
- pnl_percentage = exchange_pnl_percentage
|
|
|
- logger.debug(f"Using exchange percentage for {symbol}: {exchange_pnl_percentage}%")
|
|
|
+ if db_pnl_percentage is not None:
|
|
|
+ pnl_percentage = db_pnl_percentage
|
|
|
elif margin_used is not None and margin_used > 0 and unrealized_pnl != 0:
|
|
|
pnl_percentage = (unrealized_pnl / margin_used) * 100
|
|
|
- logger.debug(f"Using margin-based calculation for {symbol}: {pnl_percentage}%")
|
|
|
elif entry_price != 0 and abs_current_amount != 0 and unrealized_pnl != 0:
|
|
|
initial_value = entry_price * abs_current_amount
|
|
|
pnl_percentage = (unrealized_pnl / initial_value) * 100
|
|
|
- logger.debug(f"Using position value calculation for {symbol}: {pnl_percentage}%")
|
|
|
- # else pnl_percentage remains 0.0
|
|
|
-
|
|
|
- # Get ROE (Return on Equity) and margin info from exchange data
|
|
|
- roe_percentage = None
|
|
|
- live_margin_used = None
|
|
|
- # Use the exchange data we fetched once at the beginning
|
|
|
- if exchange_positions_data:
|
|
|
- for pos_data in exchange_positions_data:
|
|
|
- if pos_data.get('symbol') == symbol:
|
|
|
- # Get margin from CCXT data
|
|
|
- live_margin_used = pos_data.get('initialMargin')
|
|
|
-
|
|
|
- # Get ROE from raw exchange info
|
|
|
- info_data = pos_data.get('info', {})
|
|
|
- position_info = info_data.get('position', {})
|
|
|
- roe_raw = position_info.get('returnOnEquity')
|
|
|
- if roe_raw is not None:
|
|
|
- try:
|
|
|
- roe_percentage = float(roe_raw) * 100 # Convert to percentage
|
|
|
- logger.debug(f"Found ROE for {symbol}: {roe_percentage}%")
|
|
|
- except (ValueError, TypeError):
|
|
|
- logger.warning(f"Could not parse ROE value: {roe_raw} for {symbol}")
|
|
|
-
|
|
|
- # Also try to get margin from raw exchange info if CCXT doesn't have it
|
|
|
- if live_margin_used is None:
|
|
|
- live_margin_used = position_info.get('marginUsed')
|
|
|
- if live_margin_used is not None:
|
|
|
- try:
|
|
|
- live_margin_used = float(live_margin_used)
|
|
|
- except (ValueError, TypeError):
|
|
|
- live_margin_used = None
|
|
|
- break
|
|
|
-
|
|
|
- # Use live margin data if available, otherwise fall back to database
|
|
|
- margin_used = live_margin_used if live_margin_used is not None else position_trade.get('margin_used')
|
|
|
|
|
|
# Add to totals
|
|
|
individual_position_value = position_trade.get('position_value')
|
|
@@ -292,12 +242,6 @@ class InfoCommands:
|
|
|
if margin_used is not None:
|
|
|
total_margin_used += margin_used
|
|
|
|
|
|
- # Add equity used for ROE calculation
|
|
|
- if roe_percentage is not None and roe_percentage != 0:
|
|
|
- # Calculate equity used from: unrealized_pnl = equity_used * (roe_percentage/100)
|
|
|
- equity_used = abs(unrealized_pnl / (roe_percentage / 100)) if roe_percentage != 0 else 0
|
|
|
- total_equity_used += equity_used
|
|
|
-
|
|
|
# --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
|
pos_emoji = ""
|
|
|
direction_text = ""
|
|
@@ -351,10 +295,7 @@ class InfoCommands:
|
|
|
positions_text += f" 📈 Mark: {mark_price_str}\n"
|
|
|
|
|
|
pnl_line_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
- if roe_percentage is not None:
|
|
|
- positions_text += f" {pnl_line_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}% | ROE: {roe_percentage:+.2f}%)\n"
|
|
|
- else:
|
|
|
- positions_text += f" {pnl_line_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
+ positions_text += f" {pnl_line_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
|
|
|
# Show exchange-provided risk data if available
|
|
|
if margin_used is not None:
|
|
@@ -366,69 +307,14 @@ class InfoCommands:
|
|
|
# Show stop loss if linked
|
|
|
if position_trade.get('stop_loss_price'):
|
|
|
sl_price = position_trade['stop_loss_price']
|
|
|
- sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
|
|
|
- positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)} ({sl_status})\n"
|
|
|
+ positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)}\n"
|
|
|
|
|
|
# Show take profit if linked
|
|
|
if position_trade.get('take_profit_price'):
|
|
|
tp_price = position_trade['take_profit_price']
|
|
|
- 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"
|
|
|
+ positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)}\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"
|
|
|
|
|
@@ -443,14 +329,9 @@ class InfoCommands:
|
|
|
positions_text += f" {portfolio_emoji} Total Unrealized P&L: ${total_unrealized:,.2f}\n"
|
|
|
if total_margin_used > 0:
|
|
|
margin_pnl_percentage = (total_unrealized / total_margin_used) * 100
|
|
|
- positions_text += f" 📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)"
|
|
|
- if total_equity_used > 0:
|
|
|
- equity_pnl_percentage = (total_unrealized / total_equity_used) * 100
|
|
|
- positions_text += f" | ROE: {equity_pnl_percentage:+.2f}%"
|
|
|
- positions_text += "\n"
|
|
|
+ positions_text += f" 📊 Portfolio Return: {margin_pnl_percentage:+.2f}% (on margin)\n"
|
|
|
positions_text += "\n"
|
|
|
positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
|
|
|
- positions_text += f"📊 <b>Percentages:</b> Standard % (margin-based) | ROE % (equity-based)\n"
|
|
|
positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
|
|
|
|
|
|
else:
|
|
@@ -509,6 +390,15 @@ class InfoCommands:
|
|
|
side = order_data.get('side', '').upper()
|
|
|
amount = float(order_data.get('amount', 0))
|
|
|
price = float(order_data.get('price', 0))
|
|
|
+
|
|
|
+ # Check for trigger price (for stop loss orders)
|
|
|
+ trigger_price = order_data.get('info', {}).get('triggerPrice')
|
|
|
+ if trigger_price:
|
|
|
+ try:
|
|
|
+ price = float(trigger_price)
|
|
|
+ except (ValueError, TypeError):
|
|
|
+ pass # Keep original price if trigger price can't be parsed
|
|
|
+
|
|
|
order_type = order_data.get('type', 'unknown').title()
|
|
|
exchange_order_id = order_data.get('id', 'N/A') # Renamed for clarity
|
|
|
|