Browse Source

Enhance risk metrics retrieval and performance reporting

- Updated RiskCommands to fetch risk metrics from the trading engine's stats, adding a check for availability and notifying users if metrics are not ready.
- Refactored TradingStats to streamline performance metrics retrieval, consolidating data fetching and improving clarity in the summary report.
- Adjusted the handling of best and worst trades by ROE, ensuring accurate representation in performance summaries.
Carles Sentis 2 days ago
parent
commit
dd2d743caf
3 changed files with 51 additions and 99 deletions
  1. 5 1
      src/commands/info/risk.py
  2. 45 97
      src/stats/trading_stats.py
  3. 1 1
      trading_bot.py

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

@@ -121,7 +121,11 @@ class RiskCommands(InfoCommandsBase):
                 message += f"Average Loss: {await self.formatter.format_price_with_symbol(performance_stats.get('average_loss', 0))}\n"
             
             # Add risk metrics
-            risk_metrics = self.trading_engine.get_risk_metrics()
+            risk_metrics = self.trading_engine.stats.get_risk_metrics()
+            if not risk_metrics:
+                await self._reply(update, "Risk metrics are not available yet.")
+                return
+            
             if risk_metrics:
                 message += f"\n�� <b>Risk Metrics:</b>\n"
                 # Drawdown

+ 45 - 97
src/stats/trading_stats.py

@@ -393,34 +393,13 @@ class TradingStats:
     def get_basic_stats(self, current_balance: Optional[float] = None) -> Dict[str, Any]:
         """Get basic trading statistics from DB, primarily using aggregated tables."""
         
-        # Get counts of open positions (trades that are not yet migrated)
+        # Get counts of open positions
         open_positions_count = self._get_open_positions_count_from_db()
 
-        # Get overall aggregated stats from token_stats table
-        query_token_stats_summary = """
-            SELECT 
-                SUM(total_realized_pnl) as total_pnl_from_cycles,
-                SUM(total_completed_cycles) as total_completed_cycles_sum,
-                MIN(first_cycle_closed_at) as overall_first_cycle_closed,
-                MAX(last_cycle_closed_at) as overall_last_cycle_closed,
-                SUM(total_entry_volume) as total_entry_volume_sum,
-                MAX(best_roe_percentage) as overall_best_roe,
-                MIN(worst_roe_percentage) as overall_worst_roe
-            FROM token_stats
-        """
-        token_stats_summary = self.db_manager._fetchone_query(query_token_stats_summary)
-
-        # Find the specific trades for best/worst ROE
-        best_roe_trade = self.db_manager._fetchone_query("SELECT token, best_roe_pnl, best_roe_percentage FROM token_stats ORDER BY best_roe_percentage DESC LIMIT 1")
-        worst_roe_trade = self.db_manager._fetchone_query("SELECT token, worst_roe_pnl, worst_roe_percentage FROM token_stats ORDER BY worst_roe_percentage ASC LIMIT 1")
-
-        total_pnl_from_cycles = token_stats_summary['total_pnl_from_cycles'] if token_stats_summary and token_stats_summary['total_pnl_from_cycles'] is not None else 0.0
-        total_completed_cycles_sum = token_stats_summary['total_completed_cycles_sum'] if token_stats_summary and token_stats_summary['total_completed_cycles_sum'] is not None else 0
-        total_entry_volume_sum = token_stats_summary['total_entry_volume_sum'] if token_stats_summary and token_stats_summary['total_entry_volume_sum'] is not None else 0.0
-
-        # Total trades considered as sum of completed cycles and currently open positions
-        total_trades_redefined = total_completed_cycles_sum + open_positions_count
+        # Get all performance metrics from the calculator
+        perf_metrics = self.get_performance_stats()
 
+        # Get additional metadata
         initial_balance_str = self._get_metadata('initial_balance')
         initial_balance = float(initial_balance_str) if initial_balance_str else 0.0
         
@@ -430,56 +409,32 @@ class TradingStats:
         
         # Get last activity timestamp
         last_activity_ts = None
-        last_activity_query = """
-            SELECT MAX(updated_at) as last_update
-            FROM trades
-            WHERE status IN ('position_opened', 'position_closed')
-        """
-        last_activity_row = self.db_manager._fetchone_query(last_activity_query)
+        last_activity_row = self.db_manager._fetchone_query(
+            "SELECT MAX(last_cycle_closed_at) as last_update FROM token_stats"
+        )
         if last_activity_row and last_activity_row['last_update']:
