瀏覽代碼

Enhance market command to provide detailed market information for specified tokens. This update includes improved error handling, dynamic token retrieval, and a refined message format that presents comprehensive market data, including price, volume, and order book details. Additionally, the command now utilizes a dedicated formatter for consistent output across the application.

Carles Sentis 6 天之前
父節點
當前提交
39569e8b46
共有 6 個文件被更改,包括 266 次插入302 次删除
  1. 39 39
      src/commands/info/market.py
  2. 17 0
      src/commands/info/positions.py
  3. 97 109
      src/commands/info/risk.py
  4. 105 147
      src/commands/info/stats.py
  5. 7 6
      src/monitoring/risk_cleanup_manager.py
  6. 1 1
      trading_bot.py

+ 39 - 39
src/commands/info/market.py

@@ -3,59 +3,59 @@ from typing import Dict, Any, List, Optional
 from telegram import Update
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
+from src.config.config import Config
+from src.utils.token_display_formatter import get_formatter
 
 logger = logging.getLogger(__name__)
 
 class MarketCommands(InfoCommandsBase):
     """Handles all market-related commands."""
 
+    def __init__(self, trading_engine, notification_manager):
+        super().__init__(trading_engine, notification_manager)
+        self.formatter = get_formatter()
+
     async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /market command."""
+        """Handle the /market command to show market information."""
         try:
             if not self._is_authorized(update):
                 await self._reply(update, "❌ Unauthorized access.")
                 return
 
+            # Get token from command arguments or use default
+            token = context.args[0].upper() if context.args else Config.DEFAULT_TRADING_TOKEN
+            
             # Get market data
-            market_data = self.trading_engine.get_market_data()
+            market_data = await self.trading_engine.get_market_data(token)
             if not market_data:
-                await self._reply(update, "❌ Market data not available.")
+                await self._reply(update, f"❌ Could not get market data for {token}")
                 return
-
-            # Format market text
-            market_text = "📊 <b>Market Overview</b>\n\n"
-
-            for symbol, data in market_data.items():
-                try:
-                    base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    price = float(data.get('price', 0))
-                    volume_24h = float(data.get('volume_24h', 0))
-                    change_24h = float(data.get('change_24h', 0))
-                    high_24h = float(data.get('high_24h', 0))
-                    low_24h = float(data.get('low_24h', 0))
-
-                    # Format market details
-                    formatter = self._get_formatter()
-                    price_str = formatter.format_price_with_symbol(price, base_asset)
-                    volume_str = formatter.format_amount(volume_24h, base_asset)
-                    high_str = formatter.format_price_with_symbol(high_24h, base_asset)
-                    low_str = formatter.format_price_with_symbol(low_24h, base_asset)
-
-                    # Market header
-                    change_emoji = "🟢" if change_24h >= 0 else "🔴"
-                    market_text += f"{change_emoji} <b>{base_asset}</b>\n"
-                    market_text += f"   💰 Price: {price_str}\n"
-                    market_text += f"   📈 24h Change: {change_24h:+.2f}%\n"
-                    market_text += f"   📊 24h Volume: {volume_str} {base_asset}\n"
-                    market_text += f"   🔺 24h High: {high_str}\n"
-                    market_text += f"   🔻 24h Low: {low_str}\n\n"
-
-                except Exception as e:
-                    logger.error(f"Error processing market data for {symbol}: {e}")
-                    continue
-
-            await self._reply(update, market_text.strip())
-
+            
+            # Format the message
+            message = f"📊 <b>Market Information for {token}</b>\n\n"
+            
+            # Add price information
+            message += "💰 <b>Price Information:</b>\n"
+            message += f"Last Price: {self.formatter.format_price_with_symbol(market_data.get('last', 0))}\n"
+            message += f"24h High: {self.formatter.format_price_with_symbol(market_data.get('high', 0))}\n"
+            message += f"24h Low: {self.formatter.format_price_with_symbol(market_data.get('low', 0))}\n"
+            message += f"24h Change: {market_data.get('change', 0):.2f}%\n"
+            
+            # Add volume information
+            message += f"\n📈 <b>Volume Information:</b>\n"
+            message += f"24h Volume: {self.formatter.format_amount(market_data.get('volume', 0))}\n"
+            message += f"24h Turnover: {self.formatter.format_price_with_symbol(market_data.get('turnover', 0))}\n"
+            
+            # Add order book information if available
+            if 'orderbook' in market_data:
+                orderbook = market_data['orderbook']
+                message += f"\n📚 <b>Order Book:</b>\n"
+                message += f"Bid: {self.formatter.format_price_with_symbol(orderbook.get('bid', 0))}\n"
+                message += f"Ask: {self.formatter.format_price_with_symbol(orderbook.get('ask', 0))}\n"
+                message += f"Spread: {self.formatter.format_price_with_symbol(orderbook.get('spread', 0))}\n"
+            
+            await self._reply(update, message.strip())
+            
         except Exception as e:
             logger.error(f"Error in market command: {e}")
-            await self._reply(update, "❌ Error retrieving market data.")
+            await self._reply(update, "❌ Error getting market information. Please try again later.")

+ 17 - 0
src/commands/info/positions.py

@@ -210,6 +210,23 @@ class PositionsCommands(InfoCommandsBase):
                     logger.error(f"Error processing position {position_trade.get('symbol', 'unknown')}: {e}")
                     continue
 
+            # Calculate total unrealized P&L and total ROE
+            total_unrealized_pnl = 0.0
+            total_roe = 0.0
+            for pos in open_positions:
+                size = float(pos.get('size', 0))
+                entry_price = float(pos.get('entryPrice', 0))
+                mark_price = float(pos.get('markPrice', 0))
+                roe = float(pos.get('roe_percentage', 0))
+                if size != 0 and entry_price != 0:
+                    position_value = abs(size * entry_price)
+                    total_unrealized_pnl += size * (mark_price - entry_price)
+                    total_roe += roe * position_value
+                    total_position_value += position_value
+            # Weighted average ROE
+            avg_roe = (total_roe / total_position_value) if total_position_value > 0 else 0.0
+            roe_emoji = "🟢" if avg_roe >= 0 else "🔴"
+
             # Add portfolio summary
             portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
             positions_text += f"💼 <b>Total Portfolio:</b>\n"

+ 97 - 109
src/commands/info/risk.py

@@ -3,134 +3,122 @@ import html
 from telegram import Update
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
+from src.config.config import Config
+from src.utils.token_display_formatter import get_formatter
 
 logger = logging.getLogger(__name__)
 
 class RiskCommands(InfoCommandsBase):
     """Handles all risk management-related commands."""
 
+    def __init__(self, trading_engine, notification_manager):
+        super().__init__(trading_engine, notification_manager)
+        self.formatter = get_formatter()
+
     async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /risk command to show advanced risk metrics."""
