Просмотр исходного кода

Increment BOT_VERSION to 2.3.153 and enhance performance metrics in trading statistics.

- Updated BOT_VERSION for the upcoming release.
- Added calculation of PnL percentage from entry volume in completed trades summary.
- Introduced best and worst performing token metrics in performance calculations.
- Enhanced token statistics formatting to include average trade duration and improved error handling for token-specific stats.
- Updated drawdown monitoring methods for better integration with external systems.
Carles Sentis 1 день назад
Родитель
Сommit
3a4ad2f8cf
4 измененных файлов с 179 добавлено и 27 удалено
  1. 9 8
      src/commands/info_commands.py
  2. 44 3
      src/stats/performance_calculator.py
  3. 125 15
      src/stats/trading_stats.py
  4. 1 1
      trading_bot.py

+ 9 - 8
src/commands/info_commands.py

@@ -575,15 +575,20 @@ class InfoCommands:
         parts.append("📈 <b>Completed Trades Summary:</b>")
         if perf_summary.get('completed_trades', 0) > 0:
             pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
+            # Calculate PnL percentage from entry volume
+            entry_vol = perf_summary.get('completed_entry_volume', 0.0)
+            pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
+            
             parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
-            parts.append(f"• {pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({perf_summary.get('pnl_percentage', 0.0):+.2f}%)")
+            parts.append(f"• {pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({pnl_pct:+.2f}%)")
             parts.append(f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}W / {perf_summary.get('total_losses', 0)}L)")
             parts.append(f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}")
