Browse Source

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 days ago
parent
commit
39569e8b46

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

@@ -3,59 +3,59 @@ from typing import Dict, Any, List, Optional
 from telegram import Update
 from telegram import Update
 from telegram.ext import ContextTypes
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
 from .base import InfoCommandsBase
+from src.config.config import Config
+from src.utils.token_display_formatter import get_formatter
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 class MarketCommands(InfoCommandsBase):
 class MarketCommands(InfoCommandsBase):
     """Handles all market-related commands."""
     """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:
     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:
         try:
             if not self._is_authorized(update):
             if not self._is_authorized(update):
                 await self._reply(update, "❌ Unauthorized access.")
                 await self._reply(update, "❌ Unauthorized access.")
                 return
                 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
             # 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:
             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
                 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:
         except Exception as e:
             logger.error(f"Error in market command: {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}")
                     logger.error(f"Error processing position {position_trade.get('symbol', 'unknown')}: {e}")
                     continue
                     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
             # Add portfolio summary
             portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
             portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
             positions_text += f"💼 <b>Total Portfolio:</b>\n"
             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 import Update
 from telegram.ext import ContextTypes
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
 from .base import InfoCommandsBase
+from src.config.config import Config
+from src.utils.token_display_formatter import get_formatter
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 class RiskCommands(InfoCommandsBase):
 class RiskCommands(InfoCommandsBase):
     """Handles all risk management-related commands."""
     """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:
     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:
         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:
             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:
         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}")
             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."""
     """Handles all statistics-related commands."""
 
 
     async def _format_token_specific_stats_message(self, token_stats_data: Dict[str, Any], token_name: str) -> str:
     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()
         formatter = get_formatter()
-        
         if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
         if not token_stats_data or token_stats_data.get('summary_total_trades', 0) == 0:
             return (
             return (
                 f"📊 <b>{token_name} Statistics</b>\n\n"
                 f"📊 <b>{token_name} Statistics</b>\n\n"
@@ -27,61 +26,55 @@ class StatsCommands(InfoCommandsBase):
 
 
         perf_summary = token_stats_data.get('performance_summary', {})
         perf_summary = token_stats_data.get('performance_summary', {})
         open_positions = token_stats_data.get('open_positions', [])
         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:
     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."""
         """Handle the /stats command. Shows overall stats or stats for a specific token."""
@@ -102,99 +95,64 @@ class StatsCommands(InfoCommandsBase):
                 if not token_stats:
                 if not token_stats:
                     await self._reply(update, f"❌ No trading data found for {token_name}.")
                     await self._reply(update, f"❌ No trading data found for {token_name}.")
                     return
                     return
-                
                 stats_message = await self._format_token_specific_stats_message(token_stats, token_name)
                 stats_message = await self._format_token_specific_stats_message(token_stats, token_name)
                 await self._reply(update, stats_message)
                 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:
         except Exception as e:
             logger.error(f"Error in stats command: {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."""
         """Check for automatic stop loss triggers based on Config.STOP_LOSS_PERCENTAGE as safety net."""
         try:
         try:
             if not getattr(Config, 'RISK_MANAGEMENT_ENABLED', True) or Config.STOP_LOSS_PERCENTAGE <= 0:
             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
                 return
 
 
             positions = self.market_monitor_cache.cached_positions or []
             positions = self.market_monitor_cache.cached_positions or []
             if not positions:
             if not positions:
+                logger.debug("No positions found in cache for risk management check.")
                 await self._cleanup_orphaned_stop_losses() # Call within class
                 await self._cleanup_orphaned_stop_losses() # Call within class
                 return
                 return
 
 
@@ -141,19 +143,17 @@ class RiskCleanupManager:
                     unrealized_pnl = float(position.get('unrealizedPnl', 0))
                     unrealized_pnl = float(position.get('unrealizedPnl', 0))
                     
                     
                     if contracts == 0 or entry_price <= 0 or mark_price <= 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
                         continue
 
 
                     # Get ROE directly from exchange data
                     # Get ROE directly from exchange data
                     info_data = position.get('info', {})
                     info_data = position.get('info', {})
                     position_info = info_data.get('position', {})
                     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:
                     if roe_raw is not None:
                         try:
                         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
                             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):
                         except (ValueError, TypeError):
                             logger.warning(f"Could not parse ROE value: {roe_raw} for {symbol}")
                             logger.warning(f"Could not parse ROE value: {roe_raw} for {symbol}")
                             roe_percentage = 0.0
                             roe_percentage = 0.0
@@ -161,7 +161,8 @@ class RiskCleanupManager:
                         logger.warning(f"No ROE data available from exchange for {symbol}")
                         logger.warning(f"No ROE data available from exchange for {symbol}")
                         roe_percentage = 0.0
                         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:
                     if roe_percentage <= -Config.STOP_LOSS_PERCENTAGE:
                         token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
                         token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
                         position_side = "LONG" if contracts > 0 else "SHORT"
                         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
 from pathlib import Path
 
 
 # Bot version
 # Bot version
-BOT_VERSION = "2.4.203"
+BOT_VERSION = "2.4.204"
 
 
 # Add src directory to Python path
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))
 sys.path.insert(0, str(Path(__file__).parent / "src"))