Browse Source

Add leverage command and associated functionality to trading bot

- Introduced /leverage command to set leverage for a trading token.
- Implemented confirmation dialog for leverage adjustments.
- Enhanced trading_commands to handle leverage-related callbacks.
- Updated trading_engine to support leverage setting for symbols.
- Modified token_display_formatter to normalize token case consistently.
- Updated documentation to reflect new leverage command usage.
Carles Sentis 2 days ago
parent
commit
276920b7b4

+ 7 - 0
src/bot/core.py

@@ -131,6 +131,7 @@ class TelegramTradingBot:
         self.application.add_handler(CommandHandler("sl", self.trading_commands.sl_command))
         self.application.add_handler(CommandHandler("tp", self.trading_commands.tp_command))
         self.application.add_handler(CommandHandler("coo", self.trading_commands.coo_command))
+        self.application.add_handler(CommandHandler("leverage", self.trading_commands.leverage_command))
         
         # Info commands
         self.application.add_handler(CommandHandler("balance", self.balance_cmds.balance_command))
@@ -159,6 +160,10 @@ class TelegramTradingBot:
         
         # Callback and message handlers
         self.application.add_handler(CallbackQueryHandler(self.trading_commands.button_callback))
+        self.application.add_handler(MessageHandler(
+            filters.Regex(r'^(LONG|SHORT|EXIT|SL|TP|LEVERAGE|BALANCE|POSITIONS|ORDERS|STATS|MARKET|PERFORMANCE|DAILY|WEEKLY|MONTHLY|RISK|ALARM|MONITORING|LOGS|DEBUG|VERSION|COMMANDS|KEYBOARD|COO)'),
+            self.trading_commands.handle_keyboard_command
+        ))
         
     async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /start command."""
@@ -212,6 +217,7 @@ class TelegramTradingBot:
 • Order Stop Loss: Use sl:price parameter
 • /sl {Config.DEFAULT_TRADING_TOKEN} 44000 - Manual stop loss
 • /tp {Config.DEFAULT_TRADING_TOKEN} 50000 - Take profit order
+• /leverage {Config.DEFAULT_TRADING_TOKEN} 10 - Set leverage to 10x
 
 <b>📈 Performance & Analytics:</b>
 • /stats - Complete trading statistics
@@ -303,6 +309,7 @@ For support, contact your bot administrator.
 • /sl [token] [price] - Set stop loss order
 • /tp [token] [price] - Set take profit order
 • /coo [token] - Cancel all open orders
+• /leverage [token] [leverage] - Set leverage
 
 📊 <b>Account Info:</b>
 • /balance - Account balance and equity

+ 67 - 2
src/commands/trading_commands.py

@@ -9,7 +9,7 @@ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
 from telegram.ext import ContextTypes
 
 from src.config.config import Config
-from src.utils.token_display_formatter import get_formatter
+from src.utils.token_display_formatter import get_formatter, normalize_token_case
 
 logger = logging.getLogger(__name__)
 
@@ -722,6 +722,8 @@ This action cannot be undone.
             await self._execute_coo_callback(query, callback_data)
         elif callback_data == 'cancel_order':
             await query.edit_message_text("❌ Order cancelled.")
+        elif callback_data.startswith("leverage_confirm"):
+            await self._execute_leverage_callback(query, callback_data)
     
     async def _execute_long_callback(self, query, callback_data):
         """Execute long order from callback."""
@@ -885,4 +887,67 @@ This action cannot be undone.
                 query, token, cancelled_count, failed_count, cancelled_linked_sls, cancelled_orders
             )
         else:
-            await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}") 
+            await query.edit_message_text(f"❌ Cancel orders failed: {result['error']}")
+    
+    async def _execute_leverage_callback(self, query, callback_data):
+        """Execute leverage command from callback."""
+        parts = callback_data.split('_')
+        if parts[1] == "yes":
+            token = parts[2]
+            leverage = int(parts[3])
+            symbol = f"{token}/USDC:USDC"
+            
+            await query.edit_message_text(text=f"Setting leverage for {token} to {leverage}x...")
+            
+            result = self.trading_engine.set_leverage(leverage, symbol)
+            
+            if result.get("success"):
+                await query.edit_message_text(text=f"✅ Leverage for <b>{token}</b> successfully set to <b>{leverage}x</b>.", parse_mode='HTML')
+            else:
+                error_message = result.get("error", "Unknown error")
+                await query.edit_message_text(text=f"❌ Failed to set leverage for <b>{token}</b>. Reason: {error_message}", parse_mode='HTML')
+        else: # 'no'
+            await query.edit_message_text(text="Leverage adjustment cancelled.")
+    
+    async def leverage_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Command to set leverage for a symbol. Format: /leverage [token] [leverage]"""
+        chat_id = update.effective_chat.id
+        try:
+            parts = update.message.text.split()
+            if len(parts) != 3:
+                await self.notification_manager.send_generic_notification(
+                    "Format: /leverage [token] [leverage_value]", chat_id=chat_id
+                )
+                return
+
+            token = normalize_token_case(parts[1])
+            leverage = int(parts[2])
+            
+            if leverage <= 0:
+                await self.notification_manager.send_generic_notification(
+                    "Leverage must be a positive number.", chat_id=chat_id
+                )
+                return
+
+            symbol = f"{token}/USDC:USDC"
+
+            # Confirmation dialog
+            keyboard = [
+                [
+                    InlineKeyboardButton("✅ Yes, Set Leverage", callback_data=f"leverage_confirm_yes_{token}_{leverage}"),
+                    InlineKeyboardButton("❌ No, Cancel", callback_data="leverage_confirm_no"),
+                ]
+            ]
+            reply_markup = InlineKeyboardMarkup(keyboard)
+            
+            await update.message.reply_html(
+                f"Are you sure you want to set leverage for <b>{token}</b> to <b>{leverage}x</b>?",
+                reply_markup=reply_markup,
+            )
+        except (IndexError, ValueError):
+            await self.notification_manager.send_generic_notification(
+                "Invalid format. Use: /leverage [token] [leverage_value]", chat_id=chat_id
+            )
+        except Exception as e:
+            logger.error(f"Error in leverage_command: {e}", exc_info=True)
+            await self.notification_manager.send_generic_notification(f"An error occurred: {e}", chat_id=chat_id) 

