Procházet zdrojové kódy

Enhance InfoCommands and MarketMonitor to incorporate leverage and risk metrics in position reporting. Updated InfoCommands to format direction text with leverage details and improved handling of margin and liquidation prices. Enhanced MarketMonitor to update unrealized P&L and market data for open positions, ensuring accurate tracking of trading metrics. Added new method in TradingStats for updating market-related data, improving database interactions for trade lifecycle management.

Carles Sentis před 3 dny
rodič
revize
1467766bed

+ 18 - 9
src/commands/info_commands.py

@@ -185,14 +185,25 @@ class InfoCommands:
                 total_unrealized += unrealized_pnl if unrealized_pnl else 0
                 
                 # Position emoji and formatting
+                direction_text = ""
                 if position_side == 'long':
                     pos_emoji = "🟢"
-                    direction = "LONG"
+                    direction_text = "LONG"
                 else:  # Short position
                     pos_emoji = "🔴"
-                    direction = "SHORT"
+                    direction_text = "SHORT"
+                
+                # 🆕 Incorporate leverage into direction text if available
+                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}")
                 
-                pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
+                pnl_emoji = "🟢" if unrealized_pnl >= 0 else "��"
                 
                 # Format prices with proper precision for this token
                 formatter = get_formatter()
@@ -206,7 +217,7 @@ class InfoCommands:
                 elif trade_type == 'bot':
                     type_indicator = " 🤖"  # Bot-created position
                 
-                positions_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
+                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"   💰 Entry: {entry_price_str}\n"
                 positions_text += f"   📊 Mark: {mark_price_str}\n"
@@ -214,11 +225,9 @@ class InfoCommands:
                 positions_text += f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
                 
                 # Show exchange-provided risk data if available
-                if leverage:
-                    positions_text += f"   ⚡ Leverage: {leverage:.1f}x\n"
-                if margin_used:
-                    positions_text += f"   💳 Margin: ${margin_used:,.2f}\n"
-                if liquidation_price:
+                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)
                     positions_text += f"   ⚠️ Liquidation: {liq_price_str}\n"
                 

+ 82 - 0
src/monitoring/market_monitor.py

@@ -223,6 +223,88 @@ class MarketMonitor:
             self.last_cache_update = datetime.now(timezone.utc)
             
             logger.debug(f"🔄 Updated cache: {len(fresh_positions)} positions, {len(fresh_orders)} orders")
+
+            # 💹 Update unrealized P&L and mark price in DB for open positions
+            stats = self.trading_engine.get_stats()
+            if stats and fresh_positions:
+                for ex_pos in fresh_positions:
+                    symbol = ex_pos.get('symbol')
+                    if not symbol:
+                        continue
+
+                    db_trade = stats.get_trade_by_symbol_and_status(symbol, status='position_opened')
+                    
+                    if db_trade:
+                        lifecycle_id = db_trade.get('trade_lifecycle_id')
+                        if not lifecycle_id:
+                            continue
+
+                        # Extract all relevant data from exchange position (ex_pos)
+                        # Ensure to handle cases where keys might be missing or values are None/empty strings
+                        
+                        current_size_from_ex = ex_pos.get('contracts') # Usually 'contracts' in CCXT
+                        if current_size_from_ex is not None:
+                            try: current_position_size = float(current_size_from_ex) 
+                            except (ValueError, TypeError): current_position_size = None
+                        else: current_position_size = None
+
+                        entry_price_from_ex = ex_pos.get('entryPrice') or ex_pos.get('entryPx')
+                        if entry_price_from_ex is not None:
+                            try: entry_price = float(entry_price_from_ex)
+                            except (ValueError, TypeError): entry_price = None
+                        else: entry_price = None
+
+                        mark_price_from_ex = ex_pos.get('markPrice') or ex_pos.get('markPx')
+                        if mark_price_from_ex is not None:
+                            try: mark_price = float(mark_price_from_ex)
+                            except (ValueError, TypeError): mark_price = None
+                        else: mark_price = None
+
+                        unrealized_pnl_from_ex = ex_pos.get('unrealizedPnl')
+                        if unrealized_pnl_from_ex is not None:
+                            try: unrealized_pnl = float(unrealized_pnl_from_ex)
+                            except (ValueError, TypeError): unrealized_pnl = None
+                        else: unrealized_pnl = None
+
+                        liquidation_price_from_ex = ex_pos.get('liquidationPrice')
+                        if liquidation_price_from_ex is not None:
+                            try: liquidation_price = float(liquidation_price_from_ex)
+                            except (ValueError, TypeError): liquidation_price = None
+                        else: liquidation_price = None
+
+                        margin_used_from_ex = ex_pos.get('marginUsed') # Or other keys like 'initialMargin', 'maintenanceMargin' depending on exchange
+                        if margin_used_from_ex is not None:
+                            try: margin_used = float(margin_used_from_ex)
+                            except (ValueError, TypeError): margin_used = None
+                        else: margin_used = None
+
+                        leverage_from_ex = ex_pos.get('leverage')
+                        if leverage_from_ex is not None:
+                            try: leverage = float(leverage_from_ex)
+                            except (ValueError, TypeError): leverage = None
+                        else: leverage = None
+
+                        position_value_from_ex = ex_pos.get('notional') # 'notional' is common for position value
+                        if position_value_from_ex is not None:
+                            try: position_value = float(position_value_from_ex)
+                            except (ValueError, TypeError): position_value = None
+                        else: position_value = None
+                        
+                        # Fallback for position_value if notional is not available but mark_price and size are
+                        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
+
+                        stats.update_trade_market_data(
+                            trade_lifecycle_id=lifecycle_id, 
+                            unrealized_pnl=unrealized_pnl, 
+                            mark_price=mark_price,
+                            current_position_size=current_position_size,
+                            entry_price=entry_price, # This will update the entry price if the exchange provides a new average
+                            liquidation_price=liquidation_price,
+                            margin_used=margin_used,
+                            leverage=leverage,
+                            position_value=position_value
+                        )
             
             # 🆕 Detect immediate changes for responsive notifications
             if len(fresh_positions) != len(self.last_known_positions):

