浏览代码

Enhance trading statistics reporting in Telegram bot - Update daily, weekly, and monthly statistics to include performance metrics for days, weeks, and months with no trades. Refactor message formatting to ensure consistent display of trading activity, including summaries for periods without trades. Add tests to verify period stats consistency across different trading scenarios.

Carles Sentis 5 天之前
父节点
当前提交
81afc9a92a
共有 3 个文件被更改,包括 325 次插入73 次删除
  1. 78 39
      src/telegram_bot.py
  2. 105 34
      src/trading_stats.py
  3. 142 0
      tests/test_period_stats_consistency.py

+ 78 - 39
src/telegram_bot.py

@@ -2710,23 +2710,36 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
             
             total_pnl = 0
             total_trades = 0
+            trading_days = 0
             
             for day_stats in daily_stats:
-                pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
-                
-                daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
-                daily_text += f"   {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
-                daily_text += f"   🔄 Trades: {day_stats['trades']}\n\n"
-                
-                total_pnl += day_stats['pnl']
-                total_trades += day_stats['trades']
+                if day_stats['has_trades']:
+                    # Day with completed trades
+                    pnl_emoji = "🟢" if day_stats['pnl'] >= 0 else "🔴"
+                    daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
+                    daily_text += f"   {pnl_emoji} P&L: ${day_stats['pnl']:,.2f} ({day_stats['pnl_pct']:+.1f}%)\n"
+                    daily_text += f"   🔄 Trades: {day_stats['trades']}\n\n"
+                    
+                    total_pnl += day_stats['pnl']
+                    total_trades += day_stats['trades']
+                    trading_days += 1
+                else:
+                    # Day with no trades
+                    daily_text += f"📊 <b>{day_stats['date_formatted']}</b>\n"
+                    daily_text += f"   📭 No completed trades\n\n"
             
             # Add summary
-            total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            daily_text += f"💼 <b>10-Day Summary:</b>\n"
-            daily_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
-            daily_text += f"   🔄 Total Trades: {total_trades}\n"
-            daily_text += f"   📊 Avg per Day: ${total_pnl/len(daily_stats):,.2f}"
+            if trading_days > 0:
+                total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+                daily_text += f"💼 <b>10-Day Summary:</b>\n"
+                daily_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+                daily_text += f"   🔄 Total Trades: {total_trades}\n"
+                daily_text += f"   📈 Trading Days: {trading_days}/10\n"
+                daily_text += f"   📊 Avg per Trading Day: ${total_pnl/trading_days:,.2f}"
+            else:
+                daily_text += f"💼 <b>10-Day Summary:</b>\n"
+                daily_text += f"   📭 No completed trades in the last 10 days\n"
+                daily_text += f"   💡 Start trading to see daily performance!"
             
             await update.message.reply_text(daily_text.strip(), parse_mode='HTML')
             
@@ -2758,23 +2771,36 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
             
             total_pnl = 0
             total_trades = 0
+            trading_weeks = 0
             
             for week_stats in weekly_stats:
-                pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
-                
-                weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
-                weekly_text += f"   {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
-                weekly_text += f"   🔄 Trades: {week_stats['trades']}\n\n"
-                
-                total_pnl += week_stats['pnl']
-                total_trades += week_stats['trades']
+                if week_stats['has_trades']:
+                    # Week with completed trades
+                    pnl_emoji = "🟢" if week_stats['pnl'] >= 0 else "🔴"
+                    weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
+                    weekly_text += f"   {pnl_emoji} P&L: ${week_stats['pnl']:,.2f} ({week_stats['pnl_pct']:+.1f}%)\n"
+                    weekly_text += f"   🔄 Trades: {week_stats['trades']}\n\n"
+                    
+                    total_pnl += week_stats['pnl']
+                    total_trades += week_stats['trades']
+                    trading_weeks += 1
+                else:
+                    # Week with no trades
+                    weekly_text += f"📈 <b>{week_stats['week_formatted']}</b>\n"
+                    weekly_text += f"   📭 No completed trades\n\n"
             
             # Add summary
