فهرست منبع

Enhance risk metrics reporting.

- Improved risk analysis messaging by safely formatting Sharpe ratio and maximum drawdown values.
- Added profit factor and win rate metrics to risk analysis output.
- Enhanced database schema to include entry volumes for largest winning and losing cycles.
- Updated performance calculations to include percentages for largest trades, improving insights into trading performance.
Carles Sentis 1 روز پیش
والد
کامیت
0afcc335de

+ 39 - 31
src/commands/info_commands.py

@@ -1300,18 +1300,25 @@ class InfoCommands:
                 )
                 return
             
+            # Get risk metric values with safe defaults
+            sharpe_ratio = risk_metrics.get('sharpe_ratio')
+            max_drawdown_pct = risk_metrics.get('max_drawdown_live_percentage', 0.0)
+            
+            # Format values safely
+            sharpe_str = f"{sharpe_ratio:.3f}" if sharpe_ratio is not None else "N/A"
+            
             # Format the risk analysis message
             risk_text = f"""
 📊 <b>Risk Analysis & Advanced Metrics</b>
 
 🎯 <b>Risk-Adjusted Performance:</b>
-• Sharpe Ratio: {risk_metrics['sharpe_ratio']:.3f}
-• Sortino Ratio: {risk_metrics['sortino_ratio']:.3f}
-• Annual Volatility: {risk_metrics['volatility']:.2f}%
+• Sharpe Ratio: {sharpe_str}
+• Profit Factor: {risk_metrics.get('profit_factor', 0):.2f}
+• Win Rate: {risk_metrics.get('win_rate', 0):.1f}%
 
 📉 <b>Drawdown Analysis:</b>
-• Maximum Drawdown: {risk_metrics['max_drawdown']:.2f}%
-• Value at Risk (95%): {risk_metrics['var_95']:.2f}%
+• Maximum Drawdown: {max_drawdown_pct:.2f}%
+• Max Consecutive Losses: {risk_metrics.get('max_consecutive_losses', 0)}
 
 💰 <b>Portfolio Context:</b>
 • Current Balance: ${current_balance:,.2f}
@@ -1323,50 +1330,51 @@ class InfoCommands:
 """
             
             # Add interpretive guidance
-            sharpe = risk_metrics['sharpe_ratio']
-            if sharpe > 2.0:
-                risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
-            elif sharpe > 1.0:
-                risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
-            elif sharpe > 0.5:
-                risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
-            elif sharpe > 0:
-                risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
+            if sharpe_ratio is not None:
+                if sharpe_ratio > 2.0:
+                    risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
+                elif sharpe_ratio > 1.0:
+                    risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
+                elif sharpe_ratio > 0.5:
+                    risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
+                elif sharpe_ratio > 0:
+                    risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
+                else:
+                    risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
             else:
-                risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
+                risk_text += "• ⚪ <b>Insufficient data</b> for Sharpe ratio calculation\n"
             
-            max_dd = risk_metrics['max_drawdown']
-            if max_dd < 5:
+            if max_drawdown_pct < 5:
                 risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
-            elif max_dd < 15:
+            elif max_drawdown_pct < 15:
                 risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
-            elif max_dd < 30:
+            elif max_drawdown_pct < 30:
                 risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
             else:
                 risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
             
-            volatility = risk_metrics['volatility']
-            if volatility < 10:
-                risk_text += "• 🟢 <b>Low</b> portfolio volatility (< 10%)\n"
-            elif volatility < 25:
-                risk_text += "• 🟡 <b>Moderate</b> portfolio volatility (< 25%)\n"
-            elif volatility < 50:
-                risk_text += "• 🟠 <b>High</b> portfolio volatility (< 50%)\n"
+            # Add profit factor interpretation
+            profit_factor = risk_metrics.get('profit_factor', 0)
+            if profit_factor > 2.0:
+                risk_text += "• 🟢 <b>Excellent</b> profit factor (> 2.0)\n"
+            elif profit_factor > 1.5:
+                risk_text += "• 🟡 <b>Good</b> profit factor (> 1.5)\n"
+            elif profit_factor > 1.0:
+                risk_text += "• 🟠 <b>Profitable</b> but low profit factor (> 1.0)\n"
             else:
