Преглед на файлове

Enhance PositionTracker and PerformanceCalculator with improved error handling and data processing

- Updated PositionTracker to distinguish between API failures and legitimate empty positions, enhancing logging for better traceability.
- Improved handling of current positions during API failures and exceptions to avoid false notifications.
- Refactored PerformanceCalculator to include safe conversion and division methods, ensuring robust calculations for trading metrics.
- Enhanced logging throughout PerformanceCalculator for better clarity on trading statistics and performance metrics.
Carles Sentis преди 1 седмица
родител
ревизия
721fc74b5c
променени са 3 файла, в които са добавени 93 реда и са изтрити 54 реда
  1. 12 7
      src/monitoring/position_tracker.py
  2. 80 46
      src/stats/performance_calculator.py
  3. 1 1
      trading_bot.py

+ 12 - 7
src/monitoring/position_tracker.py

@@ -262,8 +262,9 @@ class PositionTracker:
             logger.debug("🔍 Fetching positions from Hyperliquid client...")
             positions = self.hl_client.get_positions()
             
-            if not positions:
-                logger.warning("📊 No positions returned from exchange - this might be wrong if you have open positions!")
+            # Distinguish between API failure (None) and legitimate empty positions ([])
+            if positions is None:
+                logger.warning("📊 API failure - could not fetch positions from exchange!")
                 # Don't clear positions during API failures - keep last known state to avoid false "position opened" notifications
                 if not self.current_positions:
                     # Only clear if we truly have no tracked positions (e.g., first startup)
@@ -271,6 +272,10 @@ class PositionTracker:
                 else:
                     logger.info(f"📊 Keeping last known positions during API failure: {list(self.current_positions.keys())}")
                 return
+            elif not positions:  # Empty list [] - legitimately no positions
+                logger.info("📊 No open positions on exchange - clearing position tracker state")
+                self.current_positions = {}
+                return
                 
             logger.info(f"📊 Raw positions data from exchange: {len(positions)} positions")
             # Log first position structure for debugging
@@ -313,25 +318,25 @@ class PositionTracker:
                             'return_on_equity': float(position_data.get('returnOnEquity', '0'))
                         }
             
-            # Check if we're recovering from API failure
+            # Log position state changes
             had_positions_before = len(self.current_positions) > 0
             getting_positions_now = len(new_positions) > 0
             
             if had_positions_before and not getting_positions_now:
-                logger.info("📊 All positions appear to have been closed")
+                logger.info("📊 All positions have been closed on exchange")
             elif not had_positions_before and getting_positions_now:
                 logger.info(f"📊 New positions detected: {list(new_positions.keys())}")
             elif had_positions_before and getting_positions_now:
                 logger.debug(f"✅ Updated current positions: {len(new_positions)} open positions ({list(new_positions.keys())})")
             else:
-                logger.debug(f"✅ Updated current positions: {len(new_positions)} open positions ({list(new_positions.keys()) if new_positions else 'none'})")
+                logger.debug(f"✅ Confirmed no open positions on exchange")
             
             self.current_positions = new_positions
             
         except Exception as e:
             logger.error(f"❌ Error updating current positions: {e}", exc_info=True)
-            # Don't clear positions on exception - keep last known state
-            logger.info(f"📊 Keeping last known positions during error: {list(self.current_positions.keys()) if self.current_positions else 'none'}")
+            # Don't clear positions on exception - keep last known state to avoid false notifications
+            logger.info(f"📊 Keeping last known positions during exception: {list(self.current_positions.keys()) if self.current_positions else 'none'}")
             
     async def _process_position_changes(self, previous: Dict, current: Dict):
         """Process changes between previous and current positions"""

+ 80 - 46
src/stats/performance_calculator.py

@@ -21,6 +21,24 @@ class PerformanceCalculator:
     def __init__(self, db_manager):
         """Initialize with database manager."""
         self.db = db_manager