-        chat_id = update.effective_chat.id
-        if not self._is_authorized(update):
-            return
-        
+        """Handle the /risk command to show risk management information."""
         try:
-            # Get current balance for context
-            balance = self.trading_engine.get_balance()
-            current_balance = 0
-            if balance and balance.get('total'):
-                current_balance = float(balance['total'].get('USDC', 0))
+            # Get token from command arguments or use default
+            token = context.args[0].upper() if context.args else Config.DEFAULT_TRADING_TOKEN
             
-            # Get risk metrics and basic stats
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
-                return
-                
-            risk_metrics = stats.get_risk_metrics()
-            basic_stats = stats.get_basic_stats()
+            # Get current positions and orders
+            positions = await self.trading_engine.get_positions()
+            orders = await self.trading_engine.get_orders()
             
-            # Check if we have enough data for risk calculations
-            if basic_stats['completed_trades'] < 2:
-                await context.bot.send_message(chat_id=chat_id, text=
-                    "📊 <b>Risk Analysis</b>\n\n"
-                    "📭 <b>Insufficient Data</b>\n\n"
-                    f"• Current completed trades: {html.escape(str(basic_stats['completed_trades']))}\n"
-                    f"• Required for risk analysis: 2+ trades\n"
-                    f"• Daily balance snapshots: {html.escape(str(stats.get_daily_balance_record_count()))}\n\n"
-                    "💡 <b>To enable risk analysis:</b>\n"
-                    "• Complete more trades to generate returns data\n"
-                    "• Bot automatically records daily balance snapshots\n"
-                    "• Risk metrics will be available after sufficient trading history\n\n"
-                    "📈 Use /stats for current performance metrics",
-                    parse_mode='HTML'
-                )
-                return
+            # Get trading statistics
+            stats = self.trading_engine.get_trading_stats()
             
