浏览代码

Enhance trading statistics retrieval in StatsCommands. Updated balance extraction logic to handle various formats, improved performance metrics display, and refined message formatting for better clarity. Added error handling for performance stats retrieval and updated session information to include current timestamp.

Carles Sentis 1 周之前
父节点
当前提交
4a09c1a16c
共有 5 个文件被更改,包括 283 次插入311 次删除
  1. 55 51
      src/commands/info/stats.py
  2. 2 1
      src/migrations/migrate_db.py
  3. 104 94
      src/stats/aggregation_manager.py
  4. 121 164
      src/stats/performance_calculator.py
  5. 1 1
      trading_bot.py

+ 55 - 51
src/commands/info/stats.py

@@ -1,6 +1,6 @@
 import logging
 from typing import Dict, Any, List, Optional
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from telegram import Update
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
@@ -99,64 +99,68 @@ class StatsCommands(InfoCommandsBase):
                 await self._reply(update, stats_message)
                 return
 
-            # --- Old format for overall stats ---
+            # Get current balance
+            current_balance = self.trading_engine.get_balance()
+            # Fix: If current_balance is a dict, extract the numeric value
+            if isinstance(current_balance, dict):
+                # Try common keys for USDC or total
+                if 'USDC' in current_balance and isinstance(current_balance['USDC'], dict):
+                    current_balance = current_balance['USDC'].get('total', 0)
+                elif 'total' in current_balance:
+                    current_balance = current_balance['total']
+                else:
+                    # Fallback: try to find a float value in the dict
+                    for v in current_balance.values():
+                        if isinstance(v, (float, int)):
+                            current_balance = v
+                            break
+                    else:
+                        current_balance = 0
+
+            # Get performance stats
+            perf = stats.get_performance_stats()
+            if not perf:
+                await self._reply(update, "❌ Could not get performance stats.")
+                return
+
+            # Format the message
             formatter = get_formatter()
-            s = stats.get_basic_stats()
-            perf = stats.get_basic_stats()
-            session = {
-                "bot_started": s.get("start_date", "N/A"),
-                "last_updated": s.get("last_trade", "N/A"),
-            }
+            stats_text_parts = ["📊 <b>Trading Statistics</b>\n"]
 
             # Account Overview
-            account_lines = [
-                "💰 <b>Account Overview:</b>",
-                f"• Current Balance: {formatter.format_price_with_symbol(s.get('current_balance', 0.0))}",
-                f"• Initial Balance: {formatter.format_price_with_symbol(s.get('initial_balance', 0.0))}",
-                f"• Open Positions: {s.get('open_positions', 0)}",
-            ]
-            total_pnl = s.get('total_pnl', 0.0)
-            total_pnl_pct = s.get('total_pnl_pct', 0.0)
-            pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            account_lines.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl)} ({total_pnl_pct:+.2f}%)")
-            account_lines.append(f"• Days Active: {s.get('days_active', 0)}")
+            stats_text_parts.append("💰 <b>Account Overview:</b>")
+            stats_text_parts.append(f"• Current Balance: {formatter.format_price_with_symbol(current_balance)}")
+            stats_text_parts.append(f"• Open Positions: {perf.get('open_positions', 0)}")
+            stats_text_parts.append(f"• Total P&L: {formatter.format_price_with_symbol(perf.get('total_pnl', 0))}")
 
             # Performance Metrics