+    
+    def _safe_float(self, value, default=0.0):
+        """Safely convert a value to float, returning default if None or invalid."""
+        if value is None:
+            return default
+        try:
+            return float(value)
+        except (ValueError, TypeError):
+            return default
+    
+    def _safe_divide(self, numerator, denominator, default=0.0):
+        """Safely divide two numbers, returning default if denominator is 0 or None."""
+        num = self._safe_float(numerator, 0.0)
+        den = self._safe_float(denominator, 0.0)
+        
+        if den == 0:
+            return default
+        return num / den
 
     def _format_duration(self, total_seconds: int) -> str:
         """Format duration from seconds to human-readable format."""
@@ -82,35 +100,38 @@ class PerformanceCalculator:
             
             # Process token stats
             for token in token_stats:
-                if token.get('total_completed_cycles', 0) > 0:
-                    total_trades += token.get('total_completed_cycles', 0)
-                    total_wins += token.get('winning_cycles', 0)
-                    total_losses += token.get('losing_cycles', 0)
-                    total_pnl += token.get('total_realized_pnl', 0)
-                    total_entry_volume += token.get('total_entry_volume', 0)
-                    total_exit_volume += token.get('total_exit_volume', 0)
+                completed_cycles = self._safe_float(token.get('total_completed_cycles'), 0)
+                if completed_cycles > 0:
+                    total_trades += completed_cycles
+                    total_wins += self._safe_float(token.get('winning_cycles'), 0)
+                    total_losses += self._safe_float(token.get('losing_cycles'), 0)
+                    total_pnl += self._safe_float(token.get('total_realized_pnl'), 0)
+                    total_entry_volume += self._safe_float(token.get('total_entry_volume'), 0)
+                    total_exit_volume += self._safe_float(token.get('total_exit_volume'), 0)
                     
                     # Track largest trades
-                    token_largest_win = token.get('largest_winning_cycle_pnl', 0)
-                    token_largest_loss = token.get('largest_losing_cycle_pnl', 0)
+                    token_largest_win = self._safe_float(token.get('largest_winning_cycle_pnl'), 0)
+                    token_largest_loss = self._safe_float(token.get('largest_losing_cycle_pnl'), 0)
                     
                     if token_largest_win > largest_win:
                         largest_win = token_largest_win
                         largest_win_token = token['token']
-                        largest_win_pct = (token_largest_win / token.get('largest_winning_cycle_entry_volume', 1)) * 100
+                        entry_volume = self._safe_float(token.get('largest_winning_cycle_entry_volume'), 1.0)
+                        largest_win_pct = self._safe_divide(token_largest_win, entry_volume) * 100
                     
                     # For losses, we want the most negative number
                     if token_largest_loss < 0 and (largest_loss == 0 or token_largest_loss < largest_loss):
                         largest_loss = token_largest_loss
                         largest_loss_token = token['token']
-                        largest_loss_pct = (token_largest_loss / token.get('largest_losing_cycle_entry_volume', 1)) * 100
+                        entry_volume = self._safe_float(token.get('largest_losing_cycle_entry_volume'), 1.0)
+                        largest_loss_pct = self._safe_divide(token_largest_loss, entry_volume) * 100
                     
                     # Track best/worst tokens
-                    token_pnl = token.get('total_realized_pnl', 0)
-                    token_volume = token.get('total_entry_volume', 0)
+                    token_pnl = self._safe_float(token.get('total_realized_pnl'), 0)
+                    token_volume = self._safe_float(token.get('total_entry_volume'), 0)
                     
                     if token_volume > 0:
-                        token_pnl_pct = (token_pnl / token_volume) * 100
+                        token_pnl_pct = self._safe_divide(token_pnl, token_volume) * 100
                         
                         if best_token_name == "N/A" or token_pnl > best_token_pnl_value:
                             best_token_name = token['token']
@@ -125,20 +146,27 @@ class PerformanceCalculator:
                             worst_token_volume = token_volume
             
             # Calculate win rate and profit factor
