|
@@ -41,24 +41,29 @@ class PositionsCommands(InfoCommandsBase):
|
|
|
|
|
|
for position_trade in open_positions:
|
|
|
try:
|
|
|
+ # Get position data with defaults
|
|
|
symbol = position_trade['symbol']
|
|
|
- base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
+ base_asset = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
position_side = position_trade.get('position_side', 'unknown')
|
|
|
- abs_current_amount = abs(float(position_trade.get('current_position_size', 0)))
|
|
|
- entry_price = float(position_trade.get('entry_price', 0))
|
|
|
- mark_price = position_trade.get('mark_price', entry_price)
|
|
|
- trade_type = position_trade.get('trade_type', 'unknown')
|
|
|
-
|
|
|
- # Calculate duration
|
|
|
- duration_str = "N/A"
|
|
|
+ entry_price = float(position_trade.get('entry_price', 0.0))
|
|
|
+ current_amount = float(position_trade.get('current_position_size', 0.0))
|
|
|
+ abs_current_amount = abs(current_amount)
|
|
|
+ trade_type = position_trade.get('trade_type', 'manual')
|
|
|
+
|
|
|
+ # Calculate position duration
|
|
|
position_opened_at_str = position_trade.get('position_opened_at')
|
|
|
+ duration_str = "N/A"
|
|
|
if position_opened_at_str:
|
|
|
try:
|
|
|
- position_opened_at = datetime.fromisoformat(position_opened_at_str.replace('Z', '+00:00'))
|
|
|
- duration = datetime.now(timezone.utc) - position_opened_at
|
|
|
+ opened_at_dt = datetime.fromisoformat(position_opened_at_str)
|
|
|
+ if opened_at_dt.tzinfo is None:
|
|
|
+ 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 = duration.seconds // 3600
|
|
|
- minutes = (duration.seconds % 3600) // 60
|
|
|
+ hours, remainder = divmod(duration.seconds, 3600)
|
|
|
+ minutes, _ = divmod(remainder, 60)
|
|
|
|
|
|
parts = []
|
|
|
if days > 0:
|
|
@@ -71,41 +76,32 @@ class PositionsCommands(InfoCommandsBase):
|
|
|
except ValueError:
|
|
|
logger.warning(f"Could not parse position_opened_at: {position_opened_at_str} for {symbol}")
|
|
|
duration_str = "Error"
|
|
|
-
|
|
|
+
|
|
|
+ # Get price data with defaults
|
|
|
+ mark_price = float(position_trade.get('mark_price', entry_price))
|
|
|
+
|
|
|
# Calculate unrealized PnL
|
|
|
- unrealized_pnl = position_trade.get('unrealized_pnl', 0.0)
|
|
|
+ unrealized_pnl = float(position_trade.get('unrealized_pnl', 0.0))
|
|
|
|
|
|
# Get ROE from database
|
|
|
- roe_percentage = position_trade.get('roe_percentage', 0.0)
|
|
|
+ roe_percentage = float(position_trade.get('roe_percentage', 0.0))
|
|
|
|
|
|
# Add to totals
|
|
|
- individual_position_value = position_trade.get('position_value', 0.0)
|
|
|
- if individual_position_value is None or individual_position_value == 0:
|
|
|
+ individual_position_value = float(position_trade.get('position_value', 0.0))
|
|
|
+ if individual_position_value <= 0:
|
|
|
individual_position_value = abs_current_amount * mark_price
|
|
|
|
|
|
total_position_value += individual_position_value
|
|
|
total_unrealized += unrealized_pnl
|
|
|
|
|
|
# Add margin to total
|
|
|
- margin_used = position_trade.get('margin_used', 0.0)
|
|
|
+ margin_used = float(position_trade.get('margin_used', 0.0))
|
|
|
if margin_used > 0:
|
|
|
total_margin_used += margin_used
|
|
|
-
|
|
|
- # Format position details
|
|
|
- formatter = self._get_formatter()
|
|
|
- entry_price_str = formatter.format_price_with_symbol(entry_price, base_asset)
|
|
|
- mark_price_str = formatter.format_price_with_symbol(mark_price, base_asset)
|
|
|
- size_str = formatter.format_amount(abs_current_amount, base_asset)
|
|
|
-
|
|
|
- # Position header
|
|
|
- pos_emoji = ""
|
|
|
- direction_text = ""
|
|
|
- if position_side == 'long':
|
|
|
- pos_emoji = "🟢"
|
|
|
- direction_text = "LONG"
|
|
|
- else: # Short position
|
|
|
- pos_emoji = "🔴"
|
|
|
- direction_text = "SHORT"
|
|
|
+
|
|
|
+ # --- Position Header Formatting (Emoji, Direction, Leverage) ---
|
|
|
+ pos_emoji = "🟢" if position_side == 'long' else "🔴"
|
|
|
+ direction_text = position_side.upper()
|
|
|
|
|
|
leverage = position_trade.get('leverage')
|
|
|
if leverage is not None:
|
|
@@ -116,44 +112,57 @@ class PositionsCommands(InfoCommandsBase):
|
|
|
except ValueError:
|
|
|
logger.warning(f"Could not parse leverage value: {leverage} for {symbol}")
|
|
|
|
|
|
- # Add type indicator
|
|
|
+ # --- Format Output String ---
|
|
|
+ formatter = self._get_formatter()
|
|
|
+
|
|
|
+ # Get price precisions
|
|
|
+ entry_price_str = formatter.format_price_with_symbol(entry_price, base_asset)
|
|
|
+ mark_price_str = formatter.format_price_with_symbol(mark_price, base_asset)
|
|
|
+
|
|
|
+ # Get amount precision for position size
|
|
|
+ size_str = formatter.format_amount(abs_current_amount, base_asset)
|
|
|
+
|
|
|
type_indicator = ""
|
|
|
if position_trade.get('trade_lifecycle_id'):
|
|
|
type_indicator = " 🤖"
|
|
|
elif trade_type == 'external':
|
|
|
type_indicator = " 🔄"
|
|
|
-
|
|
|
- # Build position text
|
|
|
+
|
|
|
positions_text += f"{pos_emoji} <b>{base_asset} ({direction_text}){type_indicator}</b>\n"
|
|
|
positions_text += f" 📏 Size: {size_str} {base_asset}\n"
|
|
|
positions_text += f" 💰 Entry: {entry_price_str}\n"
|
|
|
positions_text += f" ⏳ Duration: {duration_str}\n"
|
|
|
+
|
|
|
+ # Display individual position value
|
|
|
positions_text += f" 🏦 Value: ${individual_position_value:,.2f}\n"
|
|
|
+
|
|
|
if mark_price > 0 and abs(mark_price - entry_price) > 1e-9:
|
|
|
positions_text += f" 📈 Mark: {mark_price_str}\n"
|
|
|
+
|
|
|
pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
positions_text += f" {pnl_emoji} P&L: ${unrealized_pnl:,.2f}\n"
|
|
|
+
|
|
|
+ # Show ROE
|
|
|
roe_emoji = "🟢" if roe_percentage >= 0 else "🔴"
|
|
|
positions_text += f" {roe_emoji} ROE: {roe_percentage:+.2f}%\n"
|
|
|
-
|
|
|
- # Add risk management info
|
|
|
- if margin_used is not None:
|
|
|
+
|
|
|
+ # Show exchange-provided risk data if available
|
|
|
+ if margin_used > 0:
|
|
|
positions_text += f" 💳 Margin Used: ${margin_used:,.2f}\n"
|
|
|
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)
|
|
|
positions_text += f" ⚠️ Liquidation: {liq_price_str}\n"
|
|
|
+
|
|
|
+ # Show stop loss if linked in database
|
|
|
if position_trade.get('stop_loss_price'):
|
|
|
sl_price = position_trade['stop_loss_price']
|
|
|
positions_text += f" 🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, base_asset)}\n"
|
|
|
+
|
|
|
+ # Show take profit if linked in database
|
|
|
if position_trade.get('take_profit_price'):
|
|
|
tp_price = position_trade['take_profit_price']
|
|
|
positions_text += f" 🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, base_asset)}\n"
|
|
|
-
|
|
|
- # Add external stop losses
|
|
|
- external_sls = self._get_external_stop_losses(symbol, position_side, entry_price, abs_current_amount, exchange_orders)
|
|
|
- for ext_sl_price in external_sls:
|
|
|
- positions_text += f" 🛡️ External SL: {formatter.format_price_with_symbol(ext_sl_price, base_asset)}\n"
|
|
|
-
|
|
|
+
|
|
|
positions_text += f" 🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
|
|
|
|
|
|
except Exception as e:
|