+ 152 - 256
src/stats/trading_stats.py

@@ -17,17 +17,10 @@ from .order_manager import OrderManager
 from .trade_lifecycle_manager import TradeLifecycleManager
 from .aggregation_manager import AggregationManager
 from .performance_calculator import PerformanceCalculator
-from src.utils.token_display_formatter import get_formatter
+from src.utils.token_display_formatter import get_formatter, normalize_token_case
 
 logger = logging.getLogger(__name__)
 
-def _normalize_token_case(token: str) -> str:
-    """Normalize token case for consistency."""
-    if any(c.isupper() for c in token):
-        return token  # Keep original case for mixed-case tokens
-    else:
-        return token.upper()  # Convert to uppercase for all-lowercase
-
 class TradingStats:
     """Refactored trading statistics tracker using modular components."""
 
@@ -340,100 +333,14 @@ class TradingStats:
         return self.performance_calculator.get_performance_stats()
     
     def get_token_performance(self, token: Optional[str] = None) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
-        """Get performance data for a specific token or all tokens."""
-        try:
-            if token:
-                # Get performance for specific token
-                query = """
-                    SELECT 
-                        symbol,
-                        COUNT(*) as total_trades,
-                        SUM(CASE WHEN realized_pnl > 0 THEN 1 ELSE 0 END) as winning_trades,
-                        SUM(realized_pnl) as total_pnl,
-                        AVG(realized_pnl) as avg_trade,
-                        MAX(realized_pnl) as largest_win,
-                        MIN(realized_pnl) as largest_loss,
-                        AVG(CASE WHEN realized_pnl > 0 THEN realized_pnl ELSE NULL END) as avg_win,
-                        AVG(CASE WHEN realized_pnl < 0 THEN realized_pnl ELSE NULL END) as avg_loss
-                    FROM trades
-                    WHERE symbol = ? AND status = 'position_closed'
-                    GROUP BY symbol
-                """
-                result = self.db_manager._fetchone_query(query, (token,))
-                
-                if not result:
-                    return {}
-                
-                # Calculate win rate
-                total_trades = result['total_trades']
-                winning_trades = result['winning_trades']
-                win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
-                
-                # Get recent trades
-                recent_trades_query = """
-                    SELECT 
-                        side,
-                        entry_price,
-                        exit_price,
-                        realized_pnl as pnl,
-                        position_opened_at,
-                        position_closed_at
-                    FROM trades
-                    WHERE symbol = ? AND status = 'position_closed'
-                    ORDER BY position_closed_at DESC
-                    LIMIT 5
-                """
-                recent_trades = self.db_manager._fetch_query(recent_trades_query, (token,))
-                
-                return {
-                    'token': token,
-                    'total_trades': total_trades,
-                    'winning_trades': winning_trades,
-                    'win_rate': win_rate,
-                    'total_pnl': result['total_pnl'],
-                    'avg_trade': result['avg_trade'],
-                    'largest_win': result['largest_win'],
-                    'largest_loss': result['largest_loss'],
-                    'avg_win': result['avg_win'],
-                    'avg_loss': result['avg_loss'],
-                    'recent_trades': recent_trades
-                }
-            else:
-                # Get performance for all tokens
-                query = """
-                    SELECT 
-                        symbol,
-                        COUNT(*) as total_trades,
-                        SUM(CASE WHEN realized_pnl > 0 THEN 1 ELSE 0 END) as winning_trades,
-                        SUM(realized_pnl) as total_pnl,
-                        AVG(realized_pnl) as avg_trade
-                    FROM trades
-                    WHERE status = 'position_closed'
-                    GROUP BY symbol
-                    ORDER BY total_pnl DESC
-                """
-                results = self.db_manager._fetch_query(query)
-                
-                performance_data = []
-                for result in results:
-                    total_trades = result['total_trades']
-                    winning_trades = result['winning_trades']
-                    win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
-                    
-                    performance_data.append({
-                        'token': result['symbol'],
-                        'total_trades': total_trades,
-                        'winning_trades': winning_trades,
-                        'win_rate': win_rate,
-                        'total_pnl': result['total_pnl'],
-                        'avg_trade': result['avg_trade']
-                    })
-                
-                return performance_data
-                
-        except Exception as e:
-            logger.error(f"Error getting token performance: {e}")
-            return [] if token is None else {}
+        """
+        Get performance statistics for a specific token or all tokens.
+        """
+        if token:
+            normalized_token = normalize_token_case(token)
+            return self.performance_calculator.get_single_token_performance(normalized_token)
+        else:
+            return self.performance_calculator.get_all_token_performance()
     
     def get_balance_history(self, days: int = 30) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
         """Get balance history."""
