浏览代码

Enhance commands command to include quick action buttons for instant access to bot functions. Updated the command text to provide a more user-friendly interface and improved error handling for message sending. Additionally, refined the risk command to present advanced risk metrics and analysis, ensuring better insights into trading performance and risk management.

Carles Sentis 1 周之前
父节点
当前提交
59c59bc131
共有 5 个文件被更改,包括 187 次插入171 次删除
  1. 49 51
      src/commands/info/commands.py
  2. 8 2
      src/commands/info/performance.py
  3. 107 101
      src/commands/info/risk.py
  4. 22 16
      src/commands/info/stats.py
  5. 1 1
      trading_bot.py

+ 49 - 51
src/commands/info/commands.py

@@ -1,5 +1,5 @@
 import logging
-from telegram import Update
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
 
@@ -9,55 +9,53 @@ class CommandsInfo(InfoCommandsBase):
     """Handles the commands information command."""
 
     async def commands_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /commands command to show available commands."""
+        """Handle the /commands and /c command with quick action buttons."""
+        chat_id = update.effective_chat.id
         if not self._is_authorized(update):
             return
-
-        try:
-            commands_text = ["🤖 <b>Available Commands</b>"]
-
-            # Trading Commands
-            commands_text.append("\n📈 <b>Trading Commands:</b>")
-            commands_text.append("• /long [token] [amount] - Open a long position")
-            commands_text.append("• /short [token] [amount] - Open a short position")
-            commands_text.append("• /exit [token] - Close position")
-            commands_text.append("• /sl [token] [price] - Set stop loss")
-            commands_text.append("• /tp [token] [price] - Set take profit")
-            commands_text.append("• /coo [token] [price] - Cancel open orders")
-
-            # Information Commands
-            commands_text.append("\n📊 <b>Information Commands:</b>")
-            commands_text.append("• /balance - Show account balance")
-            commands_text.append("• /positions - Show open positions")
-            commands_text.append("• /orders - Show open orders")
-            commands_text.append("• /trades - Show recent trades")
-            commands_text.append("• /stats - Show trading statistics")
-            commands_text.append("• /market - Show market overview")
-            commands_text.append("• /price [token] - Show token price")
-            commands_text.append("• /performance [token] - Show performance stats")
-            commands_text.append("• /daily - Show daily performance")
-            commands_text.append("• /weekly - Show weekly performance")
-            commands_text.append("• /monthly - Show monthly performance")
-            commands_text.append("• /risk - Show risk management info")
-            commands_text.append("• /balance_adjustments - Show balance adjustments")
-
-            # Management Commands
-            commands_text.append("\n⚙️ <b>Management Commands:</b>")
-            commands_text.append("• /monitoring - Show monitoring status")
-            commands_text.append("• /alarm - Set price alarms")
-            commands_text.append("• /logs - Show recent logs")
-            commands_text.append("• /debug - Show debug information")
-            commands_text.append("• /version - Show bot version")
-            commands_text.append("• /keyboard - Toggle keyboard")
-
-            # Help
-            commands_text.append("\n💡 <b>Help:</b>")
-            commands_text.append("• Use /help for detailed command information")
-            commands_text.append("• Add ? after any command for help with that command")
-
-            await self._reply(update, "\n".join(commands_text))
-
-        except Exception as e:
-            error_message = f"❌ Error processing commands command: {str(e)}"
-            await self._reply(update, error_message)
-            logger.error(f"Error in commands command: {e}") 
+        
+        commands_text = """
+📱 <b>Quick Commands</b>
+
+Tap any button below for instant access to bot functions:
+
+💡 <b>Pro Tip:</b> These buttons work the same as typing the commands manually, but faster!
+        """
+        
+        keyboard = [
+            [
+                InlineKeyboardButton("💰 Balance", callback_data="/balance"),
+                InlineKeyboardButton("📈 Positions", callback_data="/positions")
+            ],
+            [
+                InlineKeyboardButton("📋 Orders", callback_data="/orders"),
+                InlineKeyboardButton("📊 Stats", callback_data="/stats")
+            ],
+            [
+                InlineKeyboardButton("💵 Price", callback_data="/price"),
+                InlineKeyboardButton("📊 Market", callback_data="/market")
+            ],
+            [
+                InlineKeyboardButton("🏆 Performance", callback_data="/performance"),
+                InlineKeyboardButton("🔔 Alarms", callback_data="/alarm")
+            ],
+            [
+                InlineKeyboardButton("📅 Daily", callback_data="/daily"),
+                InlineKeyboardButton("📊 Weekly", callback_data="/weekly")
+            ],
+            [
+                InlineKeyboardButton("📆 Monthly", callback_data="/monthly"),
+                InlineKeyboardButton("🔄 Trades", callback_data="/trades")
+            ],
+            [
+                InlineKeyboardButton("🔄 Monitoring", callback_data="/monitoring"),
+                InlineKeyboardButton("📝 Logs", callback_data="/logs")
+            ],
+            [
+                InlineKeyboardButton("🎲 Risk", callback_data="/risk"),
+                InlineKeyboardButton("⚙️ Help", callback_data="/help")
+            ]
+        ]
+        reply_markup = InlineKeyboardMarkup(keyboard)
+        
+        await context.bot.send_message(chat_id=chat_id, text=commands_text, parse_mode='HTML', reply_markup=reply_markup) 

+ 8 - 2
src/commands/info/performance.py

@@ -53,12 +53,18 @@ class PerformanceCommands(InfoCommandsBase):
                 )
                 return
 
-            # Tokens are already sorted by P&L from get_token_performance
+            # Sort tokens by P&L
+            sorted_tokens = sorted(
+                token_performance,
+                key=lambda x: x.get('total_pnl', 0),
+                reverse=True
+            )
+
             performance_text = "🏆 <b>Token Performance Ranking</b>\n"
             performance_text += "<i>Ordered by Total P&L (Dollar Amount)</i>\n\n"
             
             # Add ranking with emojis
-            for i, stats_data in enumerate(token_performance, 1):
+            for i, stats_data in enumerate(sorted_tokens, 1):
                 # Ranking emoji
                 if i == 1:
                     rank_emoji = "🥇"

+ 107 - 101
src/commands/info/risk.py

@@ -1,8 +1,8 @@
 import logging
+import html
 from telegram import Update
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
-from src.utils.token_display_formatter import get_formatter
 
 logger = logging.getLogger(__name__)
 
@@ -10,121 +10,127 @@ class RiskCommands(InfoCommandsBase):
     """Handles all risk management-related commands."""
 
     async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /risk command to show risk management information."""