-            parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))} (Value per trade)")
+            parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
             parts.append(f"• Avg Win: {formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
             parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}")
             parts.append(f"• Entry Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
             parts.append(f"• Exit Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_exit_volume', 0.0))}")
+            parts.append(f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}")
             parts.append(f"• Cancelled Cycles: {perf_summary.get('total_cancelled', 0)}")
         else:
             parts.append("• No completed trades for this token yet.")
@@ -648,13 +653,9 @@ class InfoCommands:
             if context.args and len(context.args) > 0:
                 # Token-specific stats
                 token_name_arg = _normalize_token_case(context.args[0])
-                token_stats_data = stats_manager.get_token_detailed_stats(token_name_arg)
                 
-                if not token_stats_data: # Should not happen if get_token_detailed_stats returns a dict
-                     await reply_method(text=f"❌ Could not fetch detailed stats for {token_name_arg}.", parse_mode='HTML')
-                     return
-
-                stats_message = await self._format_token_specific_stats_message(token_stats_data, token_name_arg) # ADDED await
+                # Use the centralized formatting method from TradingStats
+                stats_message = stats_manager.format_token_stats_message(token_name_arg)
                 await reply_method(text=stats_message, parse_mode='HTML')
             else:
                 # Overall stats

+ 44 - 3
src/stats/performance_calculator.py

@@ -73,6 +73,28 @@ class PerformanceCalculator:
         initial_balance = float(self.db._get_metadata('initial_balance') or '0.0')
         roi_percentage = (total_realized_pnl / initial_balance * 100) if initial_balance > 0 else 0
 
+        # Calculate best and worst performing tokens
+        best_token_name = "N/A"
+        best_token_pnl_pct = 0.0
+        worst_token_name = "N/A" 
+        worst_token_pnl_pct = 0.0
+        
+        for token in token_stats:
+            if token.get('total_completed_cycles', 0) > 0:
+                total_pnl = token.get('total_realized_pnl', 0)
+                entry_volume = token.get('total_entry_volume', 0)
+                
+                if entry_volume > 0:
+                    pnl_pct = (total_pnl / entry_volume) * 100
+                    
+                    if best_token_name == "N/A" or pnl_pct > best_token_pnl_pct:
+                        best_token_name = token['token']
+                        best_token_pnl_pct = pnl_pct
+                    
+                    if worst_token_name == "N/A" or pnl_pct < worst_token_pnl_pct:
+                        worst_token_name = token['token']
+                        worst_token_pnl_pct = pnl_pct
+
         return {
             'total_realized_pnl': total_realized_pnl,
             'total_completed_cycles': total_completed_cycles,
@@ -82,20 +104,30 @@ class PerformanceCalculator:
             'win_rate': win_rate,
             'total_entry_volume': total_entry_volume,
             'total_exit_volume': total_exit_volume,
+            'total_trading_volume': total_entry_volume,  # Alias for compatibility
             'sum_of_winning_pnl': sum_of_winning_pnl,
             'sum_of_losing_pnl': sum_of_losing_pnl,
             'average_win_amount': average_win_amount,
             'average_loss_amount': average_loss_amount,
+            'avg_win': average_win_amount,  # Alias for compatibility
+            'avg_loss': average_loss_amount,  # Alias for compatibility
             'profit_factor': profit_factor,
             'expectancy': expectancy,
             'largest_winning_cycle': largest_winning_cycle,
             'largest_losing_cycle': largest_losing_cycle,
+            'largest_win': largest_winning_cycle,  # Alias for compatibility
+            'largest_loss': largest_losing_cycle,  # Alias for compatibility
+            'total_wins': total_winning_cycles,  # Alias for compatibility
+            'total_losses': total_losing_cycles,  # Alias for compatibility
             'roi_percentage': roi_percentage,
             'initial_balance': initial_balance,
             'current_balance': initial_balance + total_realized_pnl,
             'total_duration_seconds': total_duration_seconds,
             'average_trade_duration_seconds': average_trade_duration_seconds,
-            'average_trade_duration_formatted': average_trade_duration_formatted
+            'average_trade_duration_formatted': average_trade_duration_formatted,
+            'avg_trade_duration': average_trade_duration_formatted,  # Alias for compatibility
+            'best_performing_token': {'name': best_token_name, 'pnl_percentage': best_token_pnl_pct},
+            'worst_performing_token': {'name': worst_token_name, 'pnl_percentage': worst_token_pnl_pct}
         }
 
     def get_token_performance(self, limit: int = 20) -> List[Dict[str, Any]]:
@@ -124,8 +156,8 @@ class PerformanceCalculator:
             avg_duration = total_duration / total_cycles if total_cycles > 0 else 0
             token['average_trade_duration_formatted'] = self._format_duration(avg_duration)
             
-            # Format token for display
-            token['display_name'] = formatter.get_display_name(token['token'])
+            # Token display name (use token as-is)
+            token['display_name'] = token['token'].upper()
         
         return token_stats
 
@@ -180,6 +212,15 @@ class PerformanceCalculator:
     def get_live_max_drawdown(self) -> Tuple[float, float]:
         """Get the current live maximum drawdown from metadata."""
         try:
+            # Try to get from DrawdownMonitor first (newer system)
+            max_drawdown_pct = float(self.db._get_metadata('drawdown_max_drawdown_pct') or '0.0')
+            peak_balance = float(self.db._get_metadata('drawdown_peak_balance') or '0.0')
+            
+            if max_drawdown_pct > 0:
+                max_drawdown_absolute = peak_balance * (max_drawdown_pct / 100)
+                return max_drawdown_absolute, max_drawdown_pct
+            
+            # Fallback to legacy keys if DrawdownMonitor data not available
             max_drawdown_live = float(self.db._get_metadata('max_drawdown_live') or '0.0')
             max_drawdown_live_percentage = float(self.db._get_metadata('max_drawdown_live_percentage') or '0.0')
             return max_drawdown_live, max_drawdown_live_percentage

+ 125 - 15
src/stats/trading_stats.py

@@ -315,6 +315,18 @@ class TradingStats:
         """Update live max drawdown."""
         return self.performance_calculator.update_live_max_drawdown(current_balance)
     
+    def get_drawdown_monitor_data(self) -> Dict[str, float]:
+        """Get drawdown data from DrawdownMonitor for external monitoring systems."""
+        try:
+            peak_balance = float(self._get_metadata('drawdown_peak_balance') or '0.0')
+            max_drawdown_pct = float(self._get_metadata('drawdown_max_drawdown_pct') or '0.0')
+            return {
+                'peak_balance': peak_balance,
+                'max_drawdown_percentage': max_drawdown_pct
+            }
+        except (ValueError, TypeError):
+            return {'peak_balance': 0.0, 'max_drawdown_percentage': 0.0}
+    
     def calculate_sharpe_ratio(self, days: int = 30) -> Optional[float]:
         """Calculate Sharpe ratio."""
         return self.performance_calculator.calculate_sharpe_ratio(days)
@@ -492,13 +504,13 @@ class TradingStats:
             
             return {
                 'token': upper_token,
-                'performance': perf_stats,
-                'open_positions': {
-                    'count': len(open_trades_for_token),
-                    'total_value': total_open_value,
-                    'total_unrealized_pnl': total_open_unrealized_pnl,
-                    'positions': open_positions_summary
-                },
+                'message': f"Statistics for {upper_token}",
+                'performance_summary': perf_stats,  # Expected key by formatting method
+                'performance': perf_stats,  # Legacy compatibility
+                'open_positions': open_positions_summary,  # Direct list as expected
+                'summary_total_trades': effective_total_trades,  # Expected by formatting method
+                'summary_total_unrealized_pnl': total_open_unrealized_pnl,  # Expected by formatting method
+                'current_open_orders_count': current_open_orders_for_token,  # Expected by formatting method
                 'summary': {
                     'total_trades': effective_total_trades,
                     'open_orders': current_open_orders_for_token,
@@ -558,19 +570,117 @@ class TradingStats:
             stats_text_parts.append(f"• Open Positions: {open_positions_count}")
             stats_text_parts.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)")
             stats_text_parts.append(f"• Days Active: {basic['days_active']}\n")
-
-            # Trading Performance
-            stats_text_parts.append(f"📈 <b>Trading Performance:</b>")
-            stats_text_parts.append(f"• Total Cycles: {perf['total_completed_cycles']}")
-            stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_winning_cycles']}/{perf['total_completed_cycles']})")
+            
+            # Performance Metrics
+            stats_text_parts.append(f"\n🏆 <b>Performance Metrics:</b>")
+            stats_text_parts.append(f"• Total Completed Trades: {basic['completed_trades']}")
+            stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_wins']}/{basic['completed_trades']})")
+            stats_text_parts.append(f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
             stats_text_parts.append(f"• Profit Factor: {perf['profit_factor']:.2f}")
             stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf['expectancy'])}")
             
-            return "\n".join(stats_text_parts)
+            stats_text_parts.append(f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf['largest_win'])}")
+            stats_text_parts.append(f"• Largest Losing Trade: {formatter.format_price_with_symbol(-perf['largest_loss'])}")
+
+            best_token_stats = perf.get('best_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0})
+            worst_token_stats = perf.get('worst_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0})
+            stats_text_parts.append(f"• Best Performing Token: {best_token_stats['name']} ({best_token_stats['pnl_percentage']:+.2f}%)")
+            stats_text_parts.append(f"• Worst Performing Token: {worst_token_stats['name']} ({worst_token_stats['pnl_percentage']:+.2f}%)")
+            
+            stats_text_parts.append(f"• Average Trade Duration: {perf.get('avg_trade_duration', 'N/A')}")
+            stats_text_parts.append(f"• Portfolio Max Drawdown: {risk.get('max_drawdown_live_percentage', 0.0):.2f}% <i>(Live)</i>")
+            
+            # Session Info
+            stats_text_parts.append(f"\n\n⏰ <b>Session Info:</b>")
+            stats_text_parts.append(f"• Bot Started: {basic['start_date']}")
+            stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
+            
+            return "\n".join(stats_text_parts).strip()
+                
+        except Exception as e:
+            logger.error(f"❌ Error formatting stats message: {e}", exc_info=True)
+            return f"""📊 <b>Trading Statistics</b>\n\n❌ <b>Error loading statistics</b>\n\n🔧 <b>Debug info:</b> {str(e)[:100]}"""
+
+    def format_token_stats_message(self, token: str) -> str:
+        """Format detailed statistics for a specific token."""
+        try:
+            from src.utils.token_display_formatter import get_formatter
+            formatter = get_formatter()
+            
+            token_stats_data = self.get_token_detailed_stats(token)
+            token_name = token_stats_data.get('token', token.upper())
+            
+            if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
+                return (
+                    f"📊 <b>{token_name} Statistics</b>\n\n"
+                    f"📭 No trading data found for {token_name}.\n\n"
+                    f"💡 To trade this token, try commands like:\n"
+                    f"   <code>/long {token_name} 100</code>\n"
+                    f"   <code>/short {token_name} 100</code>"
+                )
+
+            perf_summary = token_stats_data.get('performance_summary', {})
+            open_positions = token_stats_data.get('open_positions', [])
+            
+            parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
+
+            # Completed Trades Summary
+            parts.append("📈 <b>Completed Trades Summary:</b>")
+            if perf_summary.get('completed_trades', 0) > 0:
+                pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
+                entry_vol = perf_summary.get('completed_entry_volume', 0.0)
+                pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
+                
+                parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
+                parts.append(f"• {pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({pnl_pct:+.2f}%)")
+                parts.append(f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}W / {perf_summary.get('total_losses', 0)}L)")
+                parts.append(f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}")
+                parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
+                parts.append(f"• Avg Win: {formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
+                parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}")
+                parts.append(f"• Entry Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
+                parts.append(f"• Exit Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_exit_volume', 0.0))}")
+                parts.append(f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}")
+                parts.append(f"• Cancelled Cycles: {perf_summary.get('total_cancelled', 0)}")
+            else:
+                parts.append("• No completed trades for this token yet.")
+            parts.append("")
+
+            # Open Positions
+            parts.append("📉 <b>Current Open Positions:</b>")
+            if open_positions:
+                total_open_unrealized_pnl = token_stats_data.get('summary_total_unrealized_pnl', 0.0)
+                open_pnl_emoji = "🟢" if total_open_unrealized_pnl >= 0 else "🔴"
+                
+                for pos in open_positions:
+                    pos_side_emoji = "🟢" if pos.get('side') == 'long' else "🔴"
+                    pos_pnl_emoji = "🟢" if pos.get('unrealized_pnl', 0) >= 0 else "🔴"
+                    opened_at_str = "N/A"
+                    if pos.get('opened_at'):
+                        try:
+                            from datetime import datetime
+                            opened_at_dt = datetime.fromisoformat(pos['opened_at'])
+                            opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
+                        except:
+                            pass
+                    
+                    parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
+                    parts.append(f"    Entry: {formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
+                    parts.append(f"    {pos_pnl_emoji} Unrealized P&L: {formatter.format_price_with_symbol(pos.get('unrealized_pnl',0))}")
+                    parts.append(f"    Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
+                parts.append(f"  {open_pnl_emoji} <b>Total Open P&L: {formatter.format_price_with_symbol(total_open_unrealized_pnl)}</b>")
+            else:
+                parts.append("• No open positions for this token.")
+            parts.append("")
+
+            parts.append(f"📋 Open Orders (Exchange): {token_stats_data.get('current_open_orders_count', 0)}")
+            parts.append(f"💡 Use <code>/performance {token_name}</code> for another view including recent trades.")
+            
+            return "\n".join(parts)
             
         except Exception as e:
-            logger.error(f"❌ Error formatting stats message: {e}")
-            return f"❌ Error generating statistics: {str(e)}"
+            logger.error(f"❌ Error formatting token stats message for {token}: {e}", exc_info=True)
+            return f"❌ Error generating statistics for {token}: {str(e)[:100]}"
 
     # =============================================================================
     # CONVENIENCE METHODS & HIGH-LEVEL OPERATIONS

+ 1 - 1
trading_bot.py

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