|
@@ -315,6 +315,18 @@ class TradingStats:
|
|
|
"""Update live max drawdown."""
|
|
|
return self.performance_calculator.update_live_max_drawdown(current_balance)
|
|
|
|
|
|
+ def get_drawdown_monitor_data(self) -> Dict[str, float]:
|
|
|
+ """Get drawdown data from DrawdownMonitor for external monitoring systems."""
|
|
|
+ try:
|
|
|
+ peak_balance = float(self._get_metadata('drawdown_peak_balance') or '0.0')
|
|
|
+ max_drawdown_pct = float(self._get_metadata('drawdown_max_drawdown_pct') or '0.0')
|
|
|
+ return {
|
|
|
+ 'peak_balance': peak_balance,
|
|
|
+ 'max_drawdown_percentage': max_drawdown_pct
|
|
|
+ }
|
|
|
+ except (ValueError, TypeError):
|
|
|
+ return {'peak_balance': 0.0, 'max_drawdown_percentage': 0.0}
|
|
|
+
|
|
|
def calculate_sharpe_ratio(self, days: int = 30) -> Optional[float]:
|
|
|
"""Calculate Sharpe ratio."""
|
|
|
return self.performance_calculator.calculate_sharpe_ratio(days)
|
|
@@ -492,13 +504,13 @@ class TradingStats:
|
|
|
|
|
|
return {
|
|
|
'token': upper_token,
|
|
|
- 'performance': perf_stats,
|
|
|
- 'open_positions': {
|
|
|
- 'count': len(open_trades_for_token),
|
|
|
- 'total_value': total_open_value,
|
|
|
- 'total_unrealized_pnl': total_open_unrealized_pnl,
|
|
|
- 'positions': open_positions_summary
|
|
|
- },
|
|
|
+ 'message': f"Statistics for {upper_token}",
|
|
|
+ 'performance_summary': perf_stats, # Expected key by formatting method
|
|
|
+ 'performance': perf_stats, # Legacy compatibility
|
|
|
+ 'open_positions': open_positions_summary, # Direct list as expected
|
|
|
+ 'summary_total_trades': effective_total_trades, # Expected by formatting method
|
|
|
+ 'summary_total_unrealized_pnl': total_open_unrealized_pnl, # Expected by formatting method
|
|
|
+ 'current_open_orders_count': current_open_orders_for_token, # Expected by formatting method
|
|
|
'summary': {
|
|
|
'total_trades': effective_total_trades,
|
|
|
'open_orders': current_open_orders_for_token,
|
|
@@ -558,19 +570,117 @@ class TradingStats:
|
|
|
stats_text_parts.append(f"• Open Positions: {open_positions_count}")
|
|
|
stats_text_parts.append(f"• {pnl_emoji} Total P&L: {formatter.format_price_with_symbol(total_pnl_val)} ({total_return_pct:+.2f}%)")
|
|
|
stats_text_parts.append(f"• Days Active: {basic['days_active']}\n")
|
|
|
-
|
|
|
- # Trading Performance
|
|
|
- stats_text_parts.append(f"📈 <b>Trading Performance:</b>")
|
|
|
- stats_text_parts.append(f"• Total Cycles: {perf['total_completed_cycles']}")
|
|
|
- stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_winning_cycles']}/{perf['total_completed_cycles']})")
|
|
|
+
|
|
|
+ # Performance Metrics
|
|
|
+ stats_text_parts.append(f"\n🏆 <b>Performance Metrics:</b>")
|
|
|
+ stats_text_parts.append(f"• Total Completed Trades: {basic['completed_trades']}")
|
|
|
+ stats_text_parts.append(f"• Win Rate: {perf['win_rate']:.1f}% ({perf['total_wins']}/{basic['completed_trades']})")
|
|
|
+ stats_text_parts.append(f"• Trading Volume (Entry Vol.): {formatter.format_price_with_symbol(perf.get('total_trading_volume', 0.0))}")
|
|
|
stats_text_parts.append(f"• Profit Factor: {perf['profit_factor']:.2f}")
|
|
|
stats_text_parts.append(f"• Expectancy: {formatter.format_price_with_symbol(perf['expectancy'])}")
|
|
|
|
|
|
- return "\n".join(stats_text_parts)
|
|
|
+ stats_text_parts.append(f"• Largest Winning Trade: {formatter.format_price_with_symbol(perf['largest_win'])}")
|
|
|
+ stats_text_parts.append(f"• Largest Losing Trade: {formatter.format_price_with_symbol(-perf['largest_loss'])}")
|
|
|
+
|
|
|
+ best_token_stats = perf.get('best_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0})
|
|
|
+ worst_token_stats = perf.get('worst_performing_token', {'name': 'N/A', 'pnl_percentage': 0.0})
|
|
|
+ stats_text_parts.append(f"• Best Performing Token: {best_token_stats['name']} ({best_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
+ stats_text_parts.append(f"• Worst Performing Token: {worst_token_stats['name']} ({worst_token_stats['pnl_percentage']:+.2f}%)")
|
|
|
+
|
|
|
+ stats_text_parts.append(f"• Average Trade Duration: {perf.get('avg_trade_duration', 'N/A')}")
|
|
|
+ stats_text_parts.append(f"• Portfolio Max Drawdown: {risk.get('max_drawdown_live_percentage', 0.0):.2f}% <i>(Live)</i>")
|
|
|
+
|
|
|
+ # Session Info
|
|
|
+ stats_text_parts.append(f"\n\n⏰ <b>Session Info:</b>")
|
|
|
+ stats_text_parts.append(f"• Bot Started: {basic['start_date']}")
|
|
|
+ stats_text_parts.append(f"• Stats Last Updated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
|
+
|
|
|
+ return "\n".join(stats_text_parts).strip()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error formatting stats message: {e}", exc_info=True)
|
|
|
+ return f"""📊 <b>Trading Statistics</b>\n\n❌ <b>Error loading statistics</b>\n\n🔧 <b>Debug info:</b> {str(e)[:100]}"""
|
|
|
+
|
|
|
+ def format_token_stats_message(self, token: str) -> str:
|
|
|
+ """Format detailed statistics for a specific token."""
|
|
|
+ try:
|
|
|
+ from src.utils.token_display_formatter import get_formatter
|
|
|
+ formatter = get_formatter()
|
|
|
+
|
|
|
+ token_stats_data = self.get_token_detailed_stats(token)
|
|
|
+ token_name = token_stats_data.get('token', token.upper())
|
|
|
+
|
|
|
+ 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: {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
|
|
|
+ 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:
|
|
|
+ from datetime import datetime
|
|
|
+ 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)
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error formatting stats message: {e}")
|
|
|
- return f"❌ Error generating statistics: {str(e)}"
|
|
|
+ logger.error(f"❌ Error formatting token stats message for {token}: {e}", exc_info=True)
|
|
|
+ return f"❌ Error generating statistics for {token}: {str(e)[:100]}"
|
|
|
|
|
|
# =============================================================================
|
|
|
# CONVENIENCE METHODS & HIGH-LEVEL OPERATIONS
|