+        """Handle the /risk command to show advanced risk metrics."""
+        chat_id = update.effective_chat.id
         if not self._is_authorized(update):
             return
-
+        
         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 risk metrics and basic stats
             stats = self.trading_engine.get_stats()
             if not stats:
-                await self._reply(update, "❌ Could not load trading statistics")
+                await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
                 return
-
-            # Get current positions
-            positions = stats.get_open_positions()
-            if not positions:
-                await self._reply(update, "📭 No open positions to analyze risk")
+                
+            risk_metrics = stats.get_risk_metrics()
+            basic_stats = stats.get_basic_stats()
+            
+            # 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 current orders for stop loss analysis
-            orders = self.trading_engine.get_orders() or []
             
-            # Get formatter for consistent display
-            formatter = get_formatter()
-
-            # Build risk analysis message
-            risk_text_parts = ["🎯 <b>Risk Management Overview</b>"]
-
-            # Analyze each position
-            for position in positions:
-                try:
-                    symbol = position.get('symbol', '')
-                    if not symbol:
-                        continue
+            # 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)
+            
+            # Format values safely
+            sharpe_str = f"{sharpe_ratio:.3f}" if sharpe_ratio is not None else "N/A"
+            
+            # Format the risk analysis message
+            risk_text = f"""
+📊 <b>Risk Analysis & Advanced Metrics</b>
 
-                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    side = position.get('position_side', '')
-                    size = float(position.get('current_position_size', 0))
-                    entry_price = float(position.get('entry_price', 0))
-                    current_price = float(position.get('current_price', entry_price))
-                    
-                    if size == 0 or entry_price <= 0 or current_price <= 0:
-                        continue
+🎯 <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}%
 
-                    # Calculate unrealized P&L
-                    if side == 'long':
-                        unrealized_pnl = size * (current_price - entry_price)
-                    else:  # short
-                        unrealized_pnl = size * (entry_price - current_price)
-                    
-                    # Find stop loss orders for this position
-                    stop_loss_orders = [
-                        order for order in orders 
-                        if order.get('symbol') == symbol and 
-                        order.get('type') == 'stop_loss'
-                    ]
-                    
-                    # Get the most relevant stop loss price
-                    stop_loss_price = None
-                    if stop_loss_orders:
-                        # Sort by trigger price to find the most conservative stop loss
-                        stop_loss_orders.sort(key=lambda x: float(x.get('triggerPrice', 0)))
-                        if side == 'long':
-                            stop_loss_price = float(stop_loss_orders[0].get('triggerPrice', 0))
-                        else:  # short
-                            stop_loss_price = float(stop_loss_orders[-1].get('triggerPrice', 0))
-                    
-                    # Calculate risk metrics
-                    risk_text_parts.append(f"\n📊 <b>{token}</b>")
-                    risk_text_parts.append(f"• Position: {side.upper()} {formatter.format_amount(size, token)} @ {formatter.format_price_with_symbol(entry_price, token)}")
-                    risk_text_parts.append(f"• Current: {formatter.format_price_with_symbol(current_price, token)}")
-                    
-                    # Show P&L
-                    pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-                    risk_text_parts.append(f"• P&L: {pnl_emoji} {formatter.format_price_with_symbol(unrealized_pnl)}")
-                    
-                    # Show stop loss info
-                    if stop_loss_price:
-                        risk_amount = abs(size * (stop_loss_price - entry_price))
-                        risk_text_parts.append(f"• Stop Loss: {formatter.format_price_with_symbol(stop_loss_price, token)}")
-                        risk_text_parts.append(f"• Risk Amount: {formatter.format_price_with_symbol(risk_amount)}")
-                    else:
-                        risk_text_parts.append("• Stop Loss: ❌ Not set")
-                    
-                    # Add separator between positions
-                    risk_text_parts.append("")
+📉 <b>Drawdown Analysis:</b>
+• Maximum Drawdown: {max_drawdown_pct:.2f}%
+• Max Consecutive Losses: {risk_metrics.get('max_consecutive_losses', 0)}
 
-                except (ValueError, TypeError) as e:
-                    logger.warning(f"Error processing position {position.get('symbol', 'unknown')}: {e}")
-                    continue
+💰 <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']))}
 
-            # Add portfolio-level risk metrics
-            total_position_value = 0
-            for pos in positions:
-                try:
-                    size = float(pos.get('current_position_size', 0))
-                    price = float(pos.get('current_price', 0))
-                    if size != 0 and price > 0:
-                        total_position_value += abs(size * price)
-                except (ValueError, TypeError):
-                    continue
+📊 <b>Risk Interpretation:</b>
+"""
             