@@ -565,132 +472,129 @@ class TradingStats:
         return row['count'] if row else 0
 
     def get_token_detailed_stats(self, token: str) -> Dict[str, Any]:
-        """Get detailed statistics for a specific token."""
-        try:
-            # Normalize token case
-            upper_token = _normalize_token_case(token)
-            
-            # Get aggregated stats from token_stats table
-            token_agg_stats = self.db_manager._fetchone_query(
-                "SELECT * FROM token_stats WHERE token = ?", (upper_token,)
-            )
-            
-            # Get open trades for this token
-            open_trades_for_token = self.db_manager._fetch_query(
-                "SELECT * FROM trades WHERE status = 'position_opened' AND symbol LIKE ? ORDER BY position_opened_at DESC",
-                (f"{upper_token}/%",)
-            )
-            
-            # Initialize performance stats
-            perf_stats = {
-                'completed_trades': 0,
-                'total_pnl': 0.0,
-                'pnl_percentage': 0.0,
-                'win_rate': 0.0,
-                'profit_factor': 0.0,
-                'avg_win': 0.0,
-                'avg_loss': 0.0,
-                'largest_win': 0.0,
-                'largest_loss': 0.0,
-                'expectancy': 0.0,
-                'total_wins': 0,
-                'total_losses': 0,
-                'completed_entry_volume': 0.0,
-                'completed_exit_volume': 0.0,
-                'total_cancelled': 0,
-                'total_duration_seconds': 0,
-                'avg_trade_duration': "N/A"
-            }
-            
-            if token_agg_stats:
-                total_cycles = token_agg_stats.get('total_completed_cycles', 0)
-                winning_cycles = token_agg_stats.get('winning_cycles', 0)
-                losing_cycles = token_agg_stats.get('losing_cycles', 0)
-                sum_winning_pnl = token_agg_stats.get('sum_of_winning_pnl', 0.0)
-                sum_losing_pnl = token_agg_stats.get('sum_of_losing_pnl', 0.0)
-                
-                # Calculate percentages for largest trades
-                largest_win_pnl = token_agg_stats.get('largest_winning_cycle_pnl', 0.0)
-                largest_loss_pnl = token_agg_stats.get('largest_losing_cycle_pnl', 0.0)
-                largest_win_entry_volume = token_agg_stats.get('largest_winning_cycle_entry_volume', 0.0)
-                largest_loss_entry_volume = token_agg_stats.get('largest_losing_cycle_entry_volume', 0.0)
-                
-                largest_win_percentage = (largest_win_pnl / largest_win_entry_volume * 100) if largest_win_entry_volume > 0 else 0.0
-                largest_loss_percentage = (largest_loss_pnl / largest_loss_entry_volume * 100) if largest_loss_entry_volume > 0 else 0.0
-                
-                perf_stats.update({
-                    'completed_trades': total_cycles,
-                    'total_pnl': token_agg_stats.get('total_realized_pnl', 0.0),
-                    'win_rate': (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0.0,
-                    'profit_factor': (sum_winning_pnl / sum_losing_pnl) if sum_losing_pnl > 0 else float('inf') if sum_winning_pnl > 0 else 0.0,
-                    'avg_win': (sum_winning_pnl / winning_cycles) if winning_cycles > 0 else 0.0,
-                    'avg_loss': (sum_losing_pnl / losing_cycles) if losing_cycles > 0 else 0.0,
-                    'largest_win': largest_win_pnl,
-                    'largest_loss': largest_loss_pnl,
-                    'largest_win_percentage': largest_win_percentage,
-                    'largest_loss_percentage': largest_loss_percentage,
-                    'total_wins': winning_cycles,
-                    'total_losses': losing_cycles,
-                    'completed_entry_volume': token_agg_stats.get('total_entry_volume', 0.0),
-                    'completed_exit_volume': token_agg_stats.get('total_exit_volume', 0.0),
-                    'total_cancelled': token_agg_stats.get('total_cancelled_cycles', 0),
-                    'total_duration_seconds': token_agg_stats.get('total_duration_seconds', 0)
-                })
-                
-                # Calculate expectancy
-                win_rate_decimal = perf_stats['win_rate'] / 100
-                perf_stats['expectancy'] = (perf_stats['avg_win'] * win_rate_decimal) - (perf_stats['avg_loss'] * (1 - win_rate_decimal))
-                
-                # Format average trade duration
-                if total_cycles > 0:
-                    avg_duration_seconds = token_agg_stats.get('total_duration_seconds', 0) / total_cycles
-                    perf_stats['avg_trade_duration'] = self._format_duration(avg_duration_seconds)
+        """
+        Retrieves detailed trading statistics for a single token, including calculations
+        not directly stored in the database.
+        """
+        normalized_token = normalize_token_case(token)
+        
+        # Get raw stats from DB
+        token_stats = self.db_manager._fetchone_query(
+            "SELECT * FROM token_stats WHERE token = ?", (normalized_token,)
+        )
+        
+        # Get open trades for this token
+        open_trades_for_token = self.db_manager._fetch_query(
+            "SELECT * FROM trades WHERE status = 'position_opened' AND symbol LIKE ? ORDER BY position_opened_at DESC",
+            (f"{normalized_token}/%",)
+        )
+        
+        # Initialize performance stats
+        perf_stats = {
+            'completed_trades': 0,
+            'total_pnl': 0.0,
+            'pnl_percentage': 0.0,
+            'win_rate': 0.0,
+            'profit_factor': 0.0,
+            'avg_win': 0.0,
+            'avg_loss': 0.0,
+            'largest_win': 0.0,
+            'largest_loss': 0.0,
+            'expectancy': 0.0,
+            'total_wins': 0,
+            'total_losses': 0,
+            'completed_entry_volume': 0.0,
+            'completed_exit_volume': 0.0,
+            'total_cancelled': 0,
+            'total_duration_seconds': 0,
+            'avg_trade_duration': "N/A"
+        }
+        
+        if token_stats:
+            total_cycles = token_stats.get('total_completed_cycles', 0)
+            winning_cycles = token_stats.get('winning_cycles', 0)
+            losing_cycles = token_stats.get('losing_cycles', 0)
+            sum_winning_pnl = token_stats.get('sum_of_winning_pnl', 0.0)
+            sum_losing_pnl = token_stats.get('sum_of_losing_pnl', 0.0)
             
-            # Calculate open positions summary
-            open_positions_summary = []
-            total_open_value = 0.0
-            total_open_unrealized_pnl = 0.0
+            # Calculate percentages for largest trades
+            largest_win_pnl = token_stats.get('largest_winning_cycle_pnl', 0.0)
+            largest_loss_pnl = token_stats.get('largest_losing_cycle_pnl', 0.0)
+            largest_win_entry_volume = token_stats.get('largest_winning_cycle_entry_volume', 0.0)
+            largest_loss_entry_volume = token_stats.get('largest_losing_cycle_entry_volume', 0.0)
             
-            for op_trade in open_trades_for_token:
-                open_positions_summary.append({
-                    'lifecycle_id': op_trade.get('trade_lifecycle_id'),
-                    'side': op_trade.get('position_side'),
-                    'amount': op_trade.get('current_position_size'),
-                    'entry_price': op_trade.get('entry_price'),
-                    'mark_price': op_trade.get('mark_price'),
-                    'unrealized_pnl': op_trade.get('unrealized_pnl'),
-                    'opened_at': op_trade.get('position_opened_at')
-                })
-                total_open_value += op_trade.get('value', 0.0)
-                total_open_unrealized_pnl += op_trade.get('unrealized_pnl', 0.0)
+            largest_win_percentage = (largest_win_pnl / largest_win_entry_volume * 100) if largest_win_entry_volume > 0 else 0.0
+            largest_loss_percentage = (largest_loss_pnl / largest_loss_entry_volume * 100) if largest_loss_entry_volume > 0 else 0.0
             
-            # Get open orders count for this token
-            open_orders_count_row = self.db_manager._fetchone_query(
-                "SELECT COUNT(*) as count FROM orders WHERE symbol LIKE ? AND status IN ('open', 'submitted', 'pending_trigger')",
-                 (f"{upper_token}/%",)
-            )
-            current_open_orders_for_token = open_orders_count_row['count'] if open_orders_count_row else 0
+            perf_stats.update({
+                'completed_trades': total_cycles,
+                'total_pnl': token_stats.get('total_realized_pnl', 0.0),
+                'win_rate': (winning_cycles / total_cycles * 100) if total_cycles > 0 else 0.0,
+                'profit_factor': (sum_winning_pnl / sum_losing_pnl) if sum_losing_pnl > 0 else float('inf') if sum_winning_pnl > 0 else 0.0,
+                'avg_win': (sum_winning_pnl / winning_cycles) if winning_cycles > 0 else 0.0,
+                'avg_loss': (sum_losing_pnl / losing_cycles) if losing_cycles > 0 else 0.0,
+                'largest_win': largest_win_pnl,
+                'largest_loss': largest_loss_pnl,
+                'largest_win_percentage': largest_win_percentage,
+                'largest_loss_percentage': largest_loss_percentage,
+                'total_wins': winning_cycles,
+                'total_losses': losing_cycles,
+                'completed_entry_volume': token_stats.get('total_entry_volume', 0.0),
+                'completed_exit_volume': token_stats.get('total_exit_volume', 0.0),
+                'total_cancelled': token_stats.get('total_cancelled_cycles', 0),
+                'total_duration_seconds': token_stats.get('total_duration_seconds', 0)
+            })
             
-            effective_total_trades = perf_stats['completed_trades'] + len(open_trades_for_token)
+            # Calculate expectancy
+            win_rate_decimal = perf_stats['win_rate'] / 100
+            perf_stats['expectancy'] = (perf_stats['avg_win'] * win_rate_decimal) - (perf_stats['avg_loss'] * (1 - win_rate_decimal))
             
-            return {
-                'token': upper_token,
-                'message': f"Statistics for {upper_token}",
-                'performance_summary': perf_stats,  # Expected key by formatting method
-                'performance': perf_stats,  # Legacy compatibility
-                'open_positions': open_positions_summary,  # Direct list as expected
-                'summary_total_trades': effective_total_trades,  # Expected by formatting method
-                'summary_total_unrealized_pnl': total_open_unrealized_pnl,  # Expected by formatting method
-                'current_open_orders_count': current_open_orders_for_token,  # Expected by formatting method
-                'summary': {
-                    'total_trades': effective_total_trades,
-                    'open_orders': current_open_orders_for_token,
-                }
+            # Format average trade duration
+            if total_cycles > 0:
+                avg_duration_seconds = token_stats.get('total_duration_seconds', 0) / total_cycles
+                perf_stats['avg_trade_duration'] = self._format_duration(avg_duration_seconds)
+        
+        # Calculate open positions summary
+        open_positions_summary = []
+        total_open_value = 0.0
+        total_open_unrealized_pnl = 0.0
+        
+        for op_trade in open_trades_for_token:
+            open_positions_summary.append({
+                'lifecycle_id': op_trade.get('trade_lifecycle_id'),
+                'side': op_trade.get('position_side'),
+                'amount': op_trade.get('current_position_size'),
+                'entry_price': op_trade.get('entry_price'),
+                'mark_price': op_trade.get('mark_price'),
+                'unrealized_pnl': op_trade.get('unrealized_pnl'),
+                'opened_at': op_trade.get('position_opened_at')
+            })
+            total_open_value += op_trade.get('value', 0.0)
+            total_open_unrealized_pnl += op_trade.get('unrealized_pnl', 0.0)
+        
+        # Get open orders count for this token
+        open_orders_count_row = self.db_manager._fetchone_query(
+            "SELECT COUNT(*) as count FROM orders WHERE symbol LIKE ? AND status IN ('open', 'submitted', 'pending_trigger')",
+             (f"{normalized_token}/%",)
+        )
+        current_open_orders_for_token = open_orders_count_row['count'] if open_orders_count_row else 0
+        
+        effective_total_trades = perf_stats['completed_trades'] + len(open_trades_for_token)
+        
+        return {
+            'token': normalized_token,
+            'message': f"Statistics for {normalized_token}",
+            'performance_summary': perf_stats,  # Expected key by formatting method
+            'performance': perf_stats,  # Legacy compatibility
+            'open_positions': open_positions_summary,  # Direct list as expected
+            'summary_total_trades': effective_total_trades,  # Expected by formatting method
+            'summary_total_unrealized_pnl': total_open_unrealized_pnl,  # Expected by formatting method
+            'current_open_orders_count': current_open_orders_for_token,  # Expected by formatting method
+            'summary': {
+                'total_trades': effective_total_trades,
+                'open_orders': current_open_orders_for_token,
             }
-            
-        except Exception as e:
-            logger.error(f"❌ Error getting detailed stats for {token}: {e}")
-            return {}
+        }
 
     def _format_duration(self, seconds: float) -> str:
         """Format duration in seconds to a human-readable string."""
@@ -774,60 +678,52 @@ class TradingStats:
         return "\n".join(stats_text_parts)
 
     async def format_token_stats_message(self, token: str) -> str:
-        """Formats a statistics message for a specific token."""
-        formatter = get_formatter()
-        token_stats = self.get_token_detailed_stats(token)
-        normalized_token = _normalize_token_case(token)
-        token_name = token_stats.get('token', normalized_token.upper())
+        """Formats the statistics message for a single token."""
+        normalized_token = normalize_token_case(token)
+        token_stats = self.get_token_detailed_stats(normalized_token)
         
-        if not token_stats or token_stats.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>"
-            )
+        if not token_stats or not token_stats.get('total_trades', 0):
+            return f"📊 No trading data found for {normalized_token}."
 
-        perf_summary = token_stats.get('performance_summary', {})
-        open_positions = token_stats.get('open_positions', [])
+        formatter = get_formatter()
+        token_name = token_stats.get('token', normalized_token.upper())
         
         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
+        if token_stats.get('completed_trades', 0) > 0:
+            pnl_emoji = "✅" if token_stats.get('total_pnl', 0) >= 0 else "🔻"
+            entry_vol = token_stats.get('completed_entry_volume', 0.0)
+            pnl_pct = (token_stats.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: {await 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: {await formatter.format_price_with_symbol(perf_summary.get('expectancy', 0.0))}")
-            parts.append(f"• Avg Win: {await formatter.format_price_with_symbol(perf_summary.get('avg_win', 0.0))} | Avg Loss: {await formatter.format_price_with_symbol(perf_summary.get('avg_loss', 0.0))}")
+            parts.append(f"• Total Completed: {token_stats.get('completed_trades', 0)}")
+            parts.append(f"• {pnl_emoji} Realized P&L: {await formatter.format_price_with_symbol(token_stats.get('total_pnl', 0.0))} ({pnl_pct:+.2f}%)")
+            parts.append(f"• Win Rate: {token_stats.get('win_rate', 0.0):.1f}% ({token_stats.get('total_wins', 0)}W / {token_stats.get('total_losses', 0)}L)")
+            parts.append(f"• Profit Factor: {token_stats.get('profit_factor', 0.0):.2f}")
+            parts.append(f"• Expectancy: {await formatter.format_price_with_symbol(token_stats.get('expectancy', 0.0))}")
+            parts.append(f"• Avg Win: {await formatter.format_price_with_symbol(token_stats.get('avg_win', 0.0))} | Avg Loss: {await formatter.format_price_with_symbol(token_stats.get('avg_loss', 0.0))}")
             
             # Format largest trades with percentages
-            largest_win_pct_str = f" ({perf_summary.get('largest_win_entry_pct', 0):.2f}%)" if perf_summary.get('largest_win_entry_pct') is not None else ""
-            largest_loss_pct_str = f" ({perf_summary.get('largest_loss_entry_pct', 0):.2f}%)" if perf_summary.get('largest_loss_entry_pct') is not None else ""
+            largest_win_pct_str = f" ({token_stats.get('largest_win_entry_pct', 0):.2f}%)" if token_stats.get('largest_win_entry_pct') is not None else ""
+            largest_loss_pct_str = f" ({token_stats.get('largest_loss_entry_pct', 0):.2f}%)" if token_stats.get('largest_loss_entry_pct') is not None else ""
             
-            parts.append(f"• Largest Win: {await formatter.format_price_with_symbol(perf_summary.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {await formatter.format_price_with_symbol(perf_summary.get('largest_loss', 0.0))}{largest_loss_pct_str}")
-            parts.append(f"• Entry Volume: {await formatter.format_price_with_symbol(perf_summary.get('completed_entry_volume', 0.0))}")
-            parts.append(f"• Exit Volume: {await 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)}")
+            parts.append(f"• Largest Win: {await formatter.format_price_with_symbol(token_stats.get('largest_win', 0.0))}{largest_win_pct_str} | Largest Loss: {await formatter.format_price_with_symbol(token_stats.get('largest_loss', 0.0))}{largest_loss_pct_str}")
+            parts.append(f"• Entry Volume: {await formatter.format_price_with_symbol(token_stats.get('completed_entry_volume', 0.0))}")
+            parts.append(f"• Exit Volume: {await formatter.format_price_with_symbol(token_stats.get('completed_exit_volume', 0.0))}")
+            parts.append(f"• Average Trade Duration: {token_stats.get('avg_trade_duration', 'N/A')}")
+            parts.append(f"• Cancelled Cycles: {token_stats.get('total_cancelled', 0)}")
         else:
             parts.append("• No completed trades for this token yet.")
         parts.append("")
 
         # Open Positions
         parts.append("📉 <b>Current Open Positions:</b>")
-        if open_positions:
+        if token_stats.get('open_positions', []):
             total_open_unrealized_pnl = token_stats.get('summary_total_unrealized_pnl', 0.0)
             open_pnl_emoji = "✅" if total_open_unrealized_pnl >= 0 else "🔻"
             
-            for pos in open_positions:
+            for pos in token_stats.get('open_positions', []):
                 pos_side_emoji = "🔼" if pos.get('side', 'buy').lower() == 'buy' else "🔽"
                 pos_pnl_emoji = "✅" if pos.get('unrealized_pnl', 0) >= 0 else "🔻"
                 opened_at_str = "N/A"

+ 20 - 1
src/trading/trading_engine.py

@@ -1205,4 +1205,23 @@ class TradingEngine:
                 "current_market_price": current_price,
                 "used_market_order": use_market_order
             }
-        } 
+        }
+
+    def set_leverage(self, leverage: int, symbol: str = None, params={}):
+        """Sets leverage for a symbol."""
+        try:
+            if not symbol:
+                symbol = self.default_symbol
+            
+            logger.info(f"Setting leverage for {symbol} to {leverage}x")
+            
+            # The symbol format for CCXT might need to be 'ETH/USDC:USDC'
+            # Let's ensure it is correctly formatted
+            market = self.exchange.market(symbol)
+            
+            response = self.exchange.set_leverage(leverage, market['symbol'], params)
+            logger.info(f"✅ Successfully set leverage for {symbol} to {leverage}x. Response: {response}")
+            return {"success": True, "response": response}
+        except Exception as e:
+            logger.error(f"❌ Error setting leverage for {symbol}: {e}", exc_info=True)
+            return {"success": False, "error": str(e)} 

