Răsfoiți Sursa

Refactor position data handling for improved accuracy and consistency

- Updated position data retrieval across multiple modules to access nested data structures, ensuring accurate mapping of position attributes such as size, coin, and unrealized PnL.
- Enhanced logging and error handling for better traceability in position calculations and risk management.
- Calculated ROE manually from position and price data, improving the accuracy of performance metrics.
Carles Sentis 17 ore în urmă
părinte
comite
bd4be69bf4

+ 35 - 20
src/commands/info/positions.py

@@ -33,19 +33,25 @@ class PositionsCommands(InfoCommandsBase):
             exchange_orders = self.trading_engine.get_orders() or []
             
             # Debug: Log the exchange positions data structure
-            logger.info(f"🔍 Exchange positions data: {[{k: v for k, v in pos.items() if k in ['coin', 'symbol', 'returnOnEquity', 'szi']} for pos in exchange_positions[:2]]}")
+            logger.info(f"🔍 Exchange positions data: {[{k: v for k, v in pos.items() if k in ['symbol']} for pos in exchange_positions[:2]]}")
             
             # Create lookup for exchange data by symbol
             exchange_data_by_symbol = {}
             for ex_pos in exchange_positions:
-                # Try both 'coin' and 'symbol' fields for mapping
-                symbol_key = ex_pos.get('coin', '') or ex_pos.get('symbol', '')
-                if symbol_key:
-                    # Also try extracting base asset from symbol if it's in format "TOKEN/USDC:USDC"
-                    if '/' in symbol_key:
-                        base_token = symbol_key.split('/')[0]
-                        exchange_data_by_symbol[base_token] = ex_pos
-                    exchange_data_by_symbol[symbol_key] = ex_pos
+                # Access the nested position data in info.position
+                position_data = ex_pos.get('info', {}).get('position', {})
+                if position_data:
+                    # Get the coin symbol from the nested data
+                    coin = position_data.get('coin', '')
+                    if coin:
+                        exchange_data_by_symbol[coin] = position_data
+                
+                # Also map by the CCXT symbol for backup
+                symbol = ex_pos.get('symbol', '')
+                if symbol and '/' in symbol:
+                    base_token = symbol.split('/')[0]
+                    if base_token not in exchange_data_by_symbol and position_data:
+                        exchange_data_by_symbol[base_token] = position_data
                     
             logger.info(f"🔍 Exchange data keys: {list(exchange_data_by_symbol.keys())}")
 
@@ -113,31 +119,40 @@ class PositionsCommands(InfoCommandsBase):
                     exchange_data = exchange_data_by_symbol.get(base_asset)
                     logger.info(f"🔍 Looking for '{base_asset}' in exchange data. Found: {exchange_data is not None}")
                     if exchange_data:
-                        logger.info(f"🔍 Exchange data for {base_asset}: ROE={exchange_data.get('returnOnEquity')}, markPrice={exchange_data.get('markPrice')}")
+                        logger.info(f"🔍 Exchange data for {base_asset}: ROE={exchange_data.get('returnOnEquity')}, entryPx={exchange_data.get('entryPx')}")
                     
                     # Get price data with defaults - prioritize live exchange data
                     mark_price = entry_price  # Default to entry price
                     
-                    # Try to get live mark price from exchange first
-                    if exchange_data and exchange_data.get('markPrice') is not None:
+                    # For Hyperliquid, we need to calculate current mark price from position value and size
+                    if exchange_data:
                         try:
-                            mark_price = float(exchange_data['markPrice'])
+                            position_value = float(exchange_data.get('positionValue', 0))
+                            position_size = float(exchange_data.get('szi', 0))
+                            if position_size != 0:
+                                mark_price = position_value / abs(position_size)
                         except (ValueError, TypeError):
-                            logger.warning(f"Could not convert exchange mark_price for {symbol}")
-                    # Fallback to database mark price
-                    elif position_trade.get('mark_price') is not None:
+                            logger.warning(f"Could not calculate mark price from exchange data for {symbol}")
+                    
+                    # Fallback to database mark price if calculation fails
+                    if mark_price == entry_price and position_trade.get('mark_price') is not None:
                         try:
                             mark_price = float(position_trade['mark_price'])
                         except (ValueError, TypeError):
                             logger.warning(f"Could not convert database mark_price for {symbol}")
                     