-                risk_text += "• 🔴 <b>Very High</b> portfolio volatility (> 50%)\n"
+                risk_text += "• 🔴 <b>Unprofitable</b> trading strategy (< 1.0)\n"
             
             risk_text += f"""
 
 💡 <b>Risk Definitions:</b>
 • <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
-• <b>Sortino Ratio:</b> Return / downside volatility (focuses on bad volatility)
+• <b>Profit Factor:</b> Total winning trades / Total losing trades
+• <b>Win Rate:</b> Percentage of profitable trades
 • <b>Max Drawdown:</b> Largest peak-to-trough decline
-• <b>VaR 95%:</b> Maximum expected loss 95% of the time
-• <b>Volatility:</b> Annualized standard deviation of returns
+• <b>Max Consecutive Losses:</b> Longest streak of losing trades
 
 📈 <b>Data Based On:</b>
 • Completed Trades: {html.escape(str(basic_stats['completed_trades']))}
-• Daily Balance Records: {html.escape(str(stats.get_daily_balance_record_count()))}
 • Trading Period: {html.escape(str(basic_stats['days_active']))} days
 
 🔄 Use /stats for trading performance metrics

+ 2 - 0
src/migrations/migrate_db.py

@@ -60,6 +60,8 @@ TOKEN_STATS_TABLE_SCHEMA = {
     "sum_of_losing_pnl": "REAL DEFAULT 0.0",
     "largest_winning_cycle_pnl": "REAL DEFAULT 0.0",
     "largest_losing_cycle_pnl": "REAL DEFAULT 0.0",
+    "largest_winning_cycle_entry_volume": "REAL DEFAULT 0.0",
+    "largest_losing_cycle_entry_volume": "REAL DEFAULT 0.0",
     "first_cycle_closed_at": "TEXT",
     "last_cycle_closed_at": "TEXT",
     "total_cancelled_cycles": "INTEGER DEFAULT 0",

+ 23 - 4
src/stats/aggregation_manager.py

@@ -78,9 +78,9 @@ class AggregationManager:
             INSERT INTO token_stats (
                 token, total_realized_pnl, total_completed_cycles, winning_cycles, losing_cycles,
                 total_entry_volume, total_exit_volume, sum_of_winning_pnl, sum_of_losing_pnl,
-                largest_winning_cycle_pnl, largest_losing_cycle_pnl, 
+                largest_winning_cycle_pnl, largest_losing_cycle_pnl, largest_winning_cycle_entry_volume, largest_losing_cycle_entry_volume,
                 first_cycle_closed_at, last_cycle_closed_at, total_duration_seconds, updated_at
-            ) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
+            ) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
             ON CONFLICT(token) DO UPDATE SET
                 total_realized_pnl = total_realized_pnl + excluded.total_realized_pnl,
                 total_completed_cycles = total_completed_cycles + 1,
@@ -90,8 +90,22 @@ class AggregationManager:
                 total_exit_volume = total_exit_volume + excluded.total_exit_volume,
                 sum_of_winning_pnl = sum_of_winning_pnl + excluded.sum_of_winning_pnl,
                 sum_of_losing_pnl = sum_of_losing_pnl + excluded.sum_of_losing_pnl,
-                largest_winning_cycle_pnl = MAX(largest_winning_cycle_pnl, excluded.largest_winning_cycle_pnl),
-                largest_losing_cycle_pnl = MAX(largest_losing_cycle_pnl, excluded.largest_losing_cycle_pnl),
+                largest_winning_cycle_pnl = 
+                    CASE WHEN excluded.largest_winning_cycle_pnl > largest_winning_cycle_pnl 
+                         THEN excluded.largest_winning_cycle_pnl 
+                         ELSE largest_winning_cycle_pnl END,
+                largest_losing_cycle_pnl = 
+                    CASE WHEN excluded.largest_losing_cycle_pnl > largest_losing_cycle_pnl 
+                         THEN excluded.largest_losing_cycle_pnl 
+                         ELSE largest_losing_cycle_pnl END,
+                largest_winning_cycle_entry_volume = 
+                    CASE WHEN excluded.largest_winning_cycle_pnl > largest_winning_cycle_pnl 
+                         THEN excluded.largest_winning_cycle_entry_volume 
+                         ELSE largest_winning_cycle_entry_volume END,
+                largest_losing_cycle_entry_volume = 
+                    CASE WHEN excluded.largest_losing_cycle_pnl > largest_losing_cycle_pnl 
+                         THEN excluded.largest_losing_cycle_entry_volume 
+                         ELSE largest_losing_cycle_entry_volume END,
                 first_cycle_closed_at = MIN(first_cycle_closed_at, excluded.first_cycle_closed_at),
                 last_cycle_closed_at = MAX(last_cycle_closed_at, excluded.last_cycle_closed_at),
                 total_duration_seconds = total_duration_seconds + excluded.total_duration_seconds,
@@ -102,9 +116,14 @@ class AggregationManager:
         win_pnl_contrib = realized_pnl if realized_pnl > 0 else 0.0
         loss_pnl_contrib = abs(realized_pnl) if realized_pnl < 0 else 0.0
         
+        # For largest winning/losing, we only consider them if this is the new largest
+        largest_win_entry_volume = entry_value if realized_pnl > 0 else 0.0
+        largest_loss_entry_volume = entry_value if realized_pnl < 0 else 0.0
+        
         self.db._execute_query(token_upsert_query, (
             token, realized_pnl, is_win, is_loss, entry_value, exit_value,
             win_pnl_contrib, loss_pnl_contrib, win_pnl_contrib, loss_pnl_contrib,
+            largest_win_entry_volume, largest_loss_entry_volume,
             closed_at_str, closed_at_str, duration_seconds, now_iso
         ))
 

+ 2 - 0
src/stats/database_manager.py

@@ -187,6 +187,8 @@ class DatabaseManager:
                 sum_of_losing_pnl REAL DEFAULT 0.0,
                 largest_winning_cycle_pnl REAL DEFAULT 0.0,
                 largest_losing_cycle_pnl REAL DEFAULT 0.0,
+                largest_winning_cycle_entry_volume REAL DEFAULT 0.0,
+                largest_losing_cycle_entry_volume REAL DEFAULT 0.0,
                 first_cycle_closed_at TEXT,
                 last_cycle_closed_at TEXT,
                 total_cancelled_cycles INTEGER DEFAULT 0,

+ 37 - 2
src/stats/performance_calculator.py

@@ -65,6 +65,21 @@ class PerformanceCalculator:
         largest_winning_cycle = max((t.get('largest_winning_cycle_pnl', 0) for t in token_stats), default=0)
         largest_losing_cycle = max((t.get('largest_losing_cycle_pnl', 0) for t in token_stats), default=0)
         
+        # Get entry volume for largest winning/losing trades to calculate percentages
+        largest_winning_entry_volume = 0.0
+        largest_losing_entry_volume = 0.0
+        
+        for token in token_stats:
+            if token.get('largest_winning_cycle_pnl', 0) == largest_winning_cycle and largest_winning_cycle > 0:
+                largest_winning_entry_volume = token.get('largest_winning_cycle_entry_volume', 0)
+                
+            if token.get('largest_losing_cycle_pnl', 0) == largest_losing_cycle and largest_losing_cycle > 0:
+                largest_losing_entry_volume = token.get('largest_losing_cycle_entry_volume', 0)
+        
+        # Calculate percentages for largest trades
+        largest_winning_percentage = (largest_winning_cycle / largest_winning_entry_volume * 100) if largest_winning_entry_volume > 0 else 0
+        largest_losing_percentage = (largest_losing_cycle / largest_losing_entry_volume * 100) if largest_losing_entry_volume > 0 else 0
+        
         # Average trade duration
         average_trade_duration_seconds = total_duration_seconds / total_completed_cycles if total_completed_cycles > 0 else 0
         average_trade_duration_formatted = self._format_duration(average_trade_duration_seconds)
@@ -76,8 +91,12 @@ class PerformanceCalculator:
         # Calculate best and worst performing tokens
         best_token_name = "N/A"
         best_token_pnl_pct = 0.0
+        best_token_volume = 0.0
+        best_token_pnl_value = 0.0
         worst_token_name = "N/A" 
         worst_token_pnl_pct = 0.0
+        worst_token_volume = 0.0
+        worst_token_pnl_value = 0.0
         
         for token in token_stats:
             if token.get('total_completed_cycles', 0) > 0:
@@ -90,10 +109,14 @@ class PerformanceCalculator:
                     if best_token_name == "N/A" or pnl_pct > best_token_pnl_pct:
                         best_token_name = token['token']
                         best_token_pnl_pct = pnl_pct
+                        best_token_volume = entry_volume
+                        best_token_pnl_value = total_pnl
                     
                     if worst_token_name == "N/A" or pnl_pct < worst_token_pnl_pct:
                         worst_token_name = token['token']
                         worst_token_pnl_pct = pnl_pct
+                        worst_token_volume = entry_volume
+                        worst_token_pnl_value = total_pnl
 
         return {
             'total_realized_pnl': total_realized_pnl,
@@ -117,6 +140,8 @@ class PerformanceCalculator:
             'largest_losing_cycle': largest_losing_cycle,
             'largest_win': largest_winning_cycle,  # Alias for compatibility
             'largest_loss': largest_losing_cycle,  # Alias for compatibility
+            'largest_winning_percentage': largest_winning_percentage,
+            'largest_losing_percentage': largest_losing_percentage,
             'total_wins': total_winning_cycles,  # Alias for compatibility
             'total_losses': total_losing_cycles,  # Alias for compatibility
             'roi_percentage': roi_percentage,
@@ -126,8 +151,18 @@ class PerformanceCalculator:
             'average_trade_duration_seconds': average_trade_duration_seconds,
             '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}
+            'best_performing_token': {
+                'name': best_token_name, 
+                'pnl_percentage': best_token_pnl_pct, 
+                'volume': best_token_volume,
+                'pnl_value': best_token_pnl_value
+            },
+            'worst_performing_token': {
+                'name': worst_token_name, 
+                'pnl_percentage': worst_token_pnl_pct,
+                'volume': worst_token_volume,
+                'pnl_value': worst_token_pnl_value
+            }
         }
 
     def get_token_performance(self, limit: int = 20) -> List[Dict[str, Any]]:

+ 29 - 9
src/stats/trading_stats.py

@@ -449,6 +449,15 @@ class TradingStats:
                 sum_winning_pnl = token_agg_stats.get('sum_of_winning_pnl', 0.0)
                 sum_losing_pnl = token_agg_stats.get('sum_of_losing_pnl', 0.0)
                 
+                # Calculate percentages for largest trades
+                largest_win_pnl = token_agg_stats.get('largest_winning_cycle_pnl', 0.0)
+                largest_loss_pnl = token_agg_stats.get('largest_losing_cycle_pnl', 0.0)
+                largest_win_entry_volume = token_agg_stats.get('largest_winning_cycle_entry_volume', 0.0)
+                largest_loss_entry_volume = token_agg_stats.get('largest_losing_cycle_entry_volume', 0.0)
+                
+                largest_win_percentage = (largest_win_pnl / largest_win_entry_volume * 100) if largest_win_entry_volume > 0 else 0.0
+                largest_loss_percentage = (largest_loss_pnl / largest_loss_entry_volume * 100) if largest_loss_entry_volume > 0 else 0.0
+                
                 perf_stats.update({
                     'completed_trades': total_cycles,
                     'total_pnl': token_agg_stats.get('total_realized_pnl', 0.0),
@@ -456,8 +465,10 @@ class TradingStats:
                     'profit_factor': (sum_winning_pnl / sum_losing_pnl) if sum_losing_pnl > 0 else float('inf') if sum_winning_pnl > 0 else 0.0,
                     'avg_win': (sum_winning_pnl / winning_cycles) if winning_cycles > 0 else 0.0,
                     'avg_loss': (sum_losing_pnl / losing_cycles) if losing_cycles > 0 else 0.0,
-                    'largest_win': token_agg_stats.get('largest_winning_cycle_pnl', 0.0),
-                    'largest_loss': token_agg_stats.get('largest_losing_cycle_pnl', 0.0),
+                    'largest_win': largest_win_pnl,
+                    'largest_loss': largest_loss_pnl,
+                    'largest_win_percentage': largest_win_percentage,
+                    'largest_loss_percentage': largest_loss_percentage,
                     'total_wins': winning_cycles,
                     'total_losses': losing_cycles,
                     'completed_entry_volume': token_agg_stats.get('total_entry_volume', 0.0),
@@ -579,13 +590,17 @@ class TradingStats:
             stats_text_parts.append(f"• Profit Factor: {perf['profit_factor']:.2f}")
             stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf['expectancy'])}")
             
-            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'])}")
+            largest_win_pct_str = f" ({perf.get('largest_winning_percentage', 0):+.2f}%)" if perf.get('largest_winning_percentage', 0) != 0 else ""
+            largest_loss_pct_str = f" ({perf.get('largest_losing_percentage', 0):+.2f}%)" if perf.get('largest_losing_percentage', 0) != 0 else ""
+            
+            stats_text_parts.append(f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str}")
+            stats_text_parts.append(f"• Largest Losing Trade: {formatter.format_price_with_symbol(-perf['largest_loss'])}{largest_loss_pct_str}")
 
-            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}%)")
+            best_token_stats = perf.get('best_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
+            worst_token_stats = perf.get('worst_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0, 'volume': 0.0, 'pnl_value': 0.0})
+            
+            stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
+            stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({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>")
@@ -637,7 +652,12 @@ class TradingStats:
                 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))}")
+                
+                # Format largest trades with percentages
+                largest_win_pct_str = f" ({perf_summary.get('largest_win_percentage', 0):+.2f}%)" if perf_summary.get('largest_win_percentage', 0) != 0 else ""
+                largest_loss_pct_str = f" ({perf_summary.get('largest_loss_percentage', 0):+.2f}%)" if perf_summary.get('largest_loss_percentage', 0) != 0 else ""
+                
+                parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
                 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')}")

+ 1 - 1
trading_bot.py

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