-            perf_lines = [
-                "🏆 <b>Performance Metrics:</b>",
-                f"• Total Completed Trades: {perf.get('completed_trades', 0)}",
-                f"• Win Rate: {perf.get('win_rate', 0.0):.1f}% ({perf.get('total_wins', 0)}/{perf.get('completed_trades', 0)})",
-                f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf.get('entry_volume', 0.0))}",
-                f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}",
-                f"• Expectancy: {formatter.format_price_with_symbol(perf.get('expectancy', 0.0))}",
-                f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf.get('largest_win', 0.0))} ({perf.get('largest_win_pct', 0.0):+.2f}%) ({perf.get('largest_win_token', '')})",
-                f"• Largest Losing Trade: {formatter.format_price_with_symbol(perf.get('largest_loss', 0.0))} ({perf.get('largest_loss_token', '')})",
-                f"• Worst ROE Trade: {formatter.format_price_with_symbol(perf.get('worst_roe_trade', 0.0))} ({perf.get('worst_roe_trade_pct', 0.0):+.2f}%) ({perf.get('worst_roe_trade_token', '')})",
-                f"• Best Token: {perf.get('best_token', '')} {formatter.format_price_with_symbol(perf.get('best_token_pnl', 0.0))} ({perf.get('best_token_pct', 0.0):+.2f}%)",
-                f"• Worst Token: {perf.get('worst_token', '')} {formatter.format_price_with_symbol(perf.get('worst_token_pnl', 0.0))} ({perf.get('worst_token_pct', 0.0):+.2f}%)",
-                f"• Average Trade Duration: {perf.get('avg_trade_duration', 'N/A')}",
-                f"• Portfolio Max Drawdown: {perf.get('max_drawdown', 0.0):.2f}% <i>(Live)</i>",
-            ]
+            stats_text_parts.append("\n🏆 <b>Performance Metrics:</b>")
+            stats_text_parts.append(f"• Total Trades: {perf.get('total_trades', 0)}")
+            stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.1f}% ({perf.get('total_wins', 0)}/{perf.get('total_trades', 0)})")
+            stats_text_parts.append(f"• Trading Volume: {formatter.format_price_with_symbol(perf.get('total_entry_volume', 0.0))}")
+            stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
+            stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf.get('expectancy', 0.0))}")
+
+            # Largest Trades
+            stats_text_parts.append("\n📈 <b>Largest Trades:</b>")
+            stats_text_parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf.get('largest_win', 0.0))} ({perf.get('largest_win_pct', 0.0):+.2f}%) ({perf.get('largest_win_token', 'N/A')})")
+            stats_text_parts.append(f"• Largest Loss: {formatter.format_price_with_symbol(perf.get('largest_loss', 0.0))} ({perf.get('largest_loss_pct', 0.0):+.2f}%) ({perf.get('largest_loss_token', 'N/A')})")
+
+            # Best/Worst Tokens
+            stats_text_parts.append("\n🏆 <b>Token Performance:</b>")
+            stats_text_parts.append(f"• Best Token: {perf.get('best_token', 'N/A')} {formatter.format_price_with_symbol(perf.get('best_token_pnl', 0.0))} ({perf.get('best_token_pct', 0.0):+.2f}%)")
+            stats_text_parts.append(f"• Worst Token: {perf.get('worst_token', 'N/A')} {formatter.format_price_with_symbol(perf.get('worst_token_pnl', 0.0))} ({perf.get('worst_token_pct', 0.0):+.2f}%)")
+
+            # Risk Metrics
+            stats_text_parts.append("\n⚠️ <b>Risk Metrics:</b>")
+            stats_text_parts.append(f"• Max Drawdown: {perf.get('max_drawdown_pct', 0.0):.2f}%")
 
             # Session Info
-            session_lines = [
-                "⏰ <b>Session Info:</b>",
-                f"• Bot Started: {session.get('bot_started', 'N/A')}",
-                f"• Stats Last Updated: {session.get('last_updated', 'N/A')}",
-            ]
-
-            # Combine all sections
-            stats_text = (
-                "📊 <b>Trading Statistics</b>\n\n" +
-                "\n".join(account_lines) +
-                "\n\n" +
-                "\n".join(perf_lines) +
-                "\n\n" +
-                "\n".join(session_lines)
-            )
+            stats_text_parts.append("\n⏰ <b>Session Info:</b>")
+            stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
 
-            await self._reply(update, stats_text.strip())
+            await self._reply(update, "\n".join(stats_text_parts))
 
         except Exception as e:
             logger.error(f"Error in stats command: {e}")
-            await self._reply(update, "❌ Error retrieving trading statistics.")
+            await self._reply(update, f"❌ Error getting statistics: {str(e)}")

+ 2 - 1
src/migrations/migrate_db.py

@@ -67,7 +67,8 @@ TOKEN_STATS_TABLE_SCHEMA = {
     "last_cycle_closed_at": "TEXT",
     "total_cancelled_cycles": "INTEGER DEFAULT 0",
     "total_duration_seconds": "INTEGER DEFAULT 0",
-    "updated_at": "TEXT DEFAULT CURRENT_TIMESTAMP"
+    "updated_at": "TEXT DEFAULT CURRENT_TIMESTAMP",
+    "roe_percentage": "REAL DEFAULT 0.0"
 }
 
 def get_existing_columns(conn: sqlite3.Connection, table_name: str) -> list[str]:

+ 104 - 94
src/stats/aggregation_manager.py