+ 4 - 4
src/utils/token_display_formatter.py

@@ -9,7 +9,7 @@ from functools import lru_cache
 
 logger = logging.getLogger(__name__)
 
-def _normalize_token_case(token: str) -> str:
+def normalize_token_case(token: str) -> str:
     """
     Normalize token case: if any characters are already uppercase, keep as-is.
     Otherwise, convert to uppercase. This handles mixed-case tokens like kPEPE, kBONK.
@@ -55,7 +55,7 @@ class TokenDisplayFormatter:
         Fetches price and amount precisions for a token from market data and caches them.
         Returns the cached dict {'price_decimals': X, 'amount_decimals': Y} or None if not found.
         """
-        normalized_token = _normalize_token_case(token)
+        normalized_token = normalize_token_case(token)
         if normalized_token in self._precision_cache:
             return self._precision_cache[normalized_token]
 
@@ -104,7 +104,7 @@ class TokenDisplayFormatter:
         """
         Get the number of decimal places for a token's price.
         """
-        normalized_token = _normalize_token_case(token)
+        normalized_token = normalize_token_case(token)
         precisions = self._precision_cache.get(normalized_token)
         if not precisions:
             precisions = await self._fetch_and_cache_precisions(normalized_token)
@@ -119,7 +119,7 @@ class TokenDisplayFormatter:
         """
         Get the number of decimal places for a token's amount (quantity).
         """
-        normalized_token = _normalize_token_case(token)
+        normalized_token = normalize_token_case(token)
         precisions = self._precision_cache.get(normalized_token)
         if not precisions:
             precisions = await self._fetch_and_cache_precisions(normalized_token)

+ 1 - 1
trading_bot.py

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