Ver código fonte

Enhance performance command to provide detailed token statistics and improve user feedback. Updated the performance display to include comprehensive metrics such as total P&L, ROE, and recent trades. Improved error handling for cases with no trading data and refined the formatting for better readability. Additionally, added a new method for formatting token-specific statistics, ensuring consistent presentation across the application.

Carles Sentis 1 semana atrás
pai
commit
1675f1247e
3 arquivos alterados com 307 adições e 121 exclusões
  1. 229 62
      src/commands/info/performance.py
  2. 77 58
      src/commands/info/risk.py
  3. 1 1
      trading_bot.py

+ 229 - 62
src/commands/info/performance.py

@@ -43,33 +43,68 @@ class PerformanceCommands(InfoCommandsBase):
             # Get performance data for all tokens
             token_performance = stats.get_token_performance()
             if not token_performance:
-                await self._reply(update, "📊 No performance data available yet")
+                await self._reply(update, 
+                    "📊 <b>Token Performance</b>\n\n"
+                    "📭 No trading data available yet.\n\n"
+                    "💡 Performance tracking starts after your first completed trades.\n"
+                    "Use /long or /short to start trading!",
+                    parse_mode='HTML'
+                )
                 return
 
-            # Sort tokens by P&L
-            sorted_tokens = sorted(
-                token_performance,
-                key=lambda x: x.get('total_pnl', 0),
-                reverse=True
-            )
-
-            formatter = get_formatter()
-            performance_text = ["📊 <b>Token Performance Ranking</b>"]
-
-            for data in sorted_tokens:
-                token = data.get('token', 'Unknown')
-                total_pnl = data.get('total_pnl', 0)
-                total_trades = data.get('total_trades', 0)
-                win_rate = data.get('win_rate', 0)
+            # Tokens are already sorted by P&L from get_token_performance
+            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):
+                # Ranking emoji
+                if i == 1:
+                    rank_emoji = "🥇"
+                elif i == 2:
+                    rank_emoji = "🥈"
+                elif i == 3:
+                    rank_emoji = "🥉"
+                else:
+                    rank_emoji = f"#{i}"
                 
-                if total_trades > 0:
-                    pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-                    performance_text.append(
-                        f"\n<b>{token}</b>: {pnl_emoji} {formatter.format_price_with_symbol(total_pnl)} "
-                        f"({total_trades} trades, {win_rate:.1f}% win rate)"
-                    )
-
-            await self._reply(update, "\n".join(performance_text))
+                total_pnl = stats_data.get('total_pnl', 0)
+                roe_percentage = stats_data.get('roe_percentage', 0)
+                
+                # ROE emoji (shows performance efficiency)
+                roe_emoji = "🟢" if roe_percentage >= 0 else "🔴"
+                
+                # P&L emoji (primary ranking metric)
+                pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+                
+                token_name = stats_data.get('token', 'N/A')
+                completed_trades = stats_data.get('total_trades', 0)
+                
+                # Format the line - show both P&L (primary) and ROE (efficiency)
+                performance_text += f"{rank_emoji} <b>{token_name}</b>\n"
+                performance_text += f"   {pnl_emoji} P&L: ${total_pnl:,.2f} | {roe_emoji} ROE: {roe_percentage:+.2f}%\n"
+                performance_text += f"   📊 Trades: {completed_trades}"
+                
+                # Add win rate if there are completed trades
+                if completed_trades > 0:
+                    win_rate = stats_data.get('win_rate', 0)
+                    performance_text += f" | Win: {win_rate:.0f}%"
+                
+                performance_text += "\n\n"
+            
+            # Add summary
+            total_pnl = sum(stats_data.get('total_pnl', 0) for stats_data in token_performance)
+            total_trades = sum(stats_data.get('total_trades', 0) for stats_data in token_performance)
+            total_pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+            
+            performance_text += f"💼 <b>Portfolio Summary:</b>\n"
+            performance_text += f"   {total_pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+            performance_text += f"   📈 Tokens Traded: {len(token_performance)}\n"
+            performance_text += f"   🔄 Completed Trades: {total_trades}\n\n"
+            
+            performance_text += f"💡 <b>Usage:</b> <code>/performance BTC</code> for detailed {Config.DEFAULT_TRADING_TOKEN} stats"
+            
+            await self._reply(update, performance_text.strip(), parse_mode='HTML')
 
         except Exception as e:
             error_message = f"❌ Error showing performance ranking: {str(e)}"
@@ -79,50 +114,182 @@ class PerformanceCommands(InfoCommandsBase):
     async def _show_token_performance(self, update: Update, token: str, stats) -> None:
         """Show detailed performance for a specific token."""
         try:
-            # Get performance data for the token
-            token_performance = stats.get_token_performance(token)
-            if not token_performance:
-                await self._reply(update, f"📊 No performance data available for {token}")
+            token_stats = stats.get_token_detailed_stats(token)
+            formatter = get_formatter()
+            
+            # Check if token has any data
+            if token_stats.get('summary_total_trades', 0) == 0:
+                await self._reply(update, 
+                    f"📊 <b>{token} Performance</b>\n\n"
+                    f"📭 No trading history found for {token}.\n\n"
+                    f"💡 Start trading {token} with:\n"
+                    f"• <code>/long {token} 100</code>\n"
+                    f"• <code>/short {token} 100</code>\n\n"
+                    f"🔄 Use <code>/performance</code> to see all token rankings.",
+                    parse_mode='HTML'
+                )
                 return
+            
+            # Check if there's a message (no completed trades)
+            perf_summary = token_stats.get('performance_summary', {})
+            if 'message' in token_stats and perf_summary.get('completed_trades', 0) == 0:
+                total_volume_str = formatter.format_price_with_symbol(token_stats.get('total_volume', 0), quote_asset=Config.QUOTE_CURRENCY)
+                await self._reply(update,
+                    f"📊 <b>{token} Performance</b>\n\n"
+                    f"{token_stats['message']}\n\n"
+                    f"📈 <b>Current Activity:</b>\n"
+                    f"• Total Trades: {token_stats.get('summary_total_trades', 0)}\n"
+                    f"• Buy Orders: {token_stats.get('buy_trades', 0)}\n"
+                    f"• Sell Orders: {token_stats.get('sell_trades', 0)}\n"
+                    f"• Volume: {total_volume_str}\n\n"
+                    f"💡 Complete some trades to see P&L statistics!\n"
+                    f"🔄 Use <code>/performance</code> to see all token rankings.",
+                    parse_mode='HTML'
+                )
+                return
+            
+            # Detailed stats display - use performance_summary data
+            perf_summary = token_stats.get('performance_summary', {})
+            pnl_emoji = "🟢" if perf_summary.get('total_pnl', 0) >= 0 else "🔴"
+            
+            total_pnl_str = formatter.format_price_with_symbol(perf_summary.get('total_pnl', 0))
+            completed_volume_str = formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0))
+            expectancy_str = formatter.format_price_with_symbol(perf_summary.get('expectancy', 0))
+            largest_win_str = formatter.format_price_with_symbol(perf_summary.get('largest_win', 0))
+            largest_loss_str = formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0))
+            avg_win_str = formatter.format_price_with_symbol(perf_summary.get('avg_win', 0))
+            avg_loss_str = formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0))
+            
+            # Calculate ROE (Return on Equity)
+            entry_vol = perf_summary.get('completed_entry_volume', 0)
+            roe_pct = (perf_summary.get('total_pnl', 0) / entry_vol * 100) if entry_vol > 0 else 0
+            roe_emoji = "🟢" if roe_pct >= 0 else "🔴"
 
-            formatter = get_formatter()
-            performance_text = [f"📊 <b>{token} Performance</b>"]
-
-            # Add summary statistics
-            total_pnl = token_performance.get('total_pnl', 0)
-            total_trades = token_performance.get('total_trades', 0)
-            win_rate = token_performance.get('win_rate', 0)
-            avg_trade = token_performance.get('avg_trade', 0)
-            largest_win = token_performance.get('largest_win', 0)
-            largest_loss = token_performance.get('largest_loss', 0)
-
-            pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
-            performance_text.extend([
-                f"\n<b>Summary:</b>",
-                f"• Total P&L: {pnl_emoji} {formatter.format_price_with_symbol(total_pnl)}",
-                f"• Total Trades: {total_trades}",
-                f"• Win Rate: {win_rate:.1f}%",
-                f"• Avg Trade: {formatter.format_price_with_symbol(avg_trade)}",
-                f"• Largest Win: {formatter.format_price_with_symbol(largest_win)}",
-                f"• Largest Loss: {formatter.format_price_with_symbol(largest_loss)}"
-            ])
+            performance_text = f"""
+📊 <b>{token} Detailed Performance</b>
+
+🎯 <b>Performance Summary:</b>
+• {roe_emoji} ROE (Return on Equity): {roe_pct:+.2f}%
+• {pnl_emoji} Total P&L: {total_pnl_str}
+• 💵 Total Volume: {completed_volume_str}
+• 📈 Expectancy: {expectancy_str}
 
+📊 <b>Trading Activity:</b>
+• Total Trades: {token_stats.get('summary_total_trades', 0)}
+• Completed: {perf_summary.get('completed_trades', 0)}
+• Buy Orders: {token_stats.get('buy_trades', 0)}
+• Sell Orders: {token_stats.get('sell_trades', 0)}
+
+🏆 <b>Performance Metrics:</b>
+• Win Rate: {perf_summary.get('win_rate', 0):.1f}%
+• Profit Factor: {perf_summary.get('profit_factor', 0):.2f}
+• Wins: {perf_summary.get('total_wins', 0)} | Losses: {perf_summary.get('total_losses', 0)}
+
+💡 <b>Best/Worst:</b>
+• Largest Win: {largest_win_str}
+• Largest Loss: {largest_loss_str}
+• Avg Win: {avg_win_str}
+• Avg Loss: {avg_loss_str}
+            """
+            
             # Add recent trades if available