-            # Get risk metric values with safe defaults
-            sharpe_ratio = risk_metrics.get('sharpe_ratio')
-            max_drawdown_pct = risk_metrics.get('max_drawdown_live_percentage', 0.0)
+            # Calculate unrealized P&L for open positions
+            total_unrealized_pnl = 0.0
+            position_risks = []
             
-            # Format values safely
-            sharpe_str = f"{sharpe_ratio:.3f}" if sharpe_ratio is not None else "N/A"
+            for position in positions:
+                try:
+                    # Get position details
+                    symbol = position.get('symbol', '')
+                    size = float(position.get('size', 0))
+                    entry_price = float(position.get('entryPrice', 0))
+                    mark_price = float(position.get('markPrice', 0))
+                    liquidation_price = float(position.get('liquidationPrice', 0))
+                    
+                    if size == 0 or entry_price == 0:
+                        continue
+                        
+                    # Calculate position value and P&L
+                    position_value = abs(size * entry_price)
+                    unrealized_pnl = size * (mark_price - entry_price)
+                    total_unrealized_pnl += unrealized_pnl
+                    
+                    # Calculate risk metrics
+                    price_risk = abs((mark_price - entry_price) / entry_price * 100)
+                    liquidation_risk = abs((liquidation_price - mark_price) / mark_price * 100) if liquidation_price > 0 else 0
+                    
+                    # Find stop loss orders for this position
+                    stop_loss_orders = [o for o in orders if o.get('symbol') == symbol and o.get('type') == 'stop']
+                    stop_loss_risk = 0
+                    if stop_loss_orders:
+                        stop_price = float(stop_loss_orders[0].get('price', 0))
+                        if stop_price > 0:
+                            stop_loss_risk = abs((stop_price - mark_price) / mark_price * 100)
+                    
+                    position_risks.append({
+                        'symbol': symbol,
+                        'size': size,
+                        'entry_price': entry_price,
+                        'mark_price': mark_price,
+                        'position_value': position_value,
+                        'unrealized_pnl': unrealized_pnl,
+                        'price_risk': price_risk,
+                        'liquidation_risk': liquidation_risk,
+                        'stop_loss_risk': stop_loss_risk
+                    })
+                except (ValueError, TypeError) as e:
+                    logger.error(f"Error processing position: {e}")
+                    continue
             
-            # Format the risk analysis message
-            risk_text = f"""
-📊 <b>Risk Analysis & Advanced Metrics</b>
-
-🎯 <b>Risk-Adjusted Performance:</b>
-• Sharpe Ratio: {sharpe_str}
-• Profit Factor: {risk_metrics.get('profit_factor', 0):.2f}
-• Win Rate: {risk_metrics.get('win_rate', 0):.1f}%
-
-📉 <b>Drawdown Analysis:</b>
-• Maximum Drawdown: {max_drawdown_pct:.2f}%
-• Max Consecutive Losses: {risk_metrics.get('max_consecutive_losses', 0)}
-
-💰 <b>Portfolio Context:</b>
-• Current Balance: ${current_balance:,.2f}
-• Initial Balance: ${basic_stats['initial_balance']:,.2f}
-• Total P&L: ${basic_stats['total_pnl']:,.2f}
-• Days Active: {html.escape(str(basic_stats['days_active']))}
-
-📊 <b>Risk Interpretation:</b>
-"""
+            # Get portfolio value
+            portfolio_value = float(self.trading_engine.get_portfolio_value())
             
