|
@@ -10,8 +10,77 @@ logger = logging.getLogger(__name__)
|
|
|
class StatsCommands(InfoCommandsBase):
|
|
|
"""Handles all statistics-related commands."""
|
|
|
|
|
|
+ async def _format_token_specific_stats_message(self, token_stats_data: Dict[str, Any], token_name: str) -> str:
|
|
|
+ """Format detailed statistics for a specific token."""
|
|
|
+ if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
|
|
|
+ return (
|
|
|
+ f"📊 <b>{token_name} Statistics</b>\n\n"
|
|
|
+ f"📭 No trading data found for {token_name}.\n\n"
|
|
|
+ f"💡 To trade this token, try commands like:\n"
|
|
|
+ f" <code>/long {token_name} 100</code>\n"
|
|
|
+ f" <code>/short {token_name} 100</code>"
|
|
|
+ )
|
|
|
+
|
|
|
+ perf_summary = token_stats_data.get('performance_summary', {})
|
|
|
+ open_positions = token_stats_data.get('open_positions', [])
|
|
|
+
|
|
|
+ parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
|
|
|
+
|
|
|
+ # Completed Trades Summary
|
|
|
+ parts.append("📈 <b>Completed Trades Summary:</b>")
|
|
|
+ if perf_summary.get('completed_trades', 0) > 0:
|
|
|
+ pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
|
|
|
+ entry_vol = perf_summary.get('completed_entry_volume', 0.0)
|
|
|
+ pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
|
|
|
+
|
|
|
+ parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
|
|
|
+ parts.append(f"• {pnl_emoji} Realized P&L: ${perf_summary.get('total_pnl', 0.0):,.2f} ({pnl_pct:+.2f}%)")
|
|
|
+ parts.append(f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}W / {perf_summary.get('total_losses', 0)}L)")
|
|
|
+ parts.append(f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}")
|
|
|
+ parts.append(f"• Expectancy: ${perf_summary.get('expectancy', 0.0):,.2f}")
|
|
|
+ parts.append(f"• Avg Win: ${perf_summary.get('avg_win', 0.0):,.2f} | Avg Loss: ${perf_summary.get('avg_loss', 0.0):,.2f}")
|
|
|
+ parts.append(f"• Largest Win: ${perf_summary.get('largest_win', 0.0):,.2f} | Largest Loss: ${perf_summary.get('largest_loss', 0.0):,.2f}")
|
|
|
+ parts.append(f"• Entry Volume: ${perf_summary.get('completed_entry_volume', 0.0):,.2f}")
|
|
|
+ parts.append(f"• Exit Volume: ${perf_summary.get('completed_exit_volume', 0.0):,.2f}")
|
|
|
+ parts.append(f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}")
|
|
|
+ parts.append(f"• Cancelled Cycles: {perf_summary.get('total_cancelled', 0)}")
|
|
|
+ else:
|
|
|
+ parts.append("• No completed trades for this token yet.")
|
|
|
+ parts.append("")
|
|
|
+
|
|
|
+ # Open Positions for this token
|
|
|
+ parts.append("📉 <b>Current Open Positions:</b>")
|
|
|
+ if open_positions:
|
|
|
+ total_open_unrealized_pnl = token_stats_data.get('summary_total_unrealized_pnl', 0.0)
|
|
|
+ open_pnl_emoji = "🟢" if total_open_unrealized_pnl >= 0 else "🔴"
|
|
|
+
|
|
|
+ for pos in open_positions:
|
|
|
+ pos_side_emoji = "🟢" if pos.get('side') == 'long' else "🔴"
|
|
|
+ pos_pnl_emoji = "🟢" if pos.get('unrealized_pnl', 0) >= 0 else "🔴"
|
|
|
+ opened_at_str = "N/A"
|
|
|
+ if pos.get('opened_at'):
|
|
|
+ try:
|
|
|
+ opened_at_dt = datetime.fromisoformat(pos['opened_at'])
|
|
|
+ opened_at_str = opened_at_dt.strftime('%Y-%m-%d %H:%M')
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {abs(pos.get('amount',0))} {token_name}")
|
|
|
+ parts.append(f" Entry: ${pos.get('entry_price',0):,.2f} | Mark: ${pos.get('mark_price',0):,.2f}")
|
|
|
+ parts.append(f" {pos_pnl_emoji} Unrealized P&L: ${pos.get('unrealized_pnl',0):,.2f}")
|
|
|
+ parts.append(f" Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
|
|
|
+ parts.append(f" {open_pnl_emoji} <b>Total Open P&L: ${total_open_unrealized_pnl:,.2f}</b>")
|
|
|
+ else:
|
|
|
+ parts.append("• No open positions for this token.")
|
|
|
+ parts.append("")
|
|
|
+
|
|
|
+ parts.append(f"📋 Open Orders (Exchange): {token_stats_data.get('current_open_orders_count', 0)}")
|
|
|
+ parts.append(f"💡 Use <code>/performance {token_name}</code> for another view including recent trades.")
|
|
|
+
|
|
|
+ return "\n".join(parts)
|
|
|
+
|
|
|
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
- """Handle the /stats command."""
|
|
|
+ """Handle the /stats command. Shows overall stats or stats for a specific token."""
|
|
|
try:
|
|
|
if not self._is_authorized(update):
|
|
|
await self._reply(update, "❌ Unauthorized access.")
|
|
@@ -22,93 +91,104 @@ class StatsCommands(InfoCommandsBase):
|
|
|
await self._reply(update, "❌ Trading stats not available.")
|
|
|
return
|
|
|
|
|
|
- # Get trading stats
|
|
|
- trading_stats = stats.get_basic_stats()
|
|
|
- if not trading_stats:
|
|
|
- await self._reply(update, "❌ Trading statistics not available.")
|
|
|
- return
|
|
|
-
|
|
|
- # Format stats text
|
|
|
- stats_text = "📊 <b>Trading Statistics</b>\n\n"
|
|
|
-
|
|
|
- # Add total trades
|
|
|
- total_trades = trading_stats.get('total_trades', 0)
|
|
|
- stats_text += f"📈 Total Trades: {total_trades}\n"
|
|
|
-
|
|
|
- # Add winning trades
|
|
|
- winning_trades = trading_stats.get('winning_trades', 0)
|
|
|
- win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
- stats_text += f"✅ Winning Trades: {winning_trades} ({win_rate:.2f}%)\n"
|
|
|
-
|
|
|
- # Add losing trades
|
|
|
- losing_trades = trading_stats.get('losing_trades', 0)
|
|
|
- loss_rate = (losing_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
- stats_text += f"❌ Losing Trades: {losing_trades} ({loss_rate:.2f}%)\n"
|
|
|
-
|
|
|
- # Add total P&L
|
|
|
- total_pnl = trading_stats.get('total_pnl', 0.0)
|
|
|
- pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
- stats_text += f"{pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
-
|
|
|
- # Add average P&L per trade
|
|
|
- avg_pnl = total_pnl / total_trades if total_trades > 0 else 0
|
|
|
- avg_pnl_emoji = "🟢" if avg_pnl >= 0 else "🔴"
|
|
|
- stats_text += f"{avg_pnl_emoji} Average P&L per Trade: ${avg_pnl:,.2f}\n"
|
|
|
-
|
|
|
- # Add ROE metrics
|
|
|
- total_roe = trading_stats.get('total_roe', 0.0)
|
|
|
- avg_roe = trading_stats.get('average_roe', 0.0)
|
|
|
- best_roe = trading_stats.get('best_roe', 0.0)
|
|
|
- worst_roe = trading_stats.get('worst_roe', 0.0)
|
|
|
- best_roe_token = trading_stats.get('best_roe_token', 'N/A')
|
|
|
- worst_roe_token = trading_stats.get('worst_roe_token', 'N/A')
|
|
|
-
|
|
|
- roe_emoji = "🟢" if total_roe >= 0 else "🔴"
|
|
|
- stats_text += f"{roe_emoji} Total ROE: {total_roe:+.2f}%\n"
|
|
|
- stats_text += f"{roe_emoji} Average ROE per Trade: {avg_roe:+.2f}%\n"
|
|
|
- stats_text += f"🏆 Best ROE: {best_roe:+.2f}% ({best_roe_token})\n"
|
|
|
- stats_text += f"💔 Worst ROE: {worst_roe:+.2f}% ({worst_roe_token})\n"
|
|
|
-
|
|
|
- # Add largest win and loss
|
|
|
- largest_win = trading_stats.get('largest_win', 0.0)
|
|
|
- largest_loss = trading_stats.get('largest_loss', 0.0)
|
|
|
- stats_text += f"🏆 Largest Win: ${largest_win:,.2f}\n"
|
|
|
- stats_text += f"💔 Largest Loss: ${largest_loss:,.2f}\n"
|
|
|
-
|
|
|
- # Add average win and loss
|
|
|
- avg_win = trading_stats.get('average_win', 0.0)
|
|
|
- avg_loss = trading_stats.get('average_loss', 0.0)
|
|
|
- stats_text += f"📈 Average Win: ${avg_win:,.2f}\n"
|
|
|
- stats_text += f"📉 Average Loss: ${avg_loss:,.2f}\n"
|
|
|
-
|
|
|
- # Add profit factor
|
|
|
- profit_factor = trading_stats.get('profit_factor', 0.0)
|
|
|
- stats_text += f"⚖️ Profit Factor: {profit_factor:.2f}\n"
|
|
|
-
|
|
|
- # Add time-based stats
|
|
|
- first_trade_time = trading_stats.get('first_trade_time')
|
|
|
- if first_trade_time:
|
|
|
- try:
|
|
|
- first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
|
|
|
- trading_duration = datetime.now(first_trade.tzinfo) - first_trade
|
|
|
- days = trading_duration.days
|
|
|
- hours = trading_duration.seconds // 3600
|
|
|
- stats_text += f"⏱️ Trading Duration: {days}d {hours}h\n"
|
|
|
- except ValueError:
|
|
|
- logger.warning(f"Could not parse first_trade_time: {first_trade_time}")
|
|
|
-
|
|
|
- # Add trades per day
|
|
|
- if first_trade_time:
|
|
|
- try:
|
|
|
- first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
|
|
|
- trading_duration = datetime.now(first_trade.tzinfo) - first_trade
|
|
|
- days = max(trading_duration.days, 1) # Avoid division by zero
|
|
|
- trades_per_day = total_trades / days
|
|
|
- stats_text += f"📅 Trades per Day: {trades_per_day:.2f}\n"
|
|
|
- except ValueError:
|
|
|
- pass
|
|
|
-
|
|
|
- await self._reply(update, stats_text.strip())
|
|
|
+ if context.args and len(context.args) > 0:
|
|
|
+ # Token-specific stats
|
|
|
+ token_name = context.args[0].upper()
|
|
|
+ token_stats = stats.get_token_stats(token_name)
|
|
|
+ if not token_stats:
|
|
|
+ await self._reply(update, f"❌ No trading data found for {token_name}.")
|
|
|
+ return
|
|
|
+
|
|
|
+ stats_message = await self._format_token_specific_stats_message(token_stats, token_name)
|
|
|
+ await self._reply(update, stats_message)
|
|
|
+ else:
|
|
|
+ # Overall stats
|
|
|
+ trading_stats = stats.get_basic_stats()
|
|
|
+ if not trading_stats:
|
|
|
+ await self._reply(update, "❌ Trading statistics not available.")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Format stats text
|
|
|
+ stats_text = "📊 <b>Trading Statistics</b>\n\n"
|
|
|
+
|
|
|
+ # Add total trades
|
|
|
+ total_trades = trading_stats.get('total_trades', 0)
|
|
|
+ stats_text += f"📈 Total Trades: {total_trades}\n"
|
|
|
+
|
|
|
+ # Add winning trades
|
|
|
+ winning_trades = trading_stats.get('winning_trades', 0)
|
|
|
+ win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
+ stats_text += f"✅ Winning Trades: {winning_trades} ({win_rate:.2f}%)\n"
|
|
|
+
|
|
|
+ # Add losing trades
|
|
|
+ losing_trades = trading_stats.get('losing_trades', 0)
|
|
|
+ loss_rate = (losing_trades / total_trades * 100) if total_trades > 0 else 0
|
|
|
+ stats_text += f"❌ Losing Trades: {losing_trades} ({loss_rate:.2f}%)\n"
|
|
|
+
|
|
|
+ # Add total P&L
|
|
|
+ total_pnl = trading_stats.get('total_pnl', 0.0)
|
|
|
+ pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
|
|
|
+ stats_text += f"{pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
|
|
|
+
|
|
|
+ # Add average P&L per trade
|
|
|
+ avg_pnl = total_pnl / total_trades if total_trades > 0 else 0
|
|
|
+ avg_pnl_emoji = "🟢" if avg_pnl >= 0 else "🔴"
|
|
|
+ stats_text += f"{avg_pnl_emoji} Average P&L per Trade: ${avg_pnl:,.2f}\n"
|
|
|
+
|
|
|
+ # Add ROE metrics
|
|
|
+ total_roe = trading_stats.get('total_roe', 0.0)
|
|
|
+ avg_roe = trading_stats.get('average_roe', 0.0)
|
|
|
+ best_roe = trading_stats.get('best_roe', 0.0)
|
|
|
+ worst_roe = trading_stats.get('worst_roe', 0.0)
|
|
|
+ best_roe_token = trading_stats.get('best_roe_token', 'N/A')
|
|
|
+ worst_roe_token = trading_stats.get('worst_roe_token', 'N/A')
|
|
|
+
|
|
|
+ roe_emoji = "🟢" if total_roe >= 0 else "🔴"
|
|
|
+ stats_text += f"{roe_emoji} Total ROE: {total_roe:+.2f}%\n"
|
|
|
+ stats_text += f"{roe_emoji} Average ROE per Trade: {avg_roe:+.2f}%\n"
|
|
|
+ stats_text += f"🏆 Best ROE: {best_roe:+.2f}% ({best_roe_token})\n"
|
|
|
+ stats_text += f"💔 Worst ROE: {worst_roe:+.2f}% ({worst_roe_token})\n"
|
|
|
+
|
|
|
+ # Add largest win and loss
|
|
|
+ largest_win = trading_stats.get('largest_win', 0.0)
|
|
|
+ largest_loss = trading_stats.get('largest_loss', 0.0)
|
|
|
+ stats_text += f"🏆 Largest Win: ${largest_win:,.2f}\n"
|
|
|
+ stats_text += f"💔 Largest Loss: ${largest_loss:,.2f}\n"
|
|
|
+
|
|
|
+ # Add average win and loss
|
|
|
+ avg_win = trading_stats.get('average_win', 0.0)
|
|
|
+ avg_loss = trading_stats.get('average_loss', 0.0)
|
|
|
+ stats_text += f"📈 Average Win: ${avg_win:,.2f}\n"
|
|
|
+ stats_text += f"📉 Average Loss: ${avg_loss:,.2f}\n"
|
|
|
+
|
|
|
+ # Add profit factor
|
|
|
+ profit_factor = trading_stats.get('profit_factor', 0.0)
|
|
|
+ stats_text += f"⚖️ Profit Factor: {profit_factor:.2f}\n"
|
|
|
+
|
|
|
+ # Add time-based stats
|
|
|
+ first_trade_time = trading_stats.get('first_trade_time')
|
|
|
+ if first_trade_time:
|
|
|
+ try:
|
|
|
+ first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
|
|
|
+ trading_duration = datetime.now(first_trade.tzinfo) - first_trade
|
|
|
+ days = trading_duration.days
|
|
|
+ hours = trading_duration.seconds // 3600
|
|
|
+ stats_text += f"⏱️ Trading Duration: {days}d {hours}h\n"
|
|
|
+ except ValueError:
|
|
|
+ logger.warning(f"Could not parse first_trade_time: {first_trade_time}")
|
|
|
+
|
|
|
+ # Add trades per day
|
|
|
+ if first_trade_time:
|
|
|
+ try:
|
|
|
+ first_trade = datetime.fromisoformat(first_trade_time.replace('Z', '+00:00'))
|
|
|
+ trading_duration = datetime.now(first_trade.tzinfo) - first_trade
|
|
|
+ days = max(trading_duration.days, 1) # Avoid division by zero
|
|
|
+ trades_per_day = total_trades / days
|
|
|
+ stats_text += f"📅 Trades per Day: {trades_per_day:.2f}\n"
|
|
|
+ except ValueError:
|
|
|
+ pass
|
|
|
+
|
|
|
+ await self._reply(update, stats_text.strip())
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in stats command: {e}")
|