-            total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            weekly_text += f"💼 <b>10-Week Summary:</b>\n"
-            weekly_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
-            weekly_text += f"   🔄 Total Trades: {total_trades}\n"
-            weekly_text += f"   📊 Avg per Week: ${total_pnl/len(weekly_stats):,.2f}"
+            if trading_weeks > 0:
+                total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+                weekly_text += f"💼 <b>10-Week Summary:</b>\n"
+                weekly_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+                weekly_text += f"   🔄 Total Trades: {total_trades}\n"
+                weekly_text += f"   📈 Trading Weeks: {trading_weeks}/10\n"
+                weekly_text += f"   📊 Avg per Trading Week: ${total_pnl/trading_weeks:,.2f}"
+            else:
+                weekly_text += f"💼 <b>10-Week Summary:</b>\n"
+                weekly_text += f"   📭 No completed trades in the last 10 weeks\n"
+                weekly_text += f"   💡 Start trading to see weekly performance!"
             
             await update.message.reply_text(weekly_text.strip(), parse_mode='HTML')
             
@@ -2806,23 +2832,36 @@ Will trigger when {token} price moves {alarm['direction']} ${target_price:,.2f}
             
             total_pnl = 0
             total_trades = 0
+            trading_months = 0
             
             for month_stats in monthly_stats:
-                pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
-                
-                monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
-                monthly_text += f"   {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
-                monthly_text += f"   🔄 Trades: {month_stats['trades']}\n\n"
-                
-                total_pnl += month_stats['pnl']
-                total_trades += month_stats['trades']
+                if month_stats['has_trades']:
+                    # Month with completed trades
+                    pnl_emoji = "🟢" if month_stats['pnl'] >= 0 else "🔴"
+                    monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
+                    monthly_text += f"   {pnl_emoji} P&L: ${month_stats['pnl']:,.2f} ({month_stats['pnl_pct']:+.1f}%)\n"
+                    monthly_text += f"   🔄 Trades: {month_stats['trades']}\n\n"
+                    
+                    total_pnl += month_stats['pnl']
+                    total_trades += month_stats['trades']
+                    trading_months += 1
+                else:
+                    # Month with no trades
+                    monthly_text += f"📅 <b>{month_stats['month_formatted']}</b>\n"
+                    monthly_text += f"   📭 No completed trades\n\n"
             
             # Add summary
-            total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            monthly_text += f"💼 <b>10-Month Summary:</b>\n"
-            monthly_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
-            monthly_text += f"   🔄 Total Trades: {total_trades}\n"
-            monthly_text += f"   📊 Avg per Month: ${total_pnl/len(monthly_stats):,.2f}"
+            if trading_months > 0:
+                total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+                monthly_text += f"💼 <b>10-Month Summary:</b>\n"
+                monthly_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+                monthly_text += f"   🔄 Total Trades: {total_trades}\n"
+                monthly_text += f"   📈 Trading Months: {trading_months}/10\n"
+                monthly_text += f"   📊 Avg per Trading Month: ${total_pnl/trading_months:,.2f}"
+            else:
+                monthly_text += f"💼 <b>10-Month Summary:</b>\n"
+                monthly_text += f"   📭 No completed trades in the last 10 months\n"
+                monthly_text += f"   💡 Start trading to see monthly performance!"
             
             await update.message.reply_text(monthly_text.strip(), parse_mode='HTML')
             

+ 105 - 34
src/trading_stats.py

@@ -855,74 +855,145 @@ Please try again in a moment. If the issue persists, contact support.
                     stats['pnl_pct'] = 0.0
 
     def get_daily_stats(self, limit: int = 10) -> List[Dict[str, Any]]:
-        """Get daily performance stats for the last N days."""
+        """Get daily performance stats for the last N days (including days with no trades)."""
         # Ensure period stats are up to date
         self._update_period_stats()
         
-        # Get sorted daily stats (most recent first)
+        # Generate the last N calendar days
         daily_stats = []
