stats.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import logging
  2. from typing import Dict, Any, List, Optional
  3. from datetime import datetime, timedelta, timezone
  4. from telegram import Update
  5. from telegram.ext import ContextTypes
  6. from .base import InfoCommandsBase
  7. from src.utils.token_display_formatter import get_formatter
  8. from src.config.config import Config
  9. logger = logging.getLogger(__name__)
  10. class StatsCommands(InfoCommandsBase):
  11. """Handles all statistics-related commands."""
  12. async def _format_token_specific_stats_message(self, token_stats_data: Dict[str, Any], token_name: str) -> str:
  13. """Format detailed statistics for a specific token, matching the main /stats style."""
  14. formatter = get_formatter()
  15. if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
  16. return (
  17. f"📊 <b>{token_name} Statistics</b>\n\n"
  18. f"📭 No trading data found for {token_name}.\n\n"
  19. f"💡 To trade this token, try commands like:\n"
  20. f" <code>/long {token_name} 100</code>\n"
  21. f" <code>/short {token_name} 100</code>"
  22. )
  23. perf_summary = token_stats_data.get('performance_summary', {})
  24. open_positions = token_stats_data.get('open_positions', [])
  25. session = token_stats_data.get('session_info', {})
  26. # --- Account Overview ---
  27. account_lines = [
  28. f"💰 <b>{token_name.upper()} Account Overview:</b>",
  29. f"• Current Balance: {formatter.format_price_with_symbol(perf_summary.get('current_balance', 0.0))}",
  30. f"• Initial Balance: {formatter.format_price_with_symbol(perf_summary.get('initial_balance', 0.0))}",
  31. f"• Open Positions: {len(open_positions)}",
  32. ]
  33. total_pnl = perf_summary.get('total_pnl', 0.0)
  34. entry_vol = perf_summary.get('completed_entry_volume', 0.0)
  35. total_pnl_pct = (total_pnl / entry_vol * 100) if entry_vol > 0 else 0.0
  36. pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
  37. account_lines.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl)} ({total_pnl_pct:+.2f}%)")
  38. account_lines.append(f"• Days Active: {perf_summary.get('days_active', 0)}")
  39. # --- Performance Metrics ---
  40. perf_lines = [
  41. "🏆 <b>Performance Metrics:</b>",
  42. f"• Total Completed Trades: {perf_summary.get('completed_trades', 0)}",
  43. f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}/{perf_summary.get('completed_trades', 0)})",
  44. f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}",
  45. f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}",
  46. f"• Expectancy: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}",
  47. f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))} ({perf_summary.get('largest_win_pct', 0.0):+.2f}%)",
  48. f"• Largest Losing Trade: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}",
  49. f"• Best ROE Trade: {formatter.format_price_with_symbol(perf_summary.get('best_roe_trade', 0.0))} ({perf_summary.get('best_roe_trade_pct', 0.0):+.2f}%)",
  50. f"• Worst ROE Trade: {formatter.format_price_with_symbol(perf_summary.get('worst_roe_trade', 0.0))} ({perf_summary.get('worst_roe_trade_pct', 0.0):+.2f}%)",
  51. f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}",
  52. f"• Max Drawdown: {perf_summary.get('max_drawdown', 0.0):.2f}% <i>(Live)</i>",
  53. ]
  54. # --- Session Info ---
  55. session_lines = [
  56. "⏰ <b>Session Info:</b>",
  57. f"• Bot Started: {session.get('bot_started', 'N/A')}",
  58. f"• Stats Last Updated: {session.get('last_updated', 'N/A')}",
  59. ]
  60. # Combine all sections
  61. stats_text = (
  62. f"📊 <b>{token_name.upper()} Trading Statistics</b>\n\n" +
  63. "\n".join(account_lines) +
  64. "\n\n" +
  65. "\n".join(perf_lines) +
  66. "\n\n" +
  67. "\n".join(session_lines)
  68. )
  69. return stats_text.strip()
  70. async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  71. """Handle the /stats command. Shows overall stats or stats for a specific token."""
  72. try:
  73. if not self._is_authorized(update):
  74. await self._reply(update, "❌ Unauthorized access.")
  75. return
  76. stats = self.trading_engine.get_stats()
  77. if not stats:
  78. await self._reply(update, "❌ Trading stats not available.")
  79. return
  80. if context.args and len(context.args) > 0:
  81. # Token-specific stats
  82. token_name = context.args[0].upper()
  83. token_stats = stats.get_token_stats(token_name)
  84. if not token_stats:
  85. await self._reply(update, f"❌ No trading data found for {token_name}.")
  86. return
  87. stats_message = await self._format_token_specific_stats_message(token_stats, token_name)
  88. await self._reply(update, stats_message)
  89. return
  90. # Get current balance
  91. current_balance = self.trading_engine.get_balance()
  92. # Fix: If current_balance is a dict, extract the numeric value
  93. if isinstance(current_balance, dict):
  94. # Try common keys for USDC or total
  95. if 'USDC' in current_balance and isinstance(current_balance['USDC'], dict):
  96. current_balance = current_balance['USDC'].get('total', 0)
  97. elif 'total' in current_balance:
  98. current_balance = current_balance['total']
  99. else:
  100. # Fallback: try to find a float value in the dict
  101. for v in current_balance.values():
  102. if isinstance(v, (float, int)):
  103. current_balance = v
  104. break
  105. else:
  106. current_balance = 0
  107. # Get performance stats
  108. perf = stats.get_performance_stats()
  109. if not perf:
  110. await self._reply(update, "❌ Could not get performance stats.")
  111. return
  112. # Format the message
  113. formatter = get_formatter()
  114. stats_text_parts = ["📊 <b>Trading Statistics</b>\n"]
  115. # Account Overview
  116. stats_text_parts.append("💰 <b>Account Overview:</b>")
  117. stats_text_parts.append(f"• Current Balance: {formatter.format_price_with_symbol(current_balance)}")
  118. stats_text_parts.append(f"• Open Positions: {perf.get('open_positions', 0)}")
  119. stats_text_parts.append(f"• Total P&L: {formatter.format_price_with_symbol(perf.get('total_pnl', 0))}")
  120. # Performance Metrics
  121. stats_text_parts.append("\n🏆 <b>Performance Metrics:</b>")
  122. stats_text_parts.append(f"• Total Trades: {perf.get('total_trades', 0)}")
  123. stats_text_parts.append(f"• Win Rate: {perf.get('win_rate', 0.0):.1f}% ({perf.get('total_wins', 0)}/{perf.get('total_trades', 0)})")
  124. stats_text_parts.append(f"• Trading Volume: {formatter.format_price_with_symbol(perf.get('total_entry_volume', 0.0))}")
  125. stats_text_parts.append(f"• Profit Factor: {perf.get('profit_factor', 0.0):.2f}")
  126. stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf.get('expectancy', 0.0))}")
  127. # Largest Trades
  128. stats_text_parts.append("\n📈 <b>Largest Trades:</b>")
  129. 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')})")
  130. 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')})")
  131. # Best/Worst Tokens
  132. stats_text_parts.append("\n🏆 <b>Token Performance:</b>")
  133. 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}%)")
  134. 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}%)")
  135. # Risk Metrics
  136. stats_text_parts.append("\n⚠️ <b>Risk Metrics:</b>")
  137. stats_text_parts.append(f"• Max Drawdown: {perf.get('max_drawdown_pct', 0.0):.2f}%")
  138. # Session Info
  139. stats_text_parts.append("\n⏰ <b>Session Info:</b>")
  140. stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
  141. await self._reply(update, "\n".join(stats_text_parts))
  142. except Exception as e:
  143. logger.error(f"Error in stats command: {e}")
  144. await self._reply(update, f"❌ Error getting statistics: {str(e)}")