-            # Get account balance
-            balance = self.trading_engine.get_balance()
-            if balance:
-                try:
-                    total_balance = float(balance.get('total', 0))
-                    if total_balance > 0:
-                        portfolio_risk = (total_position_value / total_balance) * 100
-                        risk_text_parts.append(f"📈 <b>Portfolio Risk</b>")
-                        risk_text_parts.append(f"• Total Position Value: {formatter.format_price_with_symbol(total_position_value)}")
-                        risk_text_parts.append(f"• Portfolio Risk: {portfolio_risk:.1f}% of balance")
-                except (ValueError, TypeError) as e:
-                    logger.warning(f"Error calculating portfolio risk: {e}")
+            # 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"
+            
+            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"
+            
+            # 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"
+            else:
+                risk_text += "• 🔴 <b>Unprofitable</b> trading strategy (< 1.0)\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
 
-            await self._reply(update, "\n".join(risk_text_parts).strip(), parse_mode='HTML')
+📈 <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
+            """
+            
+            await context.bot.send_message(chat_id=chat_id, text=risk_text.strip(), parse_mode='HTML')
+            
         except Exception as e:
             error_message = f"❌ Error processing risk command: {str(e)}"
-            await self._reply(update, error_message)
+            await context.bot.send_message(chat_id=chat_id, text=error_message)
             logger.error(f"Error in risk command: {e}")

+ 22 - 16
src/commands/info/stats.py

@@ -4,6 +4,8 @@ from datetime import datetime, timedelta
 from telegram import Update
 from telegram.ext import ContextTypes
 from .base import InfoCommandsBase
+from src.utils.token_display_formatter import get_formatter
+from src.config.config import Config
 
 logger = logging.getLogger(__name__)
 
@@ -12,6 +14,8 @@ class StatsCommands(InfoCommandsBase):
 
     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."""
