Browse Source

Enhance trading statistics functionality - Add new performance metrics including total wins, total losses, and expectancy. Improve error handling in stats message formatting to ensure robustness when no trades are recorded. Introduce a test script to verify the correctness of the stats command functionality.

Carles Sentis 5 days ago
parent
commit
05f44ba672
2 changed files with 169 additions and 34 deletions
  1. 58 34
      src/trading_stats.py
  2. 111 0
      tests/test_stats_fix.py

+ 58 - 34
src/trading_stats.py

@@ -211,7 +211,10 @@ class TradingStats:
                 'largest_win': 0,
                 'largest_loss': 0,
                 'consecutive_wins': 0,
-                'consecutive_losses': 0
+                'consecutive_losses': 0,
+                'total_wins': 0,
+                'total_losses': 0,
+                'expectancy': 0
             }
         
         # Separate wins and losses
@@ -353,51 +356,72 @@ class TradingStats:
     
     def format_stats_message(self, current_balance: float = None) -> str:
         """Format stats for Telegram display."""
-        stats = self.get_comprehensive_stats(current_balance)
-        
-        basic = stats['basic']
-        perf = stats['performance']
-        risk = stats['risk']
-        
-        message = f"""
+        try:
+            stats = self.get_comprehensive_stats(current_balance)
+            
+            basic = stats['basic']
+            perf = stats['performance']
+            risk = stats['risk']
+            
+            message = f"""
 📊 <b>Trading Statistics</b>
 
 💰 <b>Balance Overview</b>
-• Initial: ${basic['initial_balance']:,.2f}
-• Current: ${stats['current_balance']:,.2f}
-• Total P&L: ${basic['total_pnl']:,.2f}
-• Total Return: {stats['total_return']:.2f}%
+• Initial: ${basic.get('initial_balance', 0):,.2f}
+• Current: ${stats.get('current_balance', 0):,.2f}
+• Total P&L: ${basic.get('total_pnl', 0):,.2f}
+• Total Return: {stats.get('total_return', 0):.2f}%
 
 📈 <b>Trading Activity</b>
-• Total Trades: {basic['total_trades']}
-• Buy Orders: {basic['buy_trades']}
-• Sell Orders: {basic['sell_trades']}
-• Days Active: {basic['days_active']}
+• Total Trades: {basic.get('total_trades', 0)}
+• Buy Orders: {basic.get('buy_trades', 0)}
+• Sell Orders: {basic.get('sell_trades', 0)}
+• Days Active: {basic.get('days_active', 0)}
 
 🏆 <b>Performance Metrics</b>
-• Win Rate: {perf['win_rate']:.1f}%
-• Profit Factor: {perf['profit_factor']:.2f}
-• Avg Win: ${perf['avg_win']:.2f}
-• Avg Loss: ${perf['avg_loss']:.2f}
-• Expectancy: ${perf['expectancy']:.2f}
+• Win Rate: {perf.get('win_rate', 0):.1f}%
+• Profit Factor: {perf.get('profit_factor', 0):.2f}
+• Avg Win: ${perf.get('avg_win', 0):.2f}
+• Avg Loss: ${perf.get('avg_loss', 0):.2f}
+• Expectancy: ${perf.get('expectancy', 0):.2f}
 
 📊 <b>Risk Metrics</b>
-• Sharpe Ratio: {risk['sharpe_ratio']:.2f}
-• Sortino Ratio: {risk['sortino_ratio']:.2f}
-• Max Drawdown: {risk['max_drawdown']:.2f}%
-• Volatility: {risk['volatility']:.2f}%
-• VaR (95%): {risk['var_95']:.2f}%
+• Sharpe Ratio: {risk.get('sharpe_ratio', 0):.2f}
+• Sortino Ratio: {risk.get('sortino_ratio', 0):.2f}
+• Max Drawdown: {risk.get('max_drawdown', 0):.2f}%
+• Volatility: {risk.get('volatility', 0):.2f}%
+• VaR (95%): {risk.get('var_95', 0):.2f}%
 
 🎯 <b>Best/Worst</b>
-• Largest Win: ${perf['largest_win']:.2f}
-• Largest Loss: ${perf['largest_loss']:.2f}
-• Max Consecutive Wins: {perf['consecutive_wins']}
-• Max Consecutive Losses: {perf['consecutive_losses']}
+• Largest Win: ${perf.get('largest_win', 0):.2f}
+• Largest Loss: ${perf.get('largest_loss', 0):.2f}
+• Max Consecutive Wins: {perf.get('consecutive_wins', 0)}
+• Max Consecutive Losses: {perf.get('consecutive_losses', 0)}
 