-            recent_trades = token_performance.get('recent_trades', [])
-            if recent_trades:
-                performance_text.append("\n<b>Recent Trades:</b>")
-                for trade in recent_trades[:5]:  # Show last 5 trades
-                    side = trade.get('side', '').upper()
-                    pnl = trade.get('pnl', 0)
-                    pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-                    performance_text.append(
-                        f"• {side}: {pnl_emoji} {formatter.format_price_with_symbol(pnl)} "
-                        f"({trade.get('entry_price', 0)} → {trade.get('exit_price', 0)})"
-                    )
-
-            await self._reply(update, "\n".join(performance_text))
+            if token_stats.get('recent_trades'):
+                performance_text += f"\n🔄 <b>Recent Trades:</b>\n"
+                for trade in token_stats['recent_trades'][-3:]:  # Last 3 trades
+                    trade_time = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
+                    side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
+                    
+                    # trade_symbol is required for format_price and format_amount
+                    trade_symbol = trade.get('symbol', token) # Fallback to token if symbol not in trade dict
+                    trade_base_asset = trade_symbol.split('/')[0] if '/' in trade_symbol else trade_symbol
+                    
+                    # Formatting trade value. Assuming 'value' is in quote currency.
+                    trade_value_str = formatter.format_price_with_symbol(trade.get('value', 0))
+                    
+                    pnl_display_str = ""
+                    if trade.get('pnl', 0) != 0:
+                        trade_pnl_str = formatter.format_price_with_symbol(trade.get('pnl', 0))
+                        pnl_display_str = f" | P&L: {trade_pnl_str}"
+                    
+                    performance_text += f"• {side_emoji} {trade['side'].upper()} {trade_value_str} @ {trade_time}{pnl_display_str}\n"
+            
+            performance_text += f"\n🔄 Use <code>/performance</code> to see all token rankings"
+            
+            await self._reply(update, performance_text.strip(), parse_mode='HTML')
 
         except Exception as e:
             error_message = f"❌ Error showing token performance: {str(e)}"
             await self._reply(update, error_message)
             logger.error(f"Error in _show_token_performance: {e}")
+
+    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"
+                f"📭 No trading data found for {token_name}.\n\n"
+                f"💡 To trade this token, try commands like:\n"
+                f"   <code>/long {token_name} 100</code>\n"
+                f"   <code>/short {token_name} 100</code>"
+            )
+
+        perf_summary = token_stats_data.get('performance_summary', {})
+        open_positions = token_stats_data.get('open_positions', [])
+        
+        parts = [f"📊 <b>{token_name.upper()} Detailed Statistics</b>\n"]
+
+        # Completed Trades Summary (from token_stats table)
+        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 "🔴"
+            # Calculate PnL percentage from entry volume
+            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("") # Newline
+
+        # 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 # Keep N/A
+                
+                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("") # Newline
+
+        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)

+ 77 - 58
src/commands/info/risk.py

@@ -37,73 +37,92 @@ class RiskCommands(InfoCommandsBase):
 
             # Analyze each position
             for position in positions:
-                symbol = position['symbol']
-                token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                side = position['position_side']
-                size = position['current_position_size']
-                entry_price = position['entry_price']
-                current_price = position.get('current_price', entry_price)
-                
-                # 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)))
+                try:
+                    symbol = position.get('symbol', '')
+                    if not symbol:
+                        continue
+
+                    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
+
+                    # Calculate unrealized P&L
                     if side == 'long':
-                        stop_loss_price = float(stop_loss_orders[0].get('triggerPrice', 0))
+                        unrealized_pnl = size * (current_price - entry_price)
                     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("")
+                        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("")
+
+                except (ValueError, TypeError) as e:
+                    logger.warning(f"Error processing position {position.get('symbol', 'unknown')}: {e}")
+                    continue
 
             # Add portfolio-level risk metrics
-            total_position_value = sum(
-                abs(float(pos.get('current_position_size', 0)) * float(pos.get('current_price', 0)))
-                for pos in positions
-            )
+            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
             
             # Get account balance
             balance = self.trading_engine.get_balance()
             if balance:
-                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")
+                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}")
 
-            await self._reply(update, "\n".join(risk_text_parts).strip())
+            await self._reply(update, "\n".join(risk_text_parts).strip(), parse_mode='HTML')
 
         except Exception as e:
             error_message = f"❌ Error processing risk command: {str(e)}"

+ 1 - 1
trading_bot.py

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