-        for date_str, stats in sorted(self.data['daily_stats'].items(), reverse=True):
-            try:
-                date_obj = datetime.strptime(date_str, '%Y-%m-%d')
+        today = datetime.now().date()
+        
+        for i in range(limit):
+            # Calculate the date (going backwards from today)
+            target_date = today - timedelta(days=i)
+            date_str = target_date.strftime('%Y-%m-%d')
+            date_formatted = target_date.strftime('%m/%d')
+            
+            # Check if we have stats for this date
+            if date_str in self.data['daily_stats']:
+                stats = self.data['daily_stats'][date_str]
                 daily_stats.append({
                     'date': date_str,
-                    'date_formatted': date_obj.strftime('%m/%d'),
+                    'date_formatted': date_formatted,
                     'trades': stats['trades'],
                     'pnl': stats['pnl'],
                     'pnl_pct': stats['pnl_pct'],
-                    'volume': stats['volume']
+                    'volume': stats['volume'],
+                    'has_trades': True
+                })
+            else:
+                # No trades on this date
+                daily_stats.append({
+                    'date': date_str,
+                    'date_formatted': date_formatted,
+                    'trades': 0,
+                    'pnl': 0.0,
+                    'pnl_pct': 0.0,
+                    'volume': 0.0,
+                    'has_trades': False
                 })
-            except ValueError:
-                continue
         
-        return daily_stats[:limit]
+        return daily_stats
 
     def get_weekly_stats(self, limit: int = 10) -> List[Dict[str, Any]]:
-        """Get weekly performance stats for the last N weeks."""
+        """Get weekly performance stats for the last N weeks (including weeks with no trades)."""
         # Ensure period stats are up to date
         self._update_period_stats()
         
-        # Get sorted weekly stats (most recent first)
+        # Generate the last N calendar weeks
         weekly_stats = []
-        for week_str, stats in sorted(self.data['weekly_stats'].items(), reverse=True):
-            try:
-                # Parse week format YYYY-WXX
-                year, week_num = week_str.split('-W')
-                week_start = datetime.strptime(f"{year}-W{week_num.zfill(2)}-1", "%Y-W%U-%w")
-                week_end = week_start + timedelta(days=6)
-                
+        today = datetime.now().date()
+        
+        for i in range(limit):
+            # Calculate the start of the week (going backwards from today)
+            # Find the Monday of the target week
+            days_since_monday = today.weekday()
+            current_monday = today - timedelta(days=days_since_monday)
+            target_monday = current_monday - timedelta(weeks=i)
+            target_sunday = target_monday + timedelta(days=6)
+            
+            # Generate week string (YYYY-WXX format)
+            week_str = target_monday.strftime('%Y-W%U')
+            week_formatted = f"{target_monday.strftime('%m/%d')}-{target_sunday.strftime('%m/%d')}"
+            
+            # Check if we have stats for this week
+            if week_str in self.data['weekly_stats']:
+                stats = self.data['weekly_stats'][week_str]
                 weekly_stats.append({
                     'week': week_str,
-                    'week_formatted': f"{week_start.strftime('%m/%d')}-{week_end.strftime('%m/%d')}",
+                    'week_formatted': week_formatted,
                     'trades': stats['trades'],
                     'pnl': stats['pnl'],
                     'pnl_pct': stats['pnl_pct'],
-                    'volume': stats['volume']
+                    'volume': stats['volume'],
+                    'has_trades': True
+                })
+            else:
+                # No trades in this week
+                weekly_stats.append({
+                    'week': week_str,
+                    'week_formatted': week_formatted,
+                    'trades': 0,
+                    'pnl': 0.0,
+                    'pnl_pct': 0.0,
+                    'volume': 0.0,
+                    'has_trades': False
                 })
-            except (ValueError, IndexError):
-                continue
         
-        return weekly_stats[:limit]
+        return weekly_stats
 
     def get_monthly_stats(self, limit: int = 10) -> List[Dict[str, Any]]:
-        """Get monthly performance stats for the last N months."""
+        """Get monthly performance stats for the last N months (including months with no trades)."""
         # Ensure period stats are up to date
         self._update_period_stats()
         
-        # Get sorted monthly stats (most recent first)
+        # Generate the last N calendar months
         monthly_stats = []
-        for month_str, stats in sorted(self.data['monthly_stats'].items(), reverse=True):
-            try:
-                month_obj = datetime.strptime(month_str, '%Y-%m')
+        today = datetime.now().date()
+        current_month = today.replace(day=1)  # First day of current month
+        
+        for i in range(limit):
+            # Calculate the target month (going backwards from current month)
+            if i == 0:
+                target_month = current_month
+            else:
+                # Go back i months
+                year = current_month.year
+                month = current_month.month - i
+                
+                # Handle year rollover
+                while month <= 0:
+                    month += 12
+                    year -= 1
+                
+                target_month = datetime(year, month, 1).date()
+            
+            # Generate month string (YYYY-MM format)
+            month_str = target_month.strftime('%Y-%m')
+            month_formatted = target_month.strftime('%b %Y')
+            
+            # Check if we have stats for this month
+            if month_str in self.data['monthly_stats']:
+                stats = self.data['monthly_stats'][month_str]
                 monthly_stats.append({
                     'month': month_str,
-                    'month_formatted': month_obj.strftime('%b %Y'),
+                    'month_formatted': month_formatted,
                     'trades': stats['trades'],
                     'pnl': stats['pnl'],
                     'pnl_pct': stats['pnl_pct'],
-                    'volume': stats['volume']
+                    'volume': stats['volume'],
+                    'has_trades': True
+                })
+            else:
+                # No trades in this month
+                monthly_stats.append({
+                    'month': month_str,
+                    'month_formatted': month_formatted,
+                    'trades': 0,
+                    'pnl': 0.0,
+                    'pnl_pct': 0.0,
+                    'volume': 0.0,
+                    'has_trades': False
                 })
-            except ValueError:
-                continue
         
-        return monthly_stats[:limit] 
+        return monthly_stats 

+ 142 - 0
tests/test_period_stats_consistency.py