-📅 <b>Since:</b> {basic['start_date']}
-        """
-        
-        return message.strip()
+📅 <b>Since:</b> {basic.get('start_date', 'N/A')}
+            """
+            
+            return message.strip()
+            
+        except Exception as e:
+            # Fallback message if stats calculation fails
+            return f"""
+📊 <b>Trading Statistics</b>
+
+⚠️ <b>Error loading statistics:</b> {str(e)}
+
+💡 <b>This might happen if:</b>
+• No trading data has been recorded yet
+• Stats file is corrupted or missing
+• Balance information is not available
+
+🔄 <b>Try again after:</b>
+• Making a trade with /long or /short
+• Checking your balance with /balance
+• Restarting the bot
+
+📱 <b>Current Status:</b> Statistics will be available once trading activity begins.
+            """.strip()
     
     def get_recent_trades(self, limit: int = 10) -> List[Dict[str, Any]]:
         """Get recent trades."""

+ 111 - 0
tests/test_stats_fix.py

@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""
+Test script to verify the stats command fix
+"""
+
+import sys
+from pathlib import Path
+
+# Add the project root and src directory to the path
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+sys.path.insert(0, str(project_root / 'src'))
+
+from trading_stats import TradingStats
+
+def test_stats_fix():
+    """Test that stats work with no trades and with trades."""
+    print("🧪 Testing Stats Fix")
+    print("=" * 40)
+    
+    try:
+        # Test 1: New stats instance (no trades)
+        print("📊 Test 1: Empty stats (no trades)")
+        stats = TradingStats(stats_file="test_stats.json")
+        
+        # Try to format stats message
+        message = stats.format_stats_message(1000.0)
+        print("✅ Empty stats message generated successfully")
+        print(f"📝 Message length: {len(message)} characters")
+        
+        # Test 2: Performance stats with no trades
+        print("\n📊 Test 2: Performance stats with no trades")
+        perf_stats = stats.get_performance_stats()
+        
+        # Check that expectancy key exists
+        if 'expectancy' in perf_stats:
+            print(f"✅ Expectancy key exists: {perf_stats['expectancy']}")
+        else:
+            print("❌ Expectancy key missing!")
+            return False
+        
+        # Check all required keys
+        required_keys = [
+            'win_rate', 'profit_factor', 'avg_win', 'avg_loss',
+            'largest_win', 'largest_loss', 'consecutive_wins',
+            'consecutive_losses', 'total_wins', 'total_losses', 'expectancy'
+        ]
+        
+        missing_keys = [key for key in required_keys if key not in perf_stats]
+        if missing_keys:
+            print(f"❌ Missing keys: {missing_keys}")
+            return False
+        else:
+            print("✅ All required performance keys present")
+        
+        # Test 3: Add some sample trades
+        print("\n📊 Test 3: Stats with sample trades")
+        stats.record_trade("BTC/USDC:USDC", "buy", 0.001, 45000.0, "test1")
+        stats.record_trade("BTC/USDC:USDC", "sell", 0.001, 46000.0, "test2")
+        
+        # Try to format stats message with trades
+        message_with_trades = stats.format_stats_message(1010.0)
+        print("✅ Stats message with trades generated successfully")
+        
+        # Test performance stats with trades
+        perf_stats_with_trades = stats.get_performance_stats()
+        print(f"✅ Win rate: {perf_stats_with_trades['win_rate']:.1f}%")
+        print(f"✅ Expectancy: ${perf_stats_with_trades['expectancy']:.2f}")
+        
+        # Test 4: Error handling
+        print("\n📊 Test 4: Error handling")
+        try:
+            # This should not fail due to the safe .get() access
+            comprehensive_stats = stats.get_comprehensive_stats(1010.0)
+            print("✅ Comprehensive stats generated successfully")
+        except Exception as e:
+            print(f"❌ Comprehensive stats failed: {e}")
+            return False
+        
+        print("\n🎉 All stats tests passed!")
+        print("\n📝 Sample stats message:")
+        print("-" * 40)
+        print(message[:200] + "..." if len(message) > 200 else message)
+        print("-" * 40)
+        
+        # Clean up test file
+        import os
+        try:
+            os.remove("test_stats.json")
+            print("\n🧹 Test file cleaned up")
+        except:
+            pass
+        
+        return True
+        
+    except Exception as e:
+        print(f"💥 Test failed with error: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+
+if __name__ == "__main__":
+    success = test_stats_fix()
+    
+    if success:
+        print("\n🎉 Stats fix test PASSED!")
+        print("\n✅ The /stats command should now work properly")
+        sys.exit(0)
+    else:
+        print("\n💥 Stats fix test FAILED!")
+        sys.exit(1)