-            # Add interpretive guidance
-            if sharpe_ratio is not None:
-                if sharpe_ratio > 2.0:
-                    risk_text += "• 🟢 <b>Excellent</b> risk-adjusted returns (Sharpe > 2.0)\n"
-                elif sharpe_ratio > 1.0:
-                    risk_text += "• 🟡 <b>Good</b> risk-adjusted returns (Sharpe > 1.0)\n"
-                elif sharpe_ratio > 0.5:
-                    risk_text += "• 🟠 <b>Moderate</b> risk-adjusted returns (Sharpe > 0.5)\n"
-                elif sharpe_ratio > 0:
-                    risk_text += "• 🔴 <b>Poor</b> risk-adjusted returns (Sharpe > 0)\n"
-                else:
-                    risk_text += "• ⚫ <b>Negative</b> risk-adjusted returns (Sharpe < 0)\n"
-            else:
-                risk_text += "• ⚪ <b>Insufficient data</b> for Sharpe ratio calculation\n"
+            # Calculate portfolio risk metrics
+            portfolio_risk = (total_unrealized_pnl / portfolio_value * 100) if portfolio_value > 0 else 0
             
-            if max_drawdown_pct < 5:
-                risk_text += "• 🟢 <b>Low</b> maximum drawdown (< 5%)\n"
-            elif max_drawdown_pct < 15:
-                risk_text += "• 🟡 <b>Moderate</b> maximum drawdown (< 15%)\n"
-            elif max_drawdown_pct < 30:
-                risk_text += "• 🟠 <b>High</b> maximum drawdown (< 30%)\n"
-            else:
-                risk_text += "• 🔴 <b>Very High</b> maximum drawdown (> 30%)\n"
+            # Format the message
+            message = f"📊 <b>Risk Analysis for {token}</b>\n\n"
             
-            # Add profit factor interpretation
-            profit_factor = risk_metrics.get('profit_factor', 0)
-            if profit_factor > 2.0:
-                risk_text += "• 🟢 <b>Excellent</b> profit factor (> 2.0)\n"
-            elif profit_factor > 1.5:
-                risk_text += "• 🟡 <b>Good</b> profit factor (> 1.5)\n"
-            elif profit_factor > 1.0:
-                risk_text += "• 🟠 <b>Profitable</b> but low profit factor (> 1.0)\n"
+            # Add position risks
+            if position_risks:
+                message += "🔍 <b>Position Risks:</b>\n"
+                for risk in position_risks:
+                    pnl_emoji = "🟢" if risk['unrealized_pnl'] >= 0 else "🔴"
+                    message += (
+                        f"\n<b>{risk['symbol']}</b>\n"
+                        f"Size: {self.formatter.format_amount(risk['size'])}\n"
+                        f"Entry: {self.formatter.format_price_with_symbol(risk['entry_price'])}\n"
+                        f"Mark: {self.formatter.format_price_with_symbol(risk['mark_price'])}\n"
+                        f"P&L: {pnl_emoji} {self.formatter.format_price_with_symbol(risk['unrealized_pnl'])}\n"
+                        f"Price Risk: {risk['price_risk']:.2f}%\n"
+                        f"Liquidation Risk: {risk['liquidation_risk']:.2f}%\n"
+                        f"Stop Loss Risk: {risk['stop_loss_risk']:.2f}%\n"
+                    )
             else:
-                risk_text += "• 🔴 <b>Unprofitable</b> trading strategy (< 1.0)\n"
+                message += "No open positions\n"
             
-            risk_text += f"""
-
-💡 <b>Risk Definitions:</b>
-• <b>Sharpe Ratio:</b> Risk-adjusted return (excess return / volatility)
-• <b>Profit Factor:</b> Total winning trades / Total losing trades
-• <b>Win Rate:</b> Percentage of profitable trades
-• <b>Max Drawdown:</b> Largest peak-to-trough decline
-• <b>Max Consecutive Losses:</b> Longest streak of losing trades
-
-📈 <b>Data Based On:</b>
-• Completed Trades: {html.escape(str(basic_stats['completed_trades']))}
-• Trading Period: {html.escape(str(basic_stats['days_active']))} days
-
-🔄 Use /stats for trading performance metrics
-            """
+            # Add portfolio summary
+            message += f"\n📈 <b>Portfolio Summary:</b>\n"
+            message += f"Total Value: {self.formatter.format_price_with_symbol(portfolio_value)}\n"
+            message += f"Unrealized P&L: {self.formatter.format_price_with_symbol(total_unrealized_pnl)}\n"
+            message += f"Portfolio Risk: {portfolio_risk:.2f}%\n"
+            
+            # Add trading statistics
+            if stats:
+                message += f"\n📊 <b>Trading Statistics:</b>\n"
+                message += f"Win Rate: {stats.get('win_rate', 0):.2f}%\n"
+                message += f"Profit Factor: {stats.get('profit_factor', 0):.2f}\n"
+                message += f"Average Win: {self.formatter.format_price_with_symbol(stats.get('avg_win', 0))}\n"
+                message += f"Average Loss: {self.formatter.format_price_with_symbol(stats.get('avg_loss', 0))}\n"
             