@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""
+Test script to verify period stats consistency (daily, weekly, monthly).
+This test ensures that the stats show consistent time periods regardless of trading activity.
+"""
+
+import sys
+import os
+import tempfile
+from datetime import datetime, timedelta
+
+# Add src directory to path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
+
+from trading_stats import TradingStats
+
+def test_period_stats_consistency():
+    """Test that period stats show consistent time periods."""
+    
+    # Create temporary stats file
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+        temp_file = f.name
+    
+    try:
+        # Initialize TradingStats
+        stats = TradingStats(temp_file)
+        stats.set_initial_balance(10000.0)
+        
+        print("Testing Period Stats Consistency")
+        print("=" * 40)
+        
+        # Test Case 1: No trades - should still show 10 periods
+        print("\n1. Testing with no trades:")
+        daily_stats = stats.get_daily_stats(10)
+        weekly_stats = stats.get_weekly_stats(10)
+        monthly_stats = stats.get_monthly_stats(10)
+        
+        print(f"   Daily periods: {len(daily_stats)} (should be 10)")
+        print(f"   Weekly periods: {len(weekly_stats)} (should be 10)")
+        print(f"   Monthly periods: {len(monthly_stats)} (should be 10)")
+        
+        # Verify all periods have has_trades = False
+        daily_no_trades = sum(1 for day in daily_stats if not day['has_trades'])
+        weekly_no_trades = sum(1 for week in weekly_stats if not week['has_trades'])
+        monthly_no_trades = sum(1 for month in monthly_stats if not month['has_trades'])
+        
+        print(f"   Days with no trades: {daily_no_trades}/10")
+        print(f"   Weeks with no trades: {weekly_no_trades}/10")
+        print(f"   Months with no trades: {monthly_no_trades}/10")
+        
+        # Test Case 2: Add some trades on specific days
+        print("\n2. Testing with selective trades:")
+        
+        # Add trade today
+        today = datetime.now()
+        stats.record_trade_with_enhanced_tracking("BTC/USDC", "buy", 0.1, 50000.0, "test1")
+        stats.record_trade_with_enhanced_tracking("BTC/USDC", "sell", 0.1, 51000.0, "test2")
+        
+        # Add trade 3 days ago
+        three_days_ago = today - timedelta(days=3)
+        stats.record_trade_with_enhanced_tracking("ETH/USDC", "buy", 1.0, 3000.0, "test3")
+        stats.record_trade_with_enhanced_tracking("ETH/USDC", "sell", 1.0, 3100.0, "test4")
+        
+        # Get updated stats
+        daily_stats = stats.get_daily_stats(10)
+        weekly_stats = stats.get_weekly_stats(10)
+        monthly_stats = stats.get_monthly_stats(10)
+        
+        print(f"   Daily periods: {len(daily_stats)} (should still be 10)")
+        print(f"   Weekly periods: {len(weekly_stats)} (should still be 10)")
+        print(f"   Monthly periods: {len(monthly_stats)} (should still be 10)")
+        
+        # Count periods with trades
+        daily_with_trades = sum(1 for day in daily_stats if day['has_trades'])
+        weekly_with_trades = sum(1 for week in weekly_stats if week['has_trades'])
+        monthly_with_trades = sum(1 for month in monthly_stats if month['has_trades'])
+        
+        print(f"   Days with trades: {daily_with_trades} (should be 2)")
+        print(f"   Weeks with trades: {weekly_with_trades}")
+        print(f"   Months with trades: {monthly_with_trades}")
+        
+        # Test Case 3: Check date consistency
+        print("\n3. Testing date consistency:")
+        today_str = datetime.now().strftime('%Y-%m-%d')
+        
+        # Today should be the first entry (index 0)
+        if daily_stats[0]['date'] == today_str:
+            print(f"   ✅ Today ({today_str}) is first in daily stats")
+        else:
+            print(f"   ❌ Today mismatch: expected {today_str}, got {daily_stats[0]['date']}")
+        
+        # Check that dates are consecutive going backwards
+        dates_correct = True
+        for i in range(1, len(daily_stats)):
+            expected_date = (datetime.now().date() - timedelta(days=i)).strftime('%Y-%m-%d')
+            if daily_stats[i]['date'] != expected_date:
+                dates_correct = False
+                print(f"   ❌ Date mismatch at index {i}: expected {expected_date}, got {daily_stats[i]['date']}")
+                break
+        
+        if dates_correct:
+            print(f"   ✅ All daily dates are consecutive and correct")
+        
+        # Test Case 4: Verify P&L calculations only for trading periods
+        print("\n4. Testing P&L calculations:")
+        total_pnl = 0
+        trading_days = 0
+        
+        for day in daily_stats:
+            if day['has_trades']:
+                total_pnl += day['pnl']
+                trading_days += 1
+                print(f"   Trading day {day['date_formatted']}: P&L ${day['pnl']:.2f}")
+            else:
+                # Should have zero P&L and zero trades
+                if day['pnl'] == 0 and day['trades'] == 0:
+                    print(f"   No-trade day {day['date_formatted']}: ✅ Correctly shows $0.00")
+                else:
+                    print(f"   No-trade day {day['date_formatted']}: ❌ Should show $0.00")
+        
+        print(f"   Total P&L from trading days: ${total_pnl:.2f}")
+        print(f"   Trading days count: {trading_days}")
+        
+        print("\n✅ Period stats consistency test completed!")
+        print("📊 All periods now show consistent time ranges")
+        print("🎯 Days/weeks/months with no trades are properly displayed")
+        return True
+        
+    except Exception as e:
+        print(f"❌ Test failed: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+    
+    finally:
+        # Clean up temp file
+        if os.path.exists(temp_file):
+            os.unlink(temp_file)
+
+if __name__ == "__main__":
+    success = test_period_stats_consistency()
+    exit(0 if success else 1)