-            win_rate = (total_wins / total_trades * 100) if total_trades > 0 else 0
+            win_rate = self._safe_divide(total_wins, total_trades) * 100
+            
+            # Calculate sum of winning and losing trades  
+            sum_winning = sum(self._safe_float(token.get('sum_of_winning_pnl'), 0) for token in token_stats)
+            sum_losing = abs(sum(self._safe_float(token.get('sum_of_losing_pnl'), 0) for token in token_stats))
             
-            # Calculate sum of winning and losing trades
-            sum_winning = sum(token.get('sum_of_winning_pnl', 0) for token in token_stats)
-            sum_losing = abs(sum(token.get('sum_of_losing_pnl', 0) for token in token_stats))
-            profit_factor = (sum_winning / sum_losing) if sum_losing > 0 else float('inf') if sum_winning > 0 else 0
+            if sum_losing > 0:
+                profit_factor = self._safe_divide(sum_winning, sum_losing)
+            elif sum_winning > 0:
+                profit_factor = float('inf')
+            else:
+                profit_factor = 0
             
             # Calculate average P&L stats
-            avg_win_pnl = sum_winning / total_wins if total_wins > 0 else 0
-            avg_loss_pnl = sum_losing / total_losses if total_losses > 0 else 0
-            avg_trade_pnl = total_pnl / total_trades if total_trades > 0 else 0.0
+            avg_win_pnl = self._safe_divide(sum_winning, total_wins)
+            avg_loss_pnl = self._safe_divide(sum_losing, total_losses) 
+            avg_trade_pnl = self._safe_divide(total_pnl, total_trades)
             
             # Calculate expectancy
-            expectancy = (avg_win_pnl * (win_rate/100)) - (avg_loss_pnl * (1 - win_rate/100))
+            win_rate_decimal = win_rate / 100
+            expectancy = (avg_win_pnl * win_rate_decimal) - (avg_loss_pnl * (1 - win_rate_decimal))
             
             # Get max drawdown
             max_drawdown, max_drawdown_pct, drawdown_start_date = self.get_live_max_drawdown()
@@ -198,25 +226,31 @@ class PerformanceCalculator:
         )
         
         for token in token_stats:
-            total_cycles = token.get('total_completed_cycles', 0)
-            winning_cycles = token.get('winning_cycles', 0)
+            total_cycles = self._safe_float(token.get('total_completed_cycles'), 0)
+            winning_cycles = self._safe_float(token.get('winning_cycles'), 0)
             
             # Calculate win rate