-            await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
+            await update.message.reply_text(message, parse_mode='HTML')
             
         except Exception as e:
-            error_message = f"❌ Error processing risk command: {str(e)}"
-            await context.bot.send_message(chat_id=chat_id, text=error_message)
             logger.error(f"Error in risk command: {e}")
+            await update.message.reply_text("❌ Error getting risk information. Please try again later.")

+ 105 - 147
src/commands/info/stats.py

@@ -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}")

+ 7 - 6
src/monitoring/risk_cleanup_manager.py

@@ -125,10 +125,12 @@ class RiskCleanupManager:
         """Check for automatic stop loss triggers based on Config.STOP_LOSS_PERCENTAGE as safety net."""
         try:
             if not getattr(Config, 'RISK_MANAGEMENT_ENABLED', True) or Config.STOP_LOSS_PERCENTAGE <= 0:
+                logger.debug(f"Risk management disabled or STOP_LOSS_PERCENTAGE <= 0 (value: {Config.STOP_LOSS_PERCENTAGE})")
                 return
 
             positions = self.market_monitor_cache.cached_positions or []
             if not positions:
+                logger.debug("No positions found in cache for risk management check.")
                 await self._cleanup_orphaned_stop_losses() # Call within class
                 return
 
@@ -141,19 +143,17 @@ class RiskCleanupManager:
                     unrealized_pnl = float(position.get('unrealizedPnl', 0))
                     
                     if contracts == 0 or entry_price <= 0 or mark_price <= 0:
+                        logger.debug(f"Skipping position {symbol}: contracts={contracts}, entry_price={entry_price}, mark_price={mark_price}")
                         continue
 
                     # Get ROE directly from exchange data
                     info_data = position.get('info', {})
                     position_info = info_data.get('position', {})
-                    roe_raw = position_info.get('returnOnEquity')  # Changed from 'percentage' to 'returnOnEquity'
-                    
+                    roe_raw = position_info.get('returnOnEquity')
                     if roe_raw is not None:
                         try:
-                            # The exchange provides ROE as a decimal (e.g., -0.326 for -32.6%)
-                            # We need to multiply by 100 and keep the sign
                             roe_percentage = float(roe_raw) * 100
-                            logger.debug(f"Using exchange-provided ROE for {symbol}: {roe_percentage:+.2f}%")
+                            logger.debug(f"[RiskMgmt] {symbol}: ROE from exchange: {roe_percentage:+.2f}% (raw: {roe_raw})")
                         except (ValueError, TypeError):
                             logger.warning(f"Could not parse ROE value: {roe_raw} for {symbol}")
                             roe_percentage = 0.0
@@ -161,7 +161,8 @@ class RiskCleanupManager:
                         logger.warning(f"No ROE data available from exchange for {symbol}")
                         roe_percentage = 0.0
 
-                    # The exchange shows losses as negative percentages, so we compare against negative threshold
+                    logger.info(f"[RiskMgmt] {symbol}: ROE={roe_percentage:+.2f}%, Threshold=-{Config.STOP_LOSS_PERCENTAGE}% (Trigger: {roe_percentage <= -Config.STOP_LOSS_PERCENTAGE})")
+
                     if roe_percentage <= -Config.STOP_LOSS_PERCENTAGE:
                         token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
                         position_side = "LONG" if contracts > 0 else "SHORT"

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.4.203"
+BOT_VERSION = "2.4.204"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))