+        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"
@@ -34,14 +38,14 @@ class StatsCommands(InfoCommandsBase):
             pnl_pct = (perf_summary.get('total_pnl', 0.0) / entry_vol * 100) if entry_vol > 0 else 0.0
             
             parts.append(f"• Total Completed: {perf_summary.get('completed_trades', 0)}")
-            parts.append(f"• {pnl_emoji} Realized P&L: ${perf_summary.get('total_pnl', 0.0):,.2f} ({pnl_pct:+.2f}%)")
+            parts.append(f"• {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: ${perf_summary.get('expectancy', 0.0):,.2f}")
-            parts.append(f"• Avg Win: ${perf_summary.get('avg_win', 0.0):,.2f} | Avg Loss: ${perf_summary.get('avg_loss', 0.0):,.2f}")
-            parts.append(f"• Largest Win: ${perf_summary.get('largest_win', 0.0):,.2f} | Largest Loss: ${perf_summary.get('largest_loss', 0.0):,.2f}")
-            parts.append(f"• Entry Volume: ${perf_summary.get('completed_entry_volume', 0.0):,.2f}")
-            parts.append(f"• Exit Volume: ${perf_summary.get('completed_exit_volume', 0.0):,.2f}")
+            parts.append(f"• 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:
@@ -65,11 +69,11 @@ class StatsCommands(InfoCommandsBase):
                     except:
                         pass
                 
-                parts.append(f"• {pos_side_emoji} {pos.get('side', '').upper()} {abs(pos.get('amount',0))} {token_name}")
-                parts.append(f"    Entry: ${pos.get('entry_price',0):,.2f} | Mark: ${pos.get('mark_price',0):,.2f}")
-                parts.append(f"    {pos_pnl_emoji} Unrealized P&L: ${pos.get('unrealized_pnl',0):,.2f}")
+                parts.append(f"• {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: ${total_open_unrealized_pnl:,.2f}</b>")
+            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("")
@@ -108,6 +112,8 @@ class StatsCommands(InfoCommandsBase):
                     await self._reply(update, "❌ Trading statistics not available.")
                     return
 
+                formatter = get_formatter()
+
                 # Format stats text
                 stats_text = "📊 <b>Trading Statistics</b>\n\n"
 
@@ -128,12 +134,12 @@ class StatsCommands(InfoCommandsBase):
                 # Add total P&L
                 total_pnl = trading_stats.get('total_pnl', 0.0)
                 pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-                stats_text += f"{pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+                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: ${avg_pnl:,.2f}\n"
+                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)
@@ -152,14 +158,14 @@ class StatsCommands(InfoCommandsBase):
                 # Add largest win and loss
                 largest_win = trading_stats.get('largest_win', 0.0)
                 largest_loss = trading_stats.get('largest_loss', 0.0)
-                stats_text += f"🏆 Largest Win: ${largest_win:,.2f}\n"
-                stats_text += f"💔 Largest Loss: ${largest_loss:,.2f}\n"
+                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: ${avg_win:,.2f}\n"
-                stats_text += f"📉 Average Loss: ${avg_loss:,.2f}\n"
+                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)

+ 1 - 1
trading_bot.py

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