|
@@ -13,9 +13,8 @@ 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."""
|
|
|
+ """Format detailed statistics for a specific token, matching the main /stats style."""
|
|
|
formatter = get_formatter()
|
|
|
-
|
|
|
if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
|
|
|
return (
|
|
|
f"📊 <b>{token_name} Statistics</b>\n\n"
|
|
@@ -27,61 +26,55 @@ class StatsCommands(InfoCommandsBase):
|
|
|
|
|
|
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: {formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0.0))} ({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: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
|
|
|
- parts.append(f"• Avg Win: {formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
|
|
|
- parts.append(f"• Largest Win: {formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))} | Largest Loss: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}")
|
|
|
- parts.append(f"• Entry Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
|
|
|
- parts.append(f"• Exit Volume: {formatter.format_price_with_symbol(perf_summary.get('completed_exit_volume', 0.0))}")
|
|
|
- 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()} {formatter.format_amount(abs(pos.get('amount',0)), token_name)} {token_name}")
|
|
|
- parts.append(f" Entry: {formatter.format_price_with_symbol(pos.get('entry_price',0), token_name)} | Mark: {formatter.format_price_with_symbol(pos.get('mark_price',0), token_name)}")
|
|
|
- parts.append(f" {pos_pnl_emoji} Unrealized P&L: {formatter.format_price_with_symbol(pos.get('unrealized_pnl',0))}")
|
|
|
- parts.append(f" Opened: {opened_at_str} | ID: ...{pos.get('lifecycle_id', '')[-6:]}")
|
|
|
- parts.append(f" {open_pnl_emoji} <b>Total Open P&L: {formatter.format_price_with_symbol(total_open_unrealized_pnl)}</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)
|
|
|
+ session = token_stats_data.get('session_info', {})
|
|
|
+
|
|
|
+ # --- Account Overview ---
|
|
|
+ account_lines = [
|
|
|
+ f"💰 <b>{token_name.upper()} Account Overview:</b>",
|
|
|
+ f"• Current Balance: {formatter.format_price_with_symbol(perf_summary.get('current_balance', 0.0))}",
|
|
|
+ f"• Initial Balance: {formatter.format_price_with_symbol(perf_summary.get('initial_balance', 0.0))}",
|
|
|
+ f"• Open Positions: {len(open_positions)}",
|
|
|
+ ]
|
|
|
+ total_pnl = perf_summary.get('total_pnl', 0.0)
|
|
|
+ entry_vol = perf_summary.get('completed_entry_volume', 0.0)
|
|
|
+ total_pnl_pct = (total_pnl / entry_vol * 100) if entry_vol > 0 else 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: {perf_summary.get('days_active', 0)}")
|
|
|
+
|
|
|
+ # --- Performance Metrics ---
|
|
|
+ perf_lines = [
|
|
|
+ "🏆 <b>Performance Metrics:</b>",
|
|
|
+ f"• Total Completed Trades: {perf_summary.get('completed_trades', 0)}",
|
|
|
+ f"• Win Rate: {perf_summary.get('win_rate', 0.0):.1f}% ({perf_summary.get('total_wins', 0)}/{perf_summary.get('completed_trades', 0)})",
|
|
|
+ f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}",
|
|
|
+ f"• Profit Factor: {perf_summary.get('profit_factor', 0.0):.2f}",
|
|
|
+ f"• Expectancy: {formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}",
|
|
|
+ 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}%)",
|
|
|
+ f"• Largest Losing Trade: {formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}",
|
|
|
+ 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}%)",
|
|
|
+ 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}%)",
|
|
|
+ f"• Average Trade Duration: {perf_summary.get('avg_trade_duration', 'N/A')}",
|
|
|
+ f"• Max Drawdown: {perf_summary.get('max_drawdown', 0.0):.2f}% <i>(Live)</i>",
|
|
|
+ ]
|
|
|
+
|
|
|
+ # --- 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 = (
|
|
|
+ f"📊 <b>{token_name.upper()} Trading Statistics</b>\n\n" +
|
|
|
+ "\n".join(account_lines) +
|
|
|
+ "\n\n" +
|
|
|
+ "\n".join(perf_lines) +
|
|
|
+ "\n\n" +
|
|
|
+ "\n".join(session_lines)
|
|
|
+ )
|
|
|
+ return stats_text.strip()
|
|
|
|
|
|
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
|
"""Handle the /stats command. Shows overall stats or stats for a specific token."""
|
|
@@ -102,99 +95,64 @@ class StatsCommands(InfoCommandsBase):
|
|
|
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
|
|
|
+ return
|
|
|
+
|
|
|
+ # --- Old format for overall stats ---
|
|
|
+ formatter = get_formatter()
|
|
|
+ s = stats.get_basic_stats()
|
|
|
+ perf = stats.get_performance_metrics()
|
|
|
+ session = stats.get_session_info()
|
|
|
+
|
|
|
+ # 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)}")
|
|
|
+
|
|
|
+ # 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>",
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 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)
|
|
|
+ )
|
|
|
|
|
|
- formatter = get_formatter()
|
|
|
-
|
|
|
- # 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: {formatter.format_price_with_symbol(total_pnl)}\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: {formatter.format_price_with_symbol(avg_pnl)}\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: {formatter.format_price_with_symbol(largest_win)}\n"
|
|
|
- stats_text += f"💔 Largest Loss: {formatter.format_price_with_symbol(largest_loss)}\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: {formatter.format_price_with_symbol(avg_win)}\n"
|
|
|
- stats_text += f"📉 Average Loss: {formatter.format_price_with_symbol(avg_loss)}\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())
|
|
|
+ await self._reply(update, stats_text.strip())
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in stats command: {e}")
|