-            last_activity_ts = last_activity_row['last_update']
-            # Ensure timezone-aware
-            if isinstance(last_activity_ts, str):
-                last_activity_ts = datetime.fromisoformat(last_activity_ts)
-            if last_activity_ts.tzinfo is None:
-                last_activity_ts = last_activity_ts.replace(tzinfo=timezone.utc)
-
-        # Get last open trade timestamp
-        last_open_trade_query = """
-            SELECT MAX(updated_at) as last_update
-            FROM trades
-            WHERE status = 'position_opened'
-        """
-        last_open_trade_ts_row = self.db_manager._fetchone_query(last_open_trade_query)
-        if last_open_trade_ts_row and last_open_trade_ts_row['last_update']:
-            last_open_trade_ts = last_open_trade_ts_row['last_update']
-            # Ensure timezone-aware
-            if isinstance(last_open_trade_ts, str):
-                last_open_trade_ts = datetime.fromisoformat(last_open_trade_ts)
-            if last_open_trade_ts.tzinfo is None:
-                last_open_trade_ts = last_open_trade_ts.replace(tzinfo=timezone.utc)
-
-            # Now both datetimes are timezone-aware, we can compare them
-            if not last_activity_ts or last_open_trade_ts > last_activity_ts:
-                last_activity_ts = last_open_trade_ts
-
-        # Get drawdown info
-        _, max_drawdown_pct, drawdown_start_date = self.performance_calculator.get_live_max_drawdown()
+            last_activity_ts = datetime.fromisoformat(last_activity_row['last_update'])
+
+        # Best/Worst trades by ROE
+        best_roe_trade = self.db_manager._fetchone_query("SELECT token, best_roe_pnl, best_roe_percentage FROM token_stats ORDER BY best_roe_percentage DESC LIMIT 1")
+        worst_roe_trade = self.db_manager._fetchone_query("SELECT token, worst_roe_pnl, worst_roe_percentage FROM token_stats ORDER BY worst_roe_percentage ASC LIMIT 1")
+
+        total_pnl = perf_metrics.get('total_pnl', 0.0)
+        total_volume = perf_metrics.get('total_entry_volume', 0.0)
+        total_return_pct = (total_pnl / initial_balance * 100) if initial_balance > 0 else 0.0
 
         return {
-            'total_trades': total_trades_redefined,
-            'completed_trades': total_completed_cycles_sum,
             'initial_balance': initial_balance,
-            'total_pnl': total_pnl_from_cycles,
-            'total_volume': total_entry_volume_sum,
+            'total_pnl': total_pnl,
+            'total_volume': total_volume,
+            'total_return_pct': total_return_pct,
             'days_active': days_active,
             'start_date': start_date_obj.strftime('%Y-%m-%d'),
             'last_trade': last_activity_ts,
             'open_positions_count': open_positions_count,
+            'performance_metrics': perf_metrics,
             'best_roe_trade': best_roe_trade,
-            'worst_roe_trade': worst_roe_trade,
-            'max_drawdown_percentage': max_drawdown_pct,
-            'drawdown_start_date': drawdown_start_date
+            'worst_roe_trade': worst_roe_trade
         }
 
     def _get_open_positions_count_from_db(self) -> int:
@@ -656,68 +611,61 @@ class TradingStats:
 
         stats_text_parts = [
             f"📊 <b>Trading Performance Summary</b>",
-            f"• Current Balance: {await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val))} ({await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val) - initial_bal) if initial_bal > 0 else 'N/A'})",
+            f"• Current Balance: {await formatter.format_price_with_symbol(current_balance if current_balance is not None else (initial_bal + total_pnl_val))}",
             f"• Initial Balance: {await formatter.format_price_with_symbol(initial_bal)}",
-            f"• Balance Change: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)",
             f"• {pnl_emoji} Total P&L: {await formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)"
         ]
         
-        # Drawdown
-        max_drawdown_pct = basic_stats.get('max_drawdown_percentage', 0.0)
-        drawdown_start_date_iso = basic_stats.get('drawdown_start_date')
-        drawdown_date_str = ""
-        if drawdown_start_date_iso:
-            try:
-                drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
-                drawdown_date_str = f" (since {drawdown_date})"
-            except (ValueError, TypeError):
-                pass
-        stats_text_parts.append(f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}")
-        
         # Performance Metrics
         perf = basic_stats.get('performance_metrics', {})
         if perf:
             stats_text_parts.append("\n<b>Key Metrics:</b>")