@@ -54,101 +54,111 @@ class AggregationManager:
         except Exception as e:
             logger.error(f"Unexpected error migrating trade {trade_lifecycle_id} to aggregate stats: {e}", exc_info=True)
 
-    def _migrate_closed_position(self, trade_data: Dict[str, Any], token: str, now_iso: str):
+    def _migrate_closed_position(self, trade_data: Dict[str, Any], token: str, now_iso: str) -> None:
         """Migrate a closed position to aggregated stats."""
-        realized_pnl = trade_data.get('realized_pnl', 0.0)
-        entry_value = trade_data.get('value', 0.0)
-        exit_value = entry_value + realized_pnl
-        closed_at_str = trade_data.get('position_closed_at', now_iso)
-        closed_at_dt = datetime.fromisoformat(closed_at_str)
-        date_str = closed_at_dt.strftime('%Y-%m-%d')
-
-        # Calculate duration if timestamps are available
-        opened_at_str = trade_data.get('position_opened_at')
-        duration_seconds = 0
-        if opened_at_str and closed_at_str:
-            try:
-                opened_at_dt = datetime.fromisoformat(opened_at_str)
-                duration_seconds = (closed_at_dt - opened_at_dt).total_seconds()
-            except Exception:
-                duration_seconds = 0
-
-        # Get ROE directly from exchange data
-        info_data = trade_data.get('info', {})
-        position_info = info_data.get('position', {})
-        roe_raw = position_info.get('returnOnEquity')
-        roe_percentage = float(roe_raw) * 100 if roe_raw is not None else 0.0
-
-        # Update token_stats
-        token_upsert_query = """
-            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_entry_volume, largest_losing_cycle_entry_volume,
-                first_cycle_closed_at, last_cycle_closed_at, total_duration_seconds, roe_percentage, updated_at
-            ) 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,
-                winning_cycles = winning_cycles + excluded.winning_cycles,
-                losing_cycles = losing_cycles + excluded.losing_cycles,
-                total_entry_volume = total_entry_volume + excluded.total_entry_volume,
-                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 = 
-                    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,
-                roe_percentage = excluded.roe_percentage,
-                updated_at = excluded.updated_at
-        """
-        is_win = 1 if realized_pnl > 0 else 0
-        is_loss = 1 if realized_pnl < 0 else 0
-        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, roe_percentage, now_iso
-        ))
-
-        # Update daily_aggregated_stats
-        daily_upsert_query = """
-            INSERT INTO daily_aggregated_stats (
-                date, token, realized_pnl, completed_cycles, entry_volume, exit_volume
-            ) VALUES (?, ?, ?, 1, ?, ?) 
-            ON CONFLICT(date, token) DO UPDATE SET
-                realized_pnl = realized_pnl + excluded.realized_pnl,
-                completed_cycles = completed_cycles + 1,
-                entry_volume = entry_volume + excluded.entry_volume,
-                exit_volume = exit_volume + excluded.exit_volume
-        """
-        self.db._execute_query(daily_upsert_query, (
-            date_str, token, realized_pnl, entry_value, exit_value
-        ))
-        logger.info(f"Aggregated stats for closed trade lifecycle ({token}). PNL: {realized_pnl:.2f}")
+        try:
+            # Extract trade data
+            realized_pnl = trade_data.get('realized_pnl', 0.0)
+            entry_value = trade_data.get('value', 0.0)
+            exit_value = entry_value + realized_pnl  # Calculate exit value from entry + P&L
+            is_win = realized_pnl > 0
+            is_loss = realized_pnl < 0
+            
+            # Get timestamps
+            opened_at = trade_data.get('position_opened_at')
+            closed_at = trade_data.get('position_closed_at')
+            
+            if not opened_at or not closed_at:
+                logger.warning(f"Missing timestamps for trade {trade_data.get('trade_lifecycle_id')}")
+                return
+            
+            # Calculate duration
+            opened_dt = datetime.fromisoformat(opened_at)
+            closed_dt = datetime.fromisoformat(closed_at)
+            duration_seconds = int((closed_dt - opened_dt).total_seconds())
+            
+            # Calculate ROE percentage
+            roe_percentage = 0.0
+            if entry_value > 0:
+                roe_percentage = (realized_pnl / entry_value) * 100
+            
+            # Format timestamps for SQL
+            opened_at_str = opened_at
+            closed_at_str = closed_at
+            
+            # Upsert query for token_stats
+            token_upsert_query = """
+                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,
+                    first_cycle_closed_at, last_cycle_closed_at,
+                    total_duration_seconds, largest_winning_cycle_entry_volume,
+                    largest_losing_cycle_entry_volume, roe_percentage, updated_at
+                ) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+                ON CONFLICT(token) DO UPDATE SET
+                    total_realized_pnl = total_realized_pnl + ?,
+                    total_completed_cycles = total_completed_cycles + 1,
+                    winning_cycles = winning_cycles + ?,
+                    losing_cycles = losing_cycles + ?,
+                    total_entry_volume = total_entry_volume + ?,
+                    total_exit_volume = total_exit_volume + ?,
+                    sum_of_winning_pnl = sum_of_winning_pnl + ?,
+                    sum_of_losing_pnl = sum_of_losing_pnl + ?,
+                    largest_winning_cycle_pnl = CASE 
+                        WHEN ? > largest_winning_cycle_pnl THEN ?
+                        ELSE largest_winning_cycle_pnl
+                    END,
+                    largest_losing_cycle_pnl = CASE 
+                        WHEN ? < largest_losing_cycle_pnl THEN ?
+                        ELSE largest_losing_cycle_pnl
+                    END,
+                    first_cycle_closed_at = CASE 
+                        WHEN first_cycle_closed_at IS NULL OR ? < first_cycle_closed_at 
+                        THEN ? ELSE first_cycle_closed_at 
+                    END,
+                    last_cycle_closed_at = CASE 
+                        WHEN last_cycle_closed_at IS NULL OR ? > last_cycle_closed_at 
+                        THEN ? ELSE last_cycle_closed_at 
+                    END,
+                    total_duration_seconds = total_duration_seconds + ?,
+                    largest_winning_cycle_entry_volume = CASE 
+                        WHEN ? > largest_winning_cycle_pnl THEN ?
+                        ELSE largest_winning_cycle_entry_volume
+                    END,
+                    largest_losing_cycle_entry_volume = CASE 
+                        WHEN ? < largest_losing_cycle_pnl THEN ?
+                        ELSE largest_losing_cycle_entry_volume
+                    END,
+                    roe_percentage = CASE 
+                        WHEN ? > roe_percentage THEN ?
+                        ELSE roe_percentage
+                    END,
+                    updated_at = ?
+            """
+            
+            # Execute the upsert
+            self.db._execute_query(token_upsert_query, (
+                token, realized_pnl, is_win, is_loss, entry_value, exit_value,
+                realized_pnl if is_win else 0, realized_pnl if is_loss else 0,
+                realized_pnl if is_win else 0, realized_pnl if is_loss else 0,
+                closed_at_str, closed_at_str, duration_seconds, entry_value if is_win else 0,
+                entry_value if is_loss else 0, roe_percentage, now_iso,
+                # For the UPDATE part
+                realized_pnl, is_win, is_loss, entry_value, exit_value,
+                realized_pnl if is_win else 0, realized_pnl if is_loss else 0,
+                realized_pnl if is_win else 0, realized_pnl if is_win else 0,
+                realized_pnl if is_loss else 0, realized_pnl if is_loss else 0,
+                closed_at_str, closed_at_str, closed_at_str, closed_at_str,
+                duration_seconds, realized_pnl if is_win else 0, entry_value if is_win else 0,
+                realized_pnl if is_loss else 0, entry_value if is_loss else 0,
+                roe_percentage, roe_percentage, now_iso
+            ))
+            
+        except Exception as e:
+            logger.error(f"Error migrating closed position to aggregated stats: {e}")
+            raise
 
     def _migrate_cancelled_position(self, trade_data: Dict[str, Any], token: str, now_iso: str):
         """Migrate a cancelled position to aggregated stats."""