-            token['win_rate'] = (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0
+            token['win_rate'] = self._safe_divide(winning_cycles, total_cycles) * 100
             
             # Calculate profit factor
-            sum_winning = token.get('sum_of_winning_pnl', 0)
-            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
+            sum_winning = self._safe_float(token.get('sum_of_winning_pnl'), 0)
+            sum_losing = self._safe_float(token.get('sum_of_losing_pnl'), 0)
+            
+            if sum_losing > 0:
+                token['profit_factor'] = self._safe_divide(sum_winning, sum_losing)
+            elif sum_winning > 0:
+                token['profit_factor'] = float('inf')
+            else:
+                token['profit_factor'] = 0
             
             # Calculate ROE from realized PnL and entry volume
-            total_pnl = token.get('total_realized_pnl', 0)
-            entry_volume = token.get('total_entry_volume', 0)
-            token['roe_percentage'] = (total_pnl / entry_volume * 100) if entry_volume > 0 else 0.0
+            total_pnl = self._safe_float(token.get('total_realized_pnl'), 0)
+            entry_volume = self._safe_float(token.get('total_entry_volume'), 0)
+            token['roe_percentage'] = self._safe_divide(total_pnl, entry_volume) * 100
             
             # Format durations
-            total_duration = token.get('total_duration_seconds', 0)
-            avg_duration = total_duration / total_cycles if total_cycles > 0 else 0
+            total_duration = self._safe_float(token.get('total_duration_seconds'), 0)
+            avg_duration = self._safe_divide(total_duration, total_cycles)
             token['average_trade_duration_formatted'] = self._format_duration(avg_duration)
             
             # Token display name (use token as-is)
@@ -257,7 +291,7 @@ class PerformanceCalculator:
                 running_max = balance
             
             drawdown = running_max - balance
-            drawdown_percentage = (drawdown / running_max * 100) if running_max > 0 else 0
+            drawdown_percentage = self._safe_divide(drawdown, running_max) * 100
             
             if drawdown > max_drawdown:
                 max_drawdown = drawdown
@@ -266,7 +300,7 @@ class PerformanceCalculator:
         # Calculate period return
         initial_balance_period = balances[0] if balances else 0
         period_pnl = current_balance - initial_balance_period
-        period_return_percentage = (period_pnl / initial_balance_period * 100) if initial_balance_period > 0 else 0
+        period_return_percentage = self._safe_divide(period_pnl, initial_balance_period) * 100
         
         stats = {
             'peak_balance': peak_balance,
@@ -317,7 +351,7 @@ class PerformanceCalculator:
         else:
             # Still in a drawdown, check if it's a new max
             drawdown = peak_balance - current_balance
-            drawdown_percentage = (drawdown / peak_balance * 100) if peak_balance > 0 else 0
+            drawdown_percentage = self._safe_divide(drawdown, peak_balance) * 100
             
             if drawdown_percentage > max_drawdown_percentage:
                 self.db._set_metadata('drawdown_max_drawdown_pct', str(drawdown_percentage))
@@ -342,10 +376,10 @@ class PerformanceCalculator:
             # Calculate daily returns
             returns = []
             for i in range(1, len(balance_history)):
-                prev_balance = balance_history[i-1]['balance']
-                curr_balance = balance_history[i]['balance']
+                prev_balance = self._safe_float(balance_history[i-1]['balance'])
+                curr_balance = self._safe_float(balance_history[i]['balance'])
                 if prev_balance > 0:
-                    daily_return = (curr_balance - prev_balance) / prev_balance
+                    daily_return = self._safe_divide(curr_balance - prev_balance, prev_balance)
                     returns.append(daily_return)
                 
             if not returns or np.std(returns) == 0:
@@ -355,7 +389,7 @@ class PerformanceCalculator:
             avg_daily_return = np.mean(returns)
             std_dev_daily_return = np.std(returns)
             
-            sharpe_ratio = (avg_daily_return - (risk_free_rate / 365)) / std_dev_daily_return
+            sharpe_ratio = self._safe_divide(avg_daily_return - (risk_free_rate / 365), std_dev_daily_return)
             annualized_sharpe_ratio = sharpe_ratio * np.sqrt(365) # Annualize
             
             return annualized_sharpe_ratio
@@ -417,12 +451,12 @@ class PerformanceCalculator:
                     'average_daily_pnl': 0
                 }
             
-            total_pnl = sum(day.get('pnl', 0) or 0 for day in daily_stats)
-            total_trades = sum(day.get('trades', 0) or 0 for day in daily_stats)
-            total_volume = sum(day.get('volume', 0) or 0 for day in daily_stats)
-            trading_days = len([day for day in daily_stats if (day.get('trades', 0) or 0) > 0])
+            total_pnl = sum(self._safe_float(day.get('pnl')) for day in daily_stats)
+            total_trades = sum(self._safe_float(day.get('trades')) for day in daily_stats)
+            total_volume = sum(self._safe_float(day.get('volume')) for day in daily_stats)
+            trading_days = len([day for day in daily_stats if self._safe_float(day.get('trades')) > 0])
             
-            average_daily_pnl = total_pnl / trading_days if trading_days > 0 else 0
+            average_daily_pnl = self._safe_divide(total_pnl, trading_days)
             
             return {
                 'period_start': start_date,

+ 1 - 1
trading_bot.py

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