|
@@ -142,49 +142,48 @@ class InfoCommands:
|
|
|
|
|
|
for position_trade in open_positions:
|
|
|
symbol = position_trade['symbol']
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ # base_asset is the asset being traded, quote_asset is the settlement currency (usually USDC)
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ # quote_asset = symbol.split('/')[1] if '/' in symbol else "USDC" # Not strictly needed for display here
|
|
|
+
|
|
|
position_side = position_trade['position_side'] # 'long' or 'short'
|
|
|
entry_price = position_trade['entry_price']
|
|
|
- current_amount = position_trade['current_position_size']
|
|
|
- trade_type = position_trade.get('trade_type', 'manual')
|
|
|
-
|
|
|
- # 🆕 Data now comes directly from position_trade (DB)
|
|
|
- # Fields like unrealized_pnl, mark_price, etc., are expected to be in position_trade
|
|
|
- # or calculated based on DB data.
|
|
|
-
|
|
|
- # Attempt to get live data from position_trade, otherwise use defaults or calculate.
|
|
|
- # It's assumed the database record (position_trade) is updated by the heartbeat
|
|
|
- # and contains the necessary information like PnL, mark price, etc.
|
|
|
+ current_amount = position_trade['current_position_size'] # This is the size of the position
|
|
|
+ abs_current_amount = abs(current_amount)
|
|
|
+ trade_type = position_trade.get('trade_type', 'manual') # Default to manual if not specified
|
|
|
|
|
|
mark_price = position_trade.get('mark_price', entry_price) # Default to entry if not available
|
|
|
|
|
|
- # Calculate unrealized PnL if not directly available or needs recalculation with current mark_price
|
|
|
- if 'unrealized_pnl' in position_trade:
|
|
|
- unrealized_pnl = position_trade['unrealized_pnl']
|
|
|
- else:
|
|
|
+ # Calculate unrealized PnL
|
|
|
+ unrealized_pnl = position_trade.get('unrealized_pnl') # Prefer DB value if up-to-date
|
|
|
+ if unrealized_pnl is None: # Calculate if not directly available from DB
|
|
|
if position_side == 'long':
|
|
|
- unrealized_pnl = current_amount * (mark_price - entry_price)
|
|
|
+ unrealized_pnl = (mark_price - entry_price) * abs_current_amount
|
|
|
else: # Short position
|
|
|
- unrealized_pnl = current_amount * (entry_price - mark_price)
|
|
|
+ unrealized_pnl = (entry_price - mark_price) * abs_current_amount
|
|
|
+ unrealized_pnl = unrealized_pnl or 0.0 # Ensure it's not None for calculations
|
|
|
|
|
|
- position_value = position_trade.get('position_value')
|
|
|
- if position_value is None: # Calculate if not in DB
|
|
|
- position_value = abs(current_amount) * mark_price
|
|
|
+ # Tiered P&L Percentage Calculation
|
|
|
+ pnl_percentage = 0.0
|
|
|
+ exchange_pnl_percentage = position_trade.get('unrealized_pnl_percentage') # From exchange, e.g., 50.5 for 50.5%
|
|
|
+ margin_used = position_trade.get('margin_used')
|
|
|
|
|
|
- liquidation_price = position_trade.get('liquidation_price') # Optional, might not be in DB
|
|
|
- margin_used = position_trade.get('margin_used') # Optional
|
|
|
- leverage = position_trade.get('leverage') # Optional
|
|
|
-
|
|
|
- pnl_percentage = position_trade.get('pnl_percentage')
|
|
|
- if pnl_percentage is None and position_value and position_value > 0 : # Calculate if not in DB
|
|
|
- pnl_percentage = (unrealized_pnl / position_value * 100)
|
|
|
- elif pnl_percentage is None:
|
|
|
- pnl_percentage = 0
|
|
|
+ if exchange_pnl_percentage is not None:
|
|
|
+ pnl_percentage = exchange_pnl_percentage
|
|
|
+ elif margin_used is not None and margin_used > 0 and unrealized_pnl != 0:
|
|
|
+ pnl_percentage = (unrealized_pnl / margin_used) * 100
|
|
|
+ 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
|
|
|
+ # else pnl_percentage remains 0.0
|
|
|
|
|
|
- total_position_value += position_value if position_value else 0
|
|
|
- total_unrealized += unrealized_pnl if unrealized_pnl else 0
|
|
|
+ # Add to totals
|
|
|
+ current_pos_value_at_mark = abs_current_amount * mark_price
|
|
|
+ total_position_value += current_pos_value_at_mark
|
|
|
+ total_unrealized += unrealized_pnl
|
|
|
|
|
|
- # Position emoji and formatting
|
|
|
+ # --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
|
+ pos_emoji = ""
|
|
|
direction_text = ""
|
|
|
if position_side == 'long':
|
|
|
pos_emoji = "🟢"
|
|
@@ -193,55 +192,62 @@ class InfoCommands:
|
|
|
pos_emoji = "🔴"
|
|
|
direction_text = "SHORT"
|
|
|
|
|
|
- # 🆕 Incorporate leverage into direction text if available
|
|
|
+ leverage = position_trade.get('leverage')
|
|
|
if leverage is not None:
|
|
|
try:
|
|
|
leverage_val = float(leverage)
|
|
|
- # Format leverage: x5, x10.5, etc.
|
|
|
leverage_str = f"x{leverage_val:.1f}".rstrip('0').rstrip('.') if '.' in f"{leverage_val:.1f}" else f"x{int(leverage_val)}"
|
|
|
direction_text = f"{direction_text} {leverage_str}"
|
|
|
except ValueError:
|
|
|
logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
|
|
|
+
|
|
|
+ # --- Format Output String ---
|
|
|
+ # Get token info for formatting prices
|
|
|
+ # Assuming get_formatter() is available and provides necessary precision
|
|
|
+ # For direct use, we can fetch token_info if get_formatter() isn't what we expect
|
|
|
+ token_info = self.trading_engine.get_token_info(base_asset) # Ensure this method exists and works
|
|
|
+ base_precision = token_info.get('precision', {}).get('amount', 6) if token_info and token_info.get('precision') else 6 # Default amount precision
|
|
|
+ quote_precision = token_info.get('precision', {}).get('price', 2) if token_info and token_info.get('precision') else 2 # Default price precision
|
|
|
+
|
|
|
+ formatter = get_formatter() # Keep using if it wraps these precisions
|
|
|
+ entry_price_str = formatter.format_price_with_symbol(entry_price, base_asset, quote_precision)
|
|
|
+ mark_price_str = formatter.format_price_with_symbol(mark_price, base_asset, quote_precision)
|
|
|
|
|
|
- pnl_emoji = "🟢" if unrealized_pnl >= 0 else "��"
|
|
|
-
|
|
|
- # Format prices with proper precision for this token
|
|
|
- formatter = get_formatter()
|
|
|
- entry_price_str = formatter.format_price_with_symbol(entry_price, token)
|
|
|
- mark_price_str = formatter.format_price_with_symbol(mark_price, token)
|
|
|
-
|
|
|
- # Trade type indicator
|
|
|
type_indicator = ""
|
|
|
- if trade_type == 'external':
|
|
|
- type_indicator = " 🔄" # External/synced position
|
|
|
- elif trade_type == 'bot':
|
|
|
- type_indicator = " 🤖" # Bot-created position
|
|
|
+ # Determine type_indicator based on trade_lifecycle_id or trade_type
|
|
|
+ if position_trade.get('trade_lifecycle_id'): # Primary indicator for bot managed
|
|
|
+ type_indicator = " 🤖"
|
|
|
+ elif trade_type == 'external':
|
|
|
+ type_indicator = " 🔄"
|
|
|
|
|
|
- positions_text += f"{pos_emoji} <b>{token} ({direction_text}){type_indicator}</b>\n"
|
|
|
- positions_text += f" 📏 Size: {abs(current_amount):.6f} {token}\n"
|
|
|
+ positions_text += f"{pos_emoji} <b>{base_asset} ({direction_text}){type_indicator}</b>\n"
|
|
|
+ positions_text += f" 📏 Size: {abs_current_amount:.{base_precision}f} {base_asset}\n"
|
|
|
positions_text += f" 💰 Entry: {entry_price_str}\n"
|
|
|
- positions_text += f" 📊 Mark: {mark_price_str}\n"
|
|
|
- positions_text += f" 💵 Value: ${position_value:,.2f}\n"
|
|
|
- positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
+
|
|
|
+ if mark_price != 0 and abs(mark_price - entry_price) > 1e-9: # Only show mark if significantly different
|
|
|
+ positions_text += f" 📈 Mark: {mark_price_str}\n"
|
|
|
+
|
|
|
+ pnl_line_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
+ 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:
|
|
|
positions_text += f" 💳 Margin Used: ${margin_used:,.2f}\n"
|
|
|
- if liquidation_price is not None and liquidation_price > 0:
|
|
|
- liq_price_str = formatter.format_price_with_symbol(liquidation_price, token)
|
|
|
+ if position_trade.get('liquidation_price') is not None and position_trade.get('liquidation_price') > 0:
|
|
|
+ liq_price_str = formatter.format_price_with_symbol(position_trade.get('liquidation_price'), base_asset, quote_precision)
|
|
|
positions_text += f" ⚠️ Liquidation: {liq_price_str}\n"
|
|
|
|
|
|
# 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, token)} ({sl_status})\n"
|
|
|
+ positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)} ({sl_status})\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, token)} ({tp_status})\n"
|
|
|
+ positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)} ({tp_status})\n"
|
|
|
|
|
|
positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
|
|
|
|
|
@@ -320,7 +326,7 @@ class InfoCommands:
|
|
|
|
|
|
orders_text += "\n"
|
|
|
|
|
|
- orders_text += f"💼 <b>Total Orders:</b> {len(orders)}\n"
|
|
|
+ orders_text += f"�� <b>Total Orders:</b> {len(orders)}\n"
|
|
|
orders_text += f"💡 Use /coo [token] to cancel orders"
|
|
|
|
|
|
else:
|