+ 121 - 164
src/stats/performance_calculator.py

@@ -40,172 +40,129 @@ class PerformanceCalculator:
             return f"{int(days)}d {int(hours)}h"
 
     def get_performance_stats(self) -> Dict[str, Any]:
-        """Get comprehensive trading performance statistics."""
-        # Get token-level stats
-        token_stats = self.db._fetch_query("SELECT * FROM token_stats ORDER BY total_realized_pnl DESC")
-        
-        # Calculate overall aggregated metrics
-        total_realized_pnl = sum(t.get('total_realized_pnl', 0) for t in token_stats)
-        total_completed_cycles = sum(t.get('total_completed_cycles', 0) for t in token_stats)
-        total_winning_cycles = sum(t.get('winning_cycles', 0) for t in token_stats)
-        total_losing_cycles = sum(t.get('losing_cycles', 0) for t in token_stats)
-        total_cancelled_cycles = sum(t.get('total_cancelled_cycles', 0) for t in token_stats)
-        total_entry_volume = sum(t.get('total_entry_volume', 0) for t in token_stats)
-        total_exit_volume = sum(t.get('total_exit_volume', 0) for t in token_stats)
-        sum_of_winning_pnl = sum(t.get('sum_of_winning_pnl', 0) for t in token_stats)
-        sum_of_losing_pnl = sum(t.get('sum_of_losing_pnl', 0) for t in token_stats)
-        total_duration_seconds = sum(t.get('total_duration_seconds', 0) for t in token_stats)
-
-        # Calculate derived metrics
-        win_rate = (total_winning_cycles / total_completed_cycles * 100) if total_completed_cycles > 0 else 0
-        average_win_amount = sum_of_winning_pnl / total_winning_cycles if total_winning_cycles > 0 else 0
-        average_loss_amount = sum_of_losing_pnl / total_losing_cycles if total_losing_cycles > 0 else 0
-        profit_factor = sum_of_winning_pnl / sum_of_losing_pnl if sum_of_losing_pnl > 0 else float('inf') if sum_of_winning_pnl > 0 else 0
-        expectancy = (total_realized_pnl / total_completed_cycles) if total_completed_cycles > 0 else 0
-        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
-        largest_winning_token = "N/A"
-        largest_losing_token = "N/A"
-        
-        # Track largest trades by ROE for alternative rankings
-        largest_winning_roe = 0.0
-        largest_losing_roe = 0.0
-        largest_winning_roe_token = "N/A"
-        largest_losing_roe_token = "N/A"
-        largest_winning_roe_pnl = 0.0
-        largest_losing_roe_pnl = 0.0
-        
-        for token in token_stats:
-            # Track largest absolute dollar amounts
-            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)
-                largest_winning_token = token['token']
-                
-            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)
-                largest_losing_token = token['token']
-            
-            # Track largest ROE-based trades
-            winning_pnl = token.get('largest_winning_cycle_pnl', 0)
-            winning_entry_volume = token.get('largest_winning_cycle_entry_volume', 0)
-            if winning_pnl > 0 and winning_entry_volume > 0:
-                winning_roe = (winning_pnl / winning_entry_volume) * 100
-                if winning_roe > largest_winning_roe:
-                    largest_winning_roe = winning_roe
-                    largest_winning_roe_token = token['token']
-                    largest_winning_roe_pnl = winning_pnl
-            
-            losing_pnl = token.get('largest_losing_cycle_pnl', 0)
-            losing_entry_volume = token.get('largest_losing_cycle_entry_volume', 0)
-            if losing_pnl > 0 and losing_entry_volume > 0:
-                losing_roe = (losing_pnl / losing_entry_volume) * 100
-                if losing_roe > largest_losing_roe:
-                    largest_losing_roe = losing_roe
-                    largest_losing_roe_token = token['token']
-                    largest_losing_roe_pnl = losing_pnl
-        
-        # 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)
-
-        # ROI calculation
-        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 based on total P&L
-        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:
-                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
+        """Get performance stats."""
+        try:
+            # Get all token stats
+            token_stats = self.db._fetch_query(
+                "SELECT * FROM token_stats", 
+                ()
+            )
+            
+            # Get open positions
+            open_positions = self.db._fetch_query(
+                "SELECT * FROM trades WHERE status = 'position_opened'",
+                ()
+            )
+            
+            # Initialize performance metrics
+            total_trades = 0
+            total_wins = 0
+            total_losses = 0
+            total_pnl = 0.0
+            total_entry_volume = 0.0
+            total_exit_volume = 0.0
+            largest_win = 0.0
+            largest_loss = 0.0
+            largest_win_token = "N/A"
+            largest_loss_token = "N/A"
+            largest_win_pct = 0.0
+            largest_loss_pct = 0.0
+            best_token_name = "N/A"
+            best_token_pnl_value = 0.0
+            best_token_pnl_pct = 0.0
+            best_token_volume = 0.0
+            worst_token_name = "N/A"
+            worst_token_pnl_value = 0.0
+            worst_token_pnl_pct = 0.0
+            worst_token_volume = 0.0
+            
+            # 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)
                     