-            stats_text_parts.append(f"• Trading Volume (Entry Vol.): {await formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
+
+            # Drawdown
+            max_drawdown_pct = perf.get('max_drawdown_pct', 0.0)
+            drawdown_start_date_iso = perf.get('drawdown_start_date')
+            drawdown_date_str = ""
+            if drawdown_start_date_iso:
+                try:
+                    drawdown_date = datetime.fromisoformat(drawdown_start_date_iso).strftime('%Y-%m-%d')
+                    drawdown_date_str = f" (since {drawdown_date})"
+                except (ValueError, TypeError):
+                    pass
+            stats_text_parts.append(f"• Max Drawdown: {max_drawdown_pct:.2f}%{drawdown_date_str}")
+
+            stats_text_parts.append(f"• Trading Volume: {await formatter.format_price_with_symbol(perf.get('total_entry_volume', 0.0))}")
             if perf.get('expectancy') is not None:
                 stats_text_parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(perf['expectancy'])}")
-            stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.2f}% ({perf.get('num_wins', 0)} wins)")
+            stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.2f}% ({perf.get('total_wins', 0)} wins)")
             stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
             
             # Largest Trades by P&L
             if perf.get('largest_win') is not None:
-                largest_win_pct_str = f" ({perf.get('largest_win_entry_pct', 0):.2f}%)" if perf.get('largest_win_entry_pct') is not None else ""
+                largest_win_roe = perf.get('largest_win_pct', 0.0)
                 largest_win_token = perf.get('largest_win_token', 'N/A')
-                stats_text_parts.append(f"• Largest Win (P&L): {await formatter.format_price_with_symbol(perf['largest_win'])}{largest_win_pct_str} ({largest_win_token})")
-            if perf.get('largest_loss') is not None:
-                largest_loss_pct_str = f" ({perf.get('largest_loss_entry_pct', 0):.2f}%)" if perf.get('largest_loss_entry_pct') is not None else ""
+                stats_text_parts.append(f"• Largest Win (P&L): {await formatter.format_price_with_symbol(perf['largest_win'])} ({largest_win_roe:+.2f}% ROE) ({largest_win_token})")
+            
+            if perf.get('largest_loss', 0.0) != 0:
+                largest_loss_roe = perf.get('largest_loss_pct', 0.0)
                 largest_loss_token = perf.get('largest_loss_token', 'N/A')
-                stats_text_parts.append(f"• Largest Loss (P&L): {await formatter.format_price_with_symbol(perf['largest_loss'])}{largest_loss_pct_str} ({largest_loss_token})")
+                stats_text_parts.append(f"• Largest Loss (P&L): {await formatter.format_price_with_symbol(perf['largest_loss'])} ({largest_loss_roe:+.2f}% ROE) ({largest_loss_token})")
 
             # Best/Worst by ROE from basic_stats
             best_roe_trade = basic_stats.get('best_roe_trade')
             worst_roe_trade = basic_stats.get('worst_roe_trade')
 
-            if best_roe_trade:
+            if best_roe_trade and best_roe_trade.get('token') != perf.get('largest_win_token'):
                 best_roe_pnl = best_roe_trade.get('best_roe_pnl', 0.0)
                 best_roe_pct = best_roe_trade.get('best_roe_percentage', 0.0)
                 best_roe_token = best_roe_trade.get('token', 'N/A')
                 stats_text_parts.append(f"• Best Trade (ROE): {await formatter.format_price_with_symbol(best_roe_pnl)} ({best_roe_pct:+.2f}%) ({best_roe_token})")
             
-            if worst_roe_trade:
+            if worst_roe_trade and worst_roe_trade.get('token') != perf.get('largest_loss_token'):
                 worst_roe_pnl = worst_roe_trade.get('worst_roe_pnl', 0.0)
                 worst_roe_pct = worst_roe_trade.get('worst_roe_percentage', 0.0)
                 worst_roe_token = worst_roe_trade.get('token', 'N/A')
                 stats_text_parts.append(f"• Worst Trade (ROE): {await formatter.format_price_with_symbol(worst_roe_pnl)} ({worst_roe_pct:+.2f}%) ({worst_roe_token})")
 
-        # Best/Worst Tokens
-        best_token_stats = basic_stats.get('best_token')
-        worst_token_stats = basic_stats.get('worst_token')
-        if best_token_stats:
-            stats_text_parts.append(f"• Best Token: {best_token_stats['name']} {await formatter.format_price_with_symbol(best_token_stats['pnl_value'])} ({best_token_stats['pnl_percentage']:+.2f}%)")
-        if worst_token_stats:
-            stats_text_parts.append(f"• Worst Token: {worst_token_stats['name']} {await formatter.format_price_with_symbol(worst_token_stats['pnl_value'])} ({worst_token_stats['pnl_percentage']:+.2f}%)")
-
         return "\n".join(stats_text_parts)
 
     async def format_token_stats_message(self, token: str) -> str:

+ 1 - 1
trading_bot.py

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