-                    # Calculate unrealized PnL
+                    # Get unrealized PnL from exchange data first, then database
                     unrealized_pnl = 0.0
-                    if position_trade.get('unrealized_pnl') is not None:
+                    if exchange_data and exchange_data.get('unrealizedPnl') is not None:
+                        try:
+                            unrealized_pnl = float(exchange_data['unrealizedPnl'])
+                        except (ValueError, TypeError):
+                            logger.warning(f"Could not convert exchange unrealizedPnl for {symbol}")
+                    elif position_trade.get('unrealized_pnl') is not None:
                         try:
                             unrealized_pnl = float(position_trade['unrealized_pnl'])
                         except (ValueError, TypeError):
-                            logger.warning(f"Could not convert unrealized_pnl for {symbol}")
+                            logger.warning(f"Could not convert database unrealized_pnl for {symbol}")
                     
                     # Get ROE from live exchange data (much more accurate)
                     roe_percentage = 0.0
@@ -224,7 +239,7 @@ class PositionsCommands(InfoCommandsBase):
                         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"
+                    positions_text += f"   {pnl_emoji} uP&L: ${unrealized_pnl:,.2f}\n"
                     
                     # Show ROE
                     roe_emoji = "🟢" if roe_percentage >= 0 else "🔴"

+ 9 - 5
src/commands/info/risk.py

@@ -57,14 +57,18 @@ class RiskCommands(InfoCommandsBase):
         lines = ["<b>Open Positions:</b>"]
         for p in positions:
             try:
-                position_info = p.get('position', p)
-                pnl = float(p.get('unrealizedPnl', 0.0))
+                # Access nested position data from info.position
+                position_info = p.get('info', {}).get('position', {})
+                if not position_info:
+                    continue
+                    
+                pnl = float(position_info.get('unrealizedPnl', 0.0))
                 pnl_emoji = "🟢" if pnl >= 0 else "🔴"
                 
-                # Use .get() with defaults to avoid KeyErrors
-                asset = position_info.get('asset', 'N/A')
+                # Use correct Hyperliquid field names
+                asset = position_info.get('coin', 'N/A')
                 size = position_info.get('szi', 'N/A')
-                side = position_info.get('side', 'N/A')
+                side = "Long" if float(position_info.get('szi', '0')) > 0 else "Short"
                 entry_price = position_info.get('entryPx', 'N/A')
 
                 lines.append(f"• {asset}: {size} {side} @ ${entry_price} {pnl_emoji}")

+ 7 - 2
src/monitoring/pending_orders_manager.py

@@ -114,9 +114,14 @@ class PendingOrdersManager:
                 
             current_positions = {}
             for position in positions:
-                size = float(position.get('szi', '0'))
+                # Access nested position data from info.position
+                position_data = position.get('info', {}).get('position', {})
+                if not position_data:
+                    continue
+                    
+                size = float(position_data.get('szi', '0'))
                 if size != 0:
-                    symbol = position.get('coin', '')
+                    symbol = position_data.get('coin', '')
                     if symbol:
                         current_positions[symbol] = {
                             'size': size,

+ 12 - 7
src/monitoring/position_tracker.py

@@ -121,17 +121,22 @@ class PositionTracker:
                 
             new_positions = {}
             for position in positions:
-                size = float(position.get('szi', '0'))
+                # Access nested position data from info.position
+                position_data = position.get('info', {}).get('position', {})
+                if not position_data:
+                    continue
+                    
+                size = float(position_data.get('szi', '0'))
                 if size != 0:  # Only include open positions
-                    symbol = position.get('coin', '')
+                    symbol = position_data.get('coin', '')
                     if symbol:
                         new_positions[symbol] = {
                             'size': size,
-                            'entry_px': float(position.get('entryPx', '0')),
-                            'unrealized_pnl': float(position.get('unrealizedPnl', '0')),
-                            'margin_used': float(position.get('marginUsed', '0')),
-                            'max_leverage': float(position.get('maxLeverage', '1')),
-                            'return_on_equity': float(position.get('returnOnEquity', '0'))
+                            'entry_px': float(position_data.get('entryPx', '0')),
+                            'unrealized_pnl': float(position_data.get('unrealizedPnl', '0')),
+                            'margin_used': float(position_data.get('marginUsed', '0')),
+                            'max_leverage': float(position_data.get('maxLeverage', '1')),
+                            'return_on_equity': float(position_data.get('returnOnEquity', '0'))
                         }
             
             self.current_positions = new_positions

+ 18 - 8
src/monitoring/risk_manager.py

@@ -60,10 +60,15 @@ class RiskManager:
             positions_to_close = []
             
             for position in positions:
-                size = float(position.get('szi', '0'))
+                # Access nested position data from info.position
+                position_data = position.get('info', {}).get('position', {})
+                if not position_data:
+                    continue
+                    
+                size = float(position_data.get('szi', '0'))
                 if size != 0:
-                    symbol = position.get('coin', '')
-                    roe = float(position.get('returnOnEquity', '0'))
+                    symbol = position_data.get('coin', '')
+                    roe = float(position_data.get('returnOnEquity', '0'))
                     
                     # Check if ROE exceeds hard exit threshold (negative ROE = loss)
                     if roe <= self.hard_exit_roe:
@@ -71,7 +76,7 @@ class RiskManager:
                             'symbol': symbol,
                             'size': size,
                             'roe': roe,
-                            'unrealized_pnl': float(position.get('unrealizedPnl', '0'))
+                            'unrealized_pnl': float(position_data.get('unrealizedPnl', '0'))
                         })
                         
             # Close positions that exceed risk threshold