-                    # Best token = highest total P&L
-                    if best_token_name == "N/A" or total_pnl > best_token_pnl_value:
-                        best_token_name = token['token']
-                        best_token_pnl_pct = pnl_pct
-                        best_token_volume = entry_volume
-                        best_token_pnl_value = total_pnl
+                    # Track largest trades
+                    token_largest_win = token.get('largest_winning_cycle_pnl', 0)
+                    token_largest_loss = token.get('largest_losing_cycle_pnl', 0)
                     
-                    # Worst token = lowest total P&L
-                    if worst_token_name == "N/A" or total_pnl < worst_token_pnl_value:
-                        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,
-            'total_completed_cycles': total_completed_cycles,
-            'total_winning_cycles': total_winning_cycles,
-            'total_losing_cycles': total_losing_cycles,
-            'total_cancelled_cycles': total_cancelled_cycles,
-            '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
-            '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,
-            '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,
-            'avg_trade_duration': average_trade_duration_formatted,  # Alias for compatibility
-            '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
-            },
-            'largest_winning_token': largest_winning_token,
-            'largest_losing_token': largest_losing_token,
-            'largest_winning_roe': largest_winning_roe,
-            'largest_losing_roe': largest_losing_roe,
-            'largest_winning_roe_token': largest_winning_roe_token,
-            'largest_losing_roe_token': largest_losing_roe_token,
-            'largest_winning_roe_pnl': largest_winning_roe_pnl,
-            'largest_losing_roe_pnl': largest_losing_roe_pnl
-        }
+                    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
+                    
+                    if 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
+                    
+                    # Track best/worst tokens
+                    token_pnl = token.get('total_realized_pnl', 0)
+                    token_volume = token.get('total_entry_volume', 0)
+                    
+                    if token_volume > 0:
+                        token_pnl_pct = (token_pnl / token_volume) * 100
+                        
+                        if best_token_name == "N/A" or token_pnl > best_token_pnl_value:
+                            best_token_name = token['token']
+                            best_token_pnl_value = token_pnl
+                            best_token_pnl_pct = token_pnl_pct
+                            best_token_volume = token_volume
+                        
+                        if worst_token_name == "N/A" or token_pnl < worst_token_pnl_value:
+                            worst_token_name = token['token']
+                            worst_token_pnl_value = token_pnl
+                            worst_token_pnl_pct = token_pnl_pct
+                            worst_token_volume = token_volume
+            
+            # Calculate win rate and profit factor
+            win_rate = (total_wins / total_trades * 100) if total_trades > 0 else 0
+            profit_factor = (total_pnl / abs(total_losses)) if total_losses < 0 else float('inf') if total_pnl > 0 else 0
+            
+            # Calculate expectancy
+            avg_win = total_pnl / total_wins if total_wins > 0 else 0
+            avg_loss = total_losses / total_losses if total_losses > 0 else 0
+            expectancy = (avg_win * (win_rate/100)) - (abs(avg_loss) * (1 - win_rate/100))
+            
+            # Get max drawdown
+            max_drawdown, max_drawdown_pct = self.get_live_max_drawdown()
+            
+            return {
+                'total_trades': total_trades,
+                'total_wins': total_wins,
+                'total_losses': total_losses,
+                'win_rate': win_rate,
+                'total_pnl': total_pnl,
+                'total_entry_volume': total_entry_volume,
+                'total_exit_volume': total_exit_volume,
+                'profit_factor': profit_factor,
+                'expectancy': expectancy,
+                'largest_win': largest_win,
+                'largest_loss': largest_loss,
+                'largest_win_token': largest_win_token,
+                'largest_loss_token': largest_loss_token,
+                'largest_win_pct': largest_win_pct,
+                'largest_loss_pct': largest_loss_pct,
+                'best_token': best_token_name,
+                'best_token_pnl': best_token_pnl_value,
+                'best_token_pct': best_token_pnl_pct,
+                'best_token_volume': best_token_volume,
+                'worst_token': worst_token_name,
+                'worst_token_pnl': worst_token_pnl_value,
+                'worst_token_pct': worst_token_pnl_pct,
+                'worst_token_volume': worst_token_volume,
+                'max_drawdown': max_drawdown,
+                'max_drawdown_pct': max_drawdown_pct,
+                'open_positions': len(open_positions)
+            }
+            
+        except Exception as e:
+            logger.error(f"Error getting performance stats: {e}")
+            return {}
 
     def get_token_performance(self, limit: int = 20) -> List[Dict[str, Any]]:
         """Get performance stats by token, sorted by total P&L (dollar amount)."""

+ 1 - 1
trading_bot.py

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