+ 89 - 5
src/trading/trading_stats.py

@@ -10,7 +10,6 @@ import os
 import logging
 from datetime import datetime, timedelta, timezone
 from typing import Dict, List, Any, Optional, Tuple, Union
-import numpy as np
 import math
 from collections import defaultdict
 import uuid
@@ -115,6 +114,13 @@ class TradingStats:
                 -- P&L tracking
                 realized_pnl REAL DEFAULT 0,
                 unrealized_pnl REAL DEFAULT 0,
+                mark_price REAL DEFAULT 0,
+                position_value REAL DEFAULT NULL,
+                
+                -- Risk Info from Exchange
+                liquidation_price REAL DEFAULT NULL,
+                margin_used REAL DEFAULT NULL,
+                leverage REAL DEFAULT NULL,
                 
                 -- Timestamps
                 position_opened_at TEXT,
@@ -1501,16 +1507,16 @@ class TradingStats:
             if abs(exchange_position_size) < 1e-8 and not has_open_orders:
                 # Calculate realized P&L based on position side
                 position_side = current_trade['position_side']
-                entry_price = current_trade['entry_price']
-                current_amount = current_trade['current_position_size']
+                entry_price_db = current_trade['entry_price'] # entry_price from db
+                # current_amount = current_trade['current_position_size'] # Not directly used for PNL calc here
                 
                 # For a closed position, we need to calculate final P&L
                 # This would typically come from the closing trade, but for confirmation we estimate
-                estimated_pnl = current_trade.get('realized_pnl', 0)
+                estimated_pnl = current_trade.get('realized_pnl', 0) # Use existing realized_pnl if any
                 
                 success = self.update_trade_position_closed(
                     lifecycle_id, 
-                    entry_price,  # Using entry price as estimate since position is confirmed closed
+                    entry_price_db,  # Using entry price from DB as estimate since position is confirmed closed
                     estimated_pnl,
                     "exchange_confirmed_closed"
                 )
@@ -1525,3 +1531,81 @@ class TradingStats:
         except Exception as e:
             logger.error(f"❌ Error confirming position with exchange: {e}")
             return False
+
+    def update_trade_market_data(self, 
+                                 trade_lifecycle_id: str, 
+                                 unrealized_pnl: Optional[float] = None, 
+                                 mark_price: Optional[float] = None,
+                                 current_position_size: Optional[float] = None,
+                                 entry_price: Optional[float] = None,
+                                 liquidation_price: Optional[float] = None,
+                                 margin_used: Optional[float] = None,
+                                 leverage: Optional[float] = None,
+                                 position_value: Optional[float] = None) -> bool:
+        """Update market-related data for an open trade lifecycle.
+        Only updates fields for which a non-None value is provided.
+        """
+        try:
+            updates = []
+            params = []
+            
+            if unrealized_pnl is not None:
+                updates.append("unrealized_pnl = ?")
+                params.append(unrealized_pnl)
+            if mark_price is not None:
+                updates.append("mark_price = ?")
+                params.append(mark_price)
+            if current_position_size is not None:
+                updates.append("current_position_size = ?")
+                params.append(current_position_size)
+            if entry_price is not None: # If exchange provides updated avg entry
+                updates.append("entry_price = ?")
+                params.append(entry_price)
+            if liquidation_price is not None:
+                updates.append("liquidation_price = ?")
+                params.append(liquidation_price)
+            if margin_used is not None:
+                updates.append("margin_used = ?")
+                params.append(margin_used)
+            if leverage is not None:
+                updates.append("leverage = ?")
+                params.append(leverage)
+            if position_value is not None:
+                updates.append("position_value = ?")
+                params.append(position_value)
+
+            if not updates:
+                logger.debug(f"No market data fields provided to update for lifecycle {trade_lifecycle_id}.")
+                return True # No update needed, not an error
+
+            timestamp = datetime.now(timezone.utc).isoformat()
+            updates.append("updated_at = ?")
+            params.append(timestamp)
+
+            set_clause = ", ".join(updates)
+            query = f"""
+                UPDATE trades
+                SET {set_clause}
+                WHERE trade_lifecycle_id = ? AND status = 'position_opened'
+            """
+            params.append(trade_lifecycle_id)
+            
+            # Use the class's own connection self.conn
+            cursor = self.conn.cursor()
+            cursor.execute(query, tuple(params))
+            self.conn.commit()
+            updated_rows = cursor.rowcount
+
+            if updated_rows > 0:
+                logger.debug(f"💹 Updated market data for lifecycle {trade_lifecycle_id}. Fields: {updates}")
+                return True
+            else:
+                # This might happen if the lifecycle ID doesn't exist or status is not 'position_opened'
+                # logger.warning(f"⚠️ No trade found or not in 'position_opened' state for lifecycle {trade_lifecycle_id} to update market data.")
+                return False # Not necessarily an error
+
+        except Exception as e:
+            logger.error(f"❌ Error updating market data for trade lifecycle {trade_lifecycle_id}: {e}")
+            return False
+
+    # --- End Trade Lifecycle Management ---