@@ -159,11 +164,16 @@ class RiskManager:
             total_unrealized_pnl = 0
             
             for position in positions_data:
-                size = float(position.get('szi', '0'))
+                # Access nested position data from info.position
+                position_data = position.get('info', {}).get('position', {})
+                if not position_data:
+                    continue
+                    
+                size = float(position_data.get('szi', '0'))
                 if size != 0:
-                    symbol = position.get('coin', '')
-                    roe = float(position.get('returnOnEquity', '0'))
-                    unrealized_pnl = float(position.get('unrealizedPnl', '0'))
+                    symbol = position_data.get('coin', '')
+                    roe = float(position_data.get('returnOnEquity', '0'))
+                    unrealized_pnl = float(position_data.get('unrealizedPnl', '0'))
                     
                     total_unrealized_pnl += unrealized_pnl
                     

+ 6 - 10
src/notifications/notification_manager.py

@@ -427,18 +427,14 @@ class NotificationManager:
             if entry_price > 0:
                 if position_side == 'long':
                     pnl = amount * (price - entry_price)
-                    # Get ROE directly from exchange data
-                    info_data = stop_loss_info.get('info', {})
-                    position_info = info_data.get('position', {})
-                    roe_raw = position_info.get('returnOnEquity')
-                    roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
+                    # Calculate ROE manually from position and price data
+                    cost_basis = amount * entry_price
+                    roe = (pnl / cost_basis) * 100 if cost_basis > 0 else 0.0
                 else:  # short
                     pnl = amount * (entry_price - price) 
-                    # Get ROE directly from exchange data
-                    info_data = stop_loss_info.get('info', {})
-                    position_info = info_data.get('position', {})
-                    roe_raw = position_info.get('returnOnEquity')
-                    roe = float(roe_raw) * 100 if roe_raw is not None else 0.0
+                    # Calculate ROE manually from position and price data  
+                    cost_basis = amount * entry_price
+                    roe = (pnl / cost_basis) * 100 if cost_basis > 0 else 0.0
                 
                 pnl_emoji = "🟢" if pnl >= 0 else "🔴"
                 pnl_info = f"""

+ 4 - 5
src/stats/performance_calculator.py

@@ -209,11 +209,10 @@ class PerformanceCalculator:
             sum_losing = token.get('sum_of_losing_pnl', 0)
             token['profit_factor'] = sum_winning / sum_losing if sum_losing > 0 else float('inf') if sum_winning > 0 else 0
             
-            # Get ROE directly from the exchange data
-            info_data = token.get('info', {})
-            position_info = info_data.get('position', {})
-            roe_raw = position_info.get('returnOnEquity')
-            token['roe_percentage'] = float(roe_raw) * 100 if roe_raw is not None else 0.0
+            # Calculate ROE from realized PnL and entry volume
+            total_pnl = token.get('total_realized_pnl', 0)
+            entry_volume = token.get('completed_entry_volume', 0)
+            token['roe_percentage'] = (total_pnl / entry_volume * 100) if entry_volume > 0 else 0.0
             
             # Format durations
             total_duration = token.get('total_duration_seconds', 0)

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.6.290"
+BOT_VERSION = "2.6.291"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))