Procházet zdrojové kódy

Enhance InfoCommands and MarketMonitor to support unrealized P&L percentage tracking. Updated InfoCommands to calculate and display P&L metrics with improved formatting for asset details. Added new database fields for unrealized P&L percentage and integrated exchange data retrieval in MarketMonitor for better trade performance insights. Improved overall handling of position data and risk metrics.

Carles Sentis před 3 dny
rodič
revize
4acff625f3

+ 62 - 56
src/commands/info_commands.py

@@ -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:

+ 2 - 1
src/migrations/migrate_db.py

@@ -93,7 +93,8 @@ def run_migrations():
                 "liquidation_price": "REAL DEFAULT NULL",
                 "margin_used": "REAL DEFAULT NULL",
                 "leverage": "REAL DEFAULT NULL",
-                "position_value": "REAL DEFAULT NULL"
+                "position_value": "REAL DEFAULT NULL",
+                "unrealized_pnl_percentage": "REAL DEFAULT NULL"
             }
 
             for col_name, col_def in columns_to_add.items():

+ 9 - 1
src/monitoring/market_monitor.py

@@ -294,6 +294,13 @@ class MarketMonitor:
                         if position_value is None and mark_price is not None and current_position_size is not None:
                             position_value = abs(current_position_size) * mark_price
 
+                        # 🆕 Get P&L percentage from exchange if available
+                        roe_from_ex = ex_pos.get('percentage') # CCXT often uses 'percentage' for ROE
+                        if roe_from_ex is not None:
+                            try: unrealized_pnl_percentage_val = float(roe_from_ex) 
+                            except (ValueError, TypeError): unrealized_pnl_percentage_val = None
+                        else: unrealized_pnl_percentage_val = None
+
                         stats.update_trade_market_data(
                             trade_lifecycle_id=lifecycle_id, 
                             unrealized_pnl=unrealized_pnl, 
@@ -303,7 +310,8 @@ class MarketMonitor:
                             liquidation_price=liquidation_price,
                             margin_used=margin_used,
                             leverage=leverage,
-                            position_value=position_value
+                            position_value=position_value,
+                            unrealized_pnl_percentage=unrealized_pnl_percentage_val # Pass the new field
                         )
             
             # 🆕 Detect immediate changes for responsive notifications

+ 7 - 1
src/trading/trading_stats.py

@@ -13,6 +13,7 @@ from typing import Dict, List, Any, Optional, Tuple, Union
 import math
 from collections import defaultdict
 import uuid
+import numpy as np # Ensure numpy is imported as np
 
 # 🆕 Import the migration runner
 from src.migrations.migrate_db import run_migrations as run_db_migrations
@@ -127,6 +128,7 @@ class TradingStats:
                 unrealized_pnl REAL DEFAULT 0,
                 mark_price REAL DEFAULT 0,
                 position_value REAL DEFAULT NULL,
+                unrealized_pnl_percentage REAL DEFAULT NULL, 
                 
                 -- Risk Info from Exchange
                 liquidation_price REAL DEFAULT NULL,
@@ -1552,7 +1554,8 @@ class TradingStats:
                                  liquidation_price: Optional[float] = None,
                                  margin_used: Optional[float] = None,
                                  leverage: Optional[float] = None,
-                                 position_value: Optional[float] = None) -> bool:
+                                 position_value: Optional[float] = None,
+                                 unrealized_pnl_percentage: Optional[float] = None) -> bool:
         """Update market-related data for an open trade lifecycle.
         Only updates fields for which a non-None value is provided.
         """
@@ -1584,6 +1587,9 @@ class TradingStats:
             if position_value is not None:
                 updates.append("position_value = ?")
                 params.append(position_value)
+            if unrealized_pnl_percentage is not None:
+                updates.append("unrealized_pnl_percentage = ?")
+                params.append(unrealized_pnl_percentage)
 
             if not updates:
                 logger.debug(f"No market data fields provided to update for lifecycle {trade_lifecycle_id}.")