Browse Source

Refactor management commands and enhance order synchronization features

- Updated ManagementCommands to inherit from InfoCommandsBase, improving code structure.
- Refined the /sync command to streamline synchronization with exchange orders, including enhanced error handling and user feedback.
- Introduced a new method for formatting synchronization results, providing clearer messages to users.
- Enhanced OrdersCommands to adopt an exchange-first approach for retrieving orders, improving accuracy and efficiency.
- Implemented cleanup of stale pending stop loss orders based on current positions and limit orders, ensuring better resource management.
Carles Sentis 1 week ago
parent
commit
f7c6748ebc

+ 294 - 69
src/commands/info/orders.py

@@ -10,7 +10,7 @@ class OrdersCommands(InfoCommandsBase):
     """Handles all order-related commands."""
 
     async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /orders command."""
+        """Handle the /orders command with exchange-first approach."""
         try:
             if not self._is_authorized(update):
                 await self._reply(update, "❌ Unauthorized access.")
@@ -21,82 +21,307 @@ class OrdersCommands(InfoCommandsBase):
                 await self._reply(update, "❌ Trading stats not available.")
                 return
 
-            # Get open orders from database (much faster than exchange query)
-            db_orders = stats.get_orders_by_status('open', limit=50)
-            db_orders.extend(stats.get_orders_by_status('submitted', limit=50))
-            db_orders.extend(stats.get_orders_by_status('pending_trigger', limit=50))
-
-            # Get pending stop loss orders from the database
-            pending_sl_lifecycles = stats.get_pending_stop_losses_from_lifecycles()
+            # Get exchange orders (primary source of truth)
+            exchange_orders = self.trading_engine.get_orders() or []
+            
+            # Get pending stop loss orders (bot-internal only)
+            pending_sl_orders = stats.get_pending_stop_losses_from_lifecycles()
             
-            # Combine both lists
-            all_orders = db_orders + pending_sl_lifecycles
+            # Clean up stale pending SL orders
+            await self._cleanup_stale_pending_sl_orders(stats, exchange_orders)
+            
+            if not exchange_orders and not pending_sl_orders:
+                await self._reply(update, "📭 No open orders found")
+                return
+
+            # Format exchange-first orders message
+            orders_text = await self._format_exchange_first_orders_message(exchange_orders, pending_sl_orders)
+            await self._reply(update, orders_text.strip())
+
+        except Exception as e:
+            logger.error(f"Error in orders command: {e}", exc_info=True)
+            await self._reply(update, "❌ Error retrieving order information.")
 
-            if not all_orders:
-                await self._reply(update, "📭 No open or pending orders")
+    async def _cleanup_stale_pending_sl_orders(self, stats, exchange_orders: List[Dict]):
+        """Clean up pending SL orders when no more limit orders or positions exist for that token."""
+        try:
+            pending_sl_orders = stats.get_pending_stop_losses_from_lifecycles()
+            if not pending_sl_orders:
                 return
+            
+            # Get current positions
+            positions = self.trading_engine.get_positions() or []
+            position_tokens = {pos.get('symbol', '').split('/')[0] for pos in positions if pos.get('symbol')}
+            
+            # Get tokens with open limit orders on exchange
+            limit_order_tokens = set()
+            for order in exchange_orders:
+                if order.get('type') == 'limit' and not order.get('reduceOnly', False):
+                    symbol = order.get('symbol', '')
+                    token = symbol.split('/')[0] if symbol else ''
+                    if token:
+                        limit_order_tokens.add(token)
+            
+            # Find stale pending SL orders
+            stale_lifecycles = []
+            for pending_sl in pending_sl_orders:
+                symbol = pending_sl.get('symbol', '')
+                token = symbol.split('/')[0] if symbol else ''
+                
+                # If no position and no limit orders for this token, mark as stale
+                if token and token not in position_tokens and token not in limit_order_tokens:
+                    lifecycle_id = pending_sl.get('trade_lifecycle_id')
+                    if lifecycle_id:
+                        stale_lifecycles.append(lifecycle_id)
+                        logger.info(f"🧹 Marking pending SL for {token} as stale (no position/limit orders)")
+            
+            # Clean up stale pending SL orders
+            for lifecycle_id in stale_lifecycles:
+                stats.cancel_pending_stop_loss(lifecycle_id)
+                logger.info(f"✅ Cleaned up stale pending SL: {lifecycle_id}")
+                
+        except Exception as e:
+            logger.error(f"Error cleaning up stale pending SL orders: {e}")
 
-            # Format orders text
-            orders_text = "📋 <b>Open & Pending Orders</b>\n\n"
+    def _categorize_exchange_orders(self, exchange_orders: List[Dict]) -> Dict[str, List[Dict]]:
+        """Categorize exchange orders by type for better display."""
+        categories = {
+            'stop_loss': [],        # Stop loss orders (reduce-only with trigger)
+            'take_profit': [],      # Take profit orders  
+            'limit_orders': [],     # Regular limit orders
+            'market_orders': [],    # Market orders
+            'other_exit': [],       # Other reduce-only orders
+            'zero_size': []         # Zero-size position placeholders
+        }
+        
+        for order in exchange_orders:
+            amount = float(order.get('amount', 0))
+            order_type = order.get('type', '').lower()
+            is_reduce_only = order.get('reduceOnly', False) or order.get('info', {}).get('reduceOnly', False)
+            order_info = order.get('info', {})
+            has_trigger = 'triggerPrice' in order_info or order_info.get('isTrigger', False)
+            tpsl_type = order_info.get('tpsl')  # 'tp' or 'sl'
+            is_position_tpsl = order_info.get('isPositionTpsl', False)
+            
+            if amount == 0:
+                categories['zero_size'].append(order)
+            elif is_reduce_only and has_trigger and (tpsl_type == 'sl' or is_position_tpsl):
+                categories['stop_loss'].append(order)
+            elif is_reduce_only and has_trigger and tpsl_type == 'tp':
+                categories['take_profit'].append(order)
+            elif is_reduce_only:
+                categories['other_exit'].append(order)
+            elif order_type == 'limit':
+                categories['limit_orders'].append(order)
+            elif order_type == 'market':
+                categories['market_orders'].append(order)
+            else:
+                categories['other_exit'].append(order)  # Fallback
+        
+        return categories
 
-            for order in all_orders:
-                try:
-                    is_pending_sl = 'trade_lifecycle_id' in order and order.get('stop_loss_price') is not None
-                    
-                    symbol = order.get('symbol', 'unknown')
-                    base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    
-                    if is_pending_sl:
-                        # This is a pending SL from a trade lifecycle
-                        entry_side = order.get('side', 'unknown').upper()
-                        order_type = "STOP (PENDING)"
-                        side = "SELL" if entry_side == "BUY" else "BUY"
-                        price = float(order.get('stop_loss_price') or 0)
-                        amount = float(order.get('amount') or 0) # Amount from the entry order
-                        status = f"Awaiting {order.get('status', '').upper()} Entry" # e.g. Awaiting PENDING Entry
-                        order_id = order.get('trade_lifecycle_id', 'unknown')
-                    else:
-                        # This is a regular database order
-                        order_type = order.get('type', 'unknown').upper()
-                        side = order.get('side', 'unknown').upper()
-                        price = float(order.get('price') or 0)
-                        amount_requested = float(order.get('amount_requested') or 0)
-                        amount_filled = float(order.get('amount_filled') or 0)
-                        amount = amount_requested - amount_filled # Show remaining amount
-                        status = order.get('status', 'unknown').upper()
-                        order_id = order.get('exchange_order_id') or order.get('bot_order_ref_id', 'unknown')
+    async def _format_exchange_first_orders_message(self, exchange_orders: List[Dict], pending_sl_orders: List[Dict]) -> str:
+        """Format orders message with exchange orders as primary."""
+        
+        formatter = self._get_formatter()
+        message_parts = []
+        
+        # Header
+        total_exchange = len(exchange_orders)
+        total_pending = len(pending_sl_orders)
+        message_parts.append(f"📋 <b>All Orders ({total_exchange + total_pending} total)</b>\n")
+        
+        if exchange_orders:
+            # Categorize exchange orders
+            categories = self._categorize_exchange_orders(exchange_orders)
+            
+            # Regular Limit Orders
+            if categories['limit_orders']:
+                message_parts.append("📈 <b>Limit Orders:</b>")
+                for order in categories['limit_orders']:
+                    order_line = await self._format_exchange_order(order, formatter, show_type=False)
+                    message_parts.append(order_line)
+                message_parts.append("")
+            
+            # Market Orders
+            if categories['market_orders']:
+                message_parts.append("⚡ <b>Market Orders:</b>")
+                for order in categories['market_orders']:
+                    order_line = await self._format_exchange_order(order, formatter, show_type=False)
+                    message_parts.append(order_line)
+                message_parts.append("")
+            
+            # Stop Loss Orders
+            if categories['stop_loss']:
+                message_parts.append("🛑 <b>Stop Loss Orders:</b>")
+                for order in categories['stop_loss']:
+                    order_line = await self._format_stop_loss_order(order, formatter)
+                    message_parts.append(order_line)
+                message_parts.append("")
+            
+            # Take Profit Orders
+            if categories['take_profit']:
+                message_parts.append("🎯 <b>Take Profit Orders:</b>")
+                for order in categories['take_profit']:
+                    order_line = await self._format_take_profit_order(order, formatter)
+                    message_parts.append(order_line)
+                message_parts.append("")
+            
+            # Other Exit Orders
+            if categories['other_exit']:
+                message_parts.append("🔄 <b>Other Exit Orders:</b>")
+                for order in categories['other_exit']:
+                    order_line = await self._format_exchange_order(order, formatter, show_type=True)
+                    message_parts.append(order_line)
+                message_parts.append("")
+            
+            # Zero-size orders (position placeholders)
+            if categories['zero_size']:
+                message_parts.append("⚪ <b>Position Placeholders (Zero Size):</b>")
+                for order in categories['zero_size']:
+                    order_line = await self._format_zero_size_order(order, formatter)
+                    message_parts.append(order_line)
+                message_parts.append("")
+        
+        # Pending Stop Loss Orders (bot-internal only)
+        if pending_sl_orders:
+            message_parts.append("⏳ <b>Pending Stop Loss Orders (Bot Internal):</b>")
+            message_parts.append("   <i>Will be placed when entry orders fill</i>")
+            for order in pending_sl_orders:
+                order_line = await self._format_pending_sl_order(order, formatter)
+                if order_line:
+                    message_parts.append(order_line)
+            message_parts.append("")
+        
+        # Footer
+        from src.config.config import Config
+        message_parts.append("💡 <b>About Orders:</b>")
+        message_parts.append("• All orders above are live on the exchange")
+        message_parts.append("• Pending SL orders are bot-internal (not yet on exchange)")
+        message_parts.append("• Use /coo [token] to cancel all orders for a token")
+        message_parts.append(f"• Data refreshed every {Config.BOT_HEARTBEAT_SECONDS}s")
+        
+        return "\n".join(message_parts)
 
-                    # Skip fully filled orders
-                    if amount <= 0 and not is_pending_sl:
-                        continue
-                    
-                    # Format order details
-                    formatter = self._get_formatter()
-                    price_str = await formatter.format_price_with_symbol(price, base_asset)
-                    amount_str = await formatter.format_amount(amount, base_asset) if amount > 0 else "N/A"
+    async def _format_exchange_order(self, order: Dict, formatter, show_type: bool = True) -> str:
+        """Format a regular exchange order."""
+        try:
+            symbol = order.get('symbol', 'unknown')
+            base_asset = symbol.split('/')[0] if '/' in symbol else symbol
+            side = order.get('side', '').upper()
+            amount = float(order.get('amount', 0))
+            price = float(order.get('price', 0)) if order.get('price') else 0
+            order_type = order.get('type', '').upper()
+            
+            price_str = await formatter.format_price_with_symbol(price, base_asset) if price else "MARKET"
+            amount_str = await formatter.format_amount(amount, base_asset)
+            
+            side_emoji = "🟢" if side == "BUY" else "🔴"
+            
+            if show_type:
+                return f"   {side_emoji} {base_asset} {side} {order_type} - {amount_str} @ {price_str}"
+            else:
+                return f"   {side_emoji} {base_asset} {side} - {amount_str} @ {price_str}"
+            
+        except Exception as e:
+            logger.error(f"Error formatting exchange order: {e}")
+            return f"   ❌ Error formatting order: {order.get('symbol', 'unknown')}"
 
-                    # Order header
-                    side_emoji = "🟢" if side == "BUY" else "🔴"
-                    orders_text += f"{side_emoji} <b>{base_asset} {side} {order_type}</b>\n"
-                    orders_text += f"   Status: {status.replace('_', ' ')}\n"
-                    orders_text += f"   📏 Amount: {amount_str}\n"
-                    orders_text += f"   💰 {'Trigger Price' if 'STOP' in order_type else 'Price'}: {price_str}\n"
-                    
-                    # Add order ID
-                    id_label = "Lifecycle ID" if is_pending_sl else "Order ID"
-                    orders_text += f"   🆔 {id_label}: {str(order_id)[:12]}\n\n"
+    async def _format_stop_loss_order(self, order: Dict, formatter) -> str:
+        """Format a stop loss order with trigger information."""
+        try:
+            symbol = order.get('symbol', 'unknown')
+            base_asset = symbol.split('/')[0] if '/' in symbol else symbol
+            side = order.get('side', '').upper()
+            
+            # Get trigger price and amount info
+            order_info = order.get('info', {})
+            trigger_price = order_info.get('triggerPx', order_info.get('triggerPrice'))
+            trigger_condition = order_info.get('triggerCondition', '')
+            amount = float(order.get('amount', 0))
+            
+            trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
+            
+            # For zero-size position stop losses
+            if amount == 0:
+                amount_str = "Full Position"
+            else:
+                amount_str = await formatter.format_amount(amount, base_asset)
+            
+            side_emoji = "🟢" if side == "BUY" else "🔴"
+            
+            # Add trigger condition if available
+            condition_text = f" ({trigger_condition})" if trigger_condition and trigger_condition != "N/A" else ""
+            
+            return f"   {side_emoji} {base_asset} {side} {amount_str} - Trigger @ {trigger_price_str}{condition_text}"
+            
+        except Exception as e:
+            logger.error(f"Error formatting stop loss order: {e}")
+            return f"   ❌ Error formatting SL order: {order.get('symbol', 'unknown')}"
 
-                except Exception as e:
-                    logger.error(f"Error processing order {order.get('symbol', 'unknown')}: {e}", exc_info=True)
-                    continue
-            
-            # Add footer
-            orders_text += "💡 Pending SL orders are activated when the entry order fills.\n"
-            from src.config.config import Config
-            orders_text += f"🔄 Data updated every {Config.BOT_HEARTBEAT_SECONDS}s via monitoring system"
+    async def _format_take_profit_order(self, order: Dict, formatter) -> str:
+        """Format a take profit order."""
+        try:
+            symbol = order.get('symbol', 'unknown')
+            base_asset = symbol.split('/')[0] if '/' in symbol else symbol
+            side = order.get('side', '').upper()
+            amount = float(order.get('amount', 0))
+            
+            order_info = order.get('info', {})
+            trigger_price = order_info.get('triggerPx', order_info.get('triggerPrice'))
+            trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
+            amount_str = await formatter.format_amount(amount, base_asset)
+            
+            side_emoji = "🟢" if side == "BUY" else "🔴"
+            
+            return f"   {side_emoji} {base_asset} {side} {amount_str} - Target @ {trigger_price_str}"
+            
+        except Exception as e:
+            logger.error(f"Error formatting take profit order: {e}")
+            return f"   ❌ Error formatting TP order: {order.get('symbol', 'unknown')}"
 
-            await self._reply(update, orders_text.strip())
+    async def _format_zero_size_order(self, order: Dict, formatter) -> str:
+        """Format a zero-size order (position placeholder)."""
+        try:
+            symbol = order.get('symbol', 'unknown')
+            base_asset = symbol.split('/')[0] if '/' in symbol else symbol
+            side = order.get('side', '').upper()
+            order_info = order.get('info', {})
+            
+            # These are usually stop losses for positions
+            if order_info.get('isPositionTpsl', False):
+                trigger_price = order_info.get('triggerPx')
+                trigger_condition = order_info.get('triggerCondition', '')
+                trigger_price_str = await formatter.format_price_with_symbol(float(trigger_price), base_asset) if trigger_price else "N/A"
+                
+                side_emoji = "🛑" if side == "BUY" else "🛑"  # Red for stop loss
+                return f"   {side_emoji} {base_asset} Position Stop Loss - Trigger @ {trigger_price_str}"
+            else:
+                side_emoji = "⚪"
+                return f"   {side_emoji} {base_asset} {side} - Zero size placeholder"
+            
+        except Exception as e:
+            logger.error(f"Error formatting zero size order: {e}")
+            return f"   ❌ Error formatting zero-size order: {order.get('symbol', 'unknown')}"
 
+    async def _format_pending_sl_order(self, order: Dict, formatter) -> Optional[str]:
+        """Format a pending stop loss order (bot-internal)."""
+        try:
+            symbol = order.get('symbol', 'unknown')
+            base_asset = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+            
+            entry_side = order.get('side', 'unknown').upper()
+            exit_side = "SELL" if entry_side == "BUY" else "BUY"
+            price = float(order.get('stop_loss_price') or 0)
+            amount = float(order.get('amount') or 0)
+            status = order.get('status', '').upper()
+            
+            price_str = await formatter.format_price_with_symbol(price, base_asset)
+            amount_str = await formatter.format_amount(amount, base_asset)
+            
+            side_emoji = "⏳"
+            return f"   {side_emoji} {base_asset} {exit_side} {amount_str} @ {price_str} (Entry: {status})"
+            
         except Exception as e:
-            logger.error(f"Error in orders command: {e}", exc_info=True)
-            await self._reply(update, "❌ Error retrieving order information.")
+            logger.error(f"Error formatting pending SL order: {e}")
+            return None

+ 70 - 249
src/commands/management_commands.py

@@ -7,16 +7,18 @@ import logging
 import os
 import platform
 import sys
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup
 from telegram.ext import ContextTypes
 import json
+from typing import Dict, Any, List, Optional
 
 from src.config.config import Config
 from src.monitoring.alarm_manager import AlarmManager
 from src.utils.token_display_formatter import get_formatter
 from src.stats import TradingStats
 from src.config.logging_config import LoggingManager
+from .info.base import InfoCommandsBase
 
 logger = logging.getLogger(__name__)
 
@@ -31,11 +33,12 @@ def _normalize_token_case(token: str) -> str:
     else:
         return token.upper()  # Convert to uppercase for all-lowercase input
 
-class ManagementCommands:
+class ManagementCommands(InfoCommandsBase):
     """Handles all management-related Telegram commands."""
     
     def __init__(self, trading_engine, monitoring_coordinator):
         """Initialize with trading engine and monitoring coordinator."""
+        super().__init__(trading_engine)
         self.trading_engine = trading_engine
         self.monitoring_coordinator = monitoring_coordinator
         self.alarm_manager = AlarmManager()
@@ -544,263 +547,81 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
         )
 
     async def sync_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /sync command to synchronize bot state with exchange."""
-        chat_id = update.effective_chat.id
-        if not self._is_authorized(chat_id):
-            await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
-            return
-
+        """Handle the /sync command to synchronize exchange orders with database."""
         try:
-            # Get confirmation from user unless force flag is provided
-            force_sync = len(context.args) > 0 and context.args[0].lower() == "force"
-            
-            if not force_sync:
-                # Send confirmation message
-                await context.bot.send_message(
-                    chat_id=chat_id,
-                    text=(
-                        "⚠️ <b>Data Synchronization Warning</b>\n\n"
-                        "This will:\n"
-                        "• Clear ALL local trades, orders, and pending stop losses\n"
-                        "• Reset bot tracking state\n"
-                        "• Sync with current exchange positions/orders\n"
-                        "• Preserve balance and performance history\n\n"
-                        "🔄 Use <code>/sync force</code> to proceed\n"
-                        "❌ Use any other command to cancel"
-                    ),
-                    parse_mode='HTML'
-                )
+            if not self._is_authorized(update):
+                await self._reply(update, "❌ Unauthorized access.")
                 return
 
-            # Send processing message
-            processing_msg = await context.bot.send_message(
-                chat_id=chat_id,
-                text="🔄 <b>Synchronizing with Exchange...</b>\n\n⏳ Step 1/5: Clearing local data...",
-                parse_mode='HTML'
-            )
+            # Get force parameter
+            force = False
+            if context.args and context.args[0].lower() == 'force':
+                force = True
 
-            # Step 1: Clear local trading data
-            stats = self.trading_engine.get_stats()
-            if stats:
-                # Clear trades table (keep only position_closed for history)
-                stats.db_manager._execute_query(
-                    "DELETE FROM trades WHERE status IN ('pending', 'executed', 'position_opened', 'cancelled')"
-                )
-                
-                # Clear orders table (keep only filled/cancelled for history)
-                stats.db_manager._execute_query(
-                    "DELETE FROM orders WHERE status IN ('pending_submission', 'open', 'submitted', 'pending_trigger')"
-                )
-                
-                logger.info("🧹 Cleared local trading state from database")
-
-            # Update status
-            await context.bot.edit_message_text(
-                chat_id=chat_id,
-                message_id=processing_msg.message_id,
-                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n⏳ Step 2/5: Clearing pending stop losses...",
-                parse_mode='HTML'
-            )
+            await self._reply(update, "🔄 Starting order synchronization...")
 
-            # Step 2: Clear pending stop loss orders
-            try:
-                if hasattr(self.monitoring_coordinator, 'pending_orders_manager'):
-                    pending_manager = self.monitoring_coordinator.pending_orders_manager
-                    if pending_manager and hasattr(pending_manager, 'db_path'):
-                        import sqlite3
-                        with sqlite3.connect(pending_manager.db_path) as conn:
-                            conn.execute("DELETE FROM pending_stop_loss WHERE status IN ('pending', 'placed')")
-                            conn.commit()
-                        logger.info("🧹 Cleared pending stop loss orders")
-            except Exception as e:
-                logger.warning(f"Could not clear pending orders: {e}")
-
-            # Update status
-            await context.bot.edit_message_text(
-                chat_id=chat_id,
-                message_id=processing_msg.message_id,
-                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n⏳ Step 3/5: Fetching exchange state...",
-                parse_mode='HTML'
-            )
-
-            # Step 3: Fetch current exchange state
-            exchange_positions = self.trading_engine.get_positions() or []
-            exchange_orders = self.trading_engine.get_orders() or []
-            
-            # Update status
-            await context.bot.edit_message_text(
-                chat_id=chat_id,
-                message_id=processing_msg.message_id,
-                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n⏳ Step 4/5: Recreating position tracking...",
-                parse_mode='HTML'
-            )
+            # Get exchange order sync from monitoring coordinator
+            monitoring_coordinator = getattr(self.trading_engine, 'monitoring_coordinator', None)
+            if not monitoring_coordinator or not monitoring_coordinator.exchange_order_sync:
+                await self._reply(update, "❌ Order synchronization not available. Please restart the bot.")
+                return
 
-            # Step 4: Recreate position tracking for open positions
-            positions_synced = 0
-            orders_synced = 0
+            # Run synchronization
+            sync_results = monitoring_coordinator.exchange_order_sync.sync_exchange_orders_to_database()
             
-            if stats:
-                # Create trade lifecycles for open positions
-                for position in exchange_positions:
-                    try:
-                        size = float(position.get('contracts', 0))
-                        if abs(size) > 1e-8:  # Position exists
-                            symbol = position.get('symbol', '')
-                            if symbol:
-                                # Extract token from symbol (e.g., "BTC/USDC:USDC" -> "BTC")
-                                token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                                
-                                # Create a new trade lifecycle for this existing position
-                                # Use the side field from exchange (more reliable than contracts sign)
-                                exchange_side = position.get('side', '').lower()
-                                if exchange_side == 'long':
-                                    side = 'buy'
-                                    position_side = 'long'
-                                elif exchange_side == 'short':
-                                    side = 'sell' 
-                                    position_side = 'short'
-                                else:
-                                    # Fallback to contracts sign if side field missing
-                                    side = 'buy' if size > 0 else 'sell'
-                                    position_side = 'long' if size > 0 else 'short'
-                                    logger.warning(f"Using contracts sign fallback for {token}: size={size}, exchange_side='{exchange_side}'")
-                                
-                                entry_price = float(position.get('entryPrice', 0))
-                                
-                                if entry_price > 0:  # Valid position data
-                                    # Create the lifecycle entry (this generates and returns lifecycle_id)
-                                    lifecycle_id = stats.create_trade_lifecycle(
-                                        symbol=symbol,
-                                        side=side,
-                                        stop_loss_price=None,  # Will be detected from orders
-                                        trade_type='sync'
-                                    )
-                                    
-                                    if lifecycle_id:
-                                        # Update to position opened status
-                                        await stats.update_trade_position_opened(
-                                            lifecycle_id=lifecycle_id,
-                                            entry_price=entry_price,
-                                            entry_amount=abs(size),
-                                            exchange_fill_id=f"sync_{lifecycle_id[:8]}"
-                                        )
-                                        
-                                        # Update with current market data
-                                        unrealized_pnl = float(position.get('unrealizedPnl', 0))
-                                        # Handle None markPrice safely
-                                        mark_price_raw = position.get('markPrice')
-                                        mark_price = float(mark_price_raw) if mark_price_raw is not None else entry_price
-                                        
-                                        stats.update_trade_market_data(
-                                            trade_lifecycle_id=lifecycle_id,
-                                            current_position_size=abs(size),
-                                            unrealized_pnl=unrealized_pnl,
-                                            mark_price=mark_price,
-                                            position_value=abs(size) * mark_price
-                                        )
-                                        
-                                        positions_synced += 1
-                                        logger.info(f"🔄 Recreated position tracking for {token}: {position_side} {abs(size)} @ {entry_price}")
-                    
-                    except Exception as e:
-                        logger.error(f"Error recreating position for {position}: {e}")
-
-                # Link existing orders to positions
-                for order in exchange_orders:
-                    try:
-                        order_symbol = order.get('symbol', '')
-                        order_id = order.get('id', '')
-                        order_type = order.get('type', '').lower()
-                        is_reduce_only = order.get('reduceOnly', False)
-                        
-                        if order_symbol and order_id and is_reduce_only:
-                            # This might be a stop loss or take profit order
-                            # Find the corresponding position
-                            matching_trade = stats.get_trade_by_symbol_and_status(order_symbol, 'position_opened')
-                            if matching_trade:
-                                lifecycle_id = matching_trade.get('trade_lifecycle_id')
-                                if lifecycle_id:
-                                    order_price = float(order.get('price', 0))
-                                    stop_price = float(order.get('stopPrice', 0)) or order_price
-                                    
-                                    if 'stop' in order_type and stop_price > 0:
-                                        # Link as stop loss
-                                        await stats.link_stop_loss_to_trade(lifecycle_id, order_id, stop_price)
-                                        orders_synced += 1
-                                        logger.info(f"🔗 Linked stop loss order {order_id} to position {lifecycle_id[:8]}")
-                                    elif order_type in ['limit', 'take_profit'] and order_price > 0:
-                                        # Link as take profit
-                                        await stats.link_take_profit_to_trade(lifecycle_id, order_id, order_price)
-                                        orders_synced += 1
-                                        logger.info(f"🔗 Linked take profit order {order_id} to position {lifecycle_id[:8]}")
-                    
-                    except Exception as e:
-                        logger.error(f"Error linking order {order}: {e}")
-
-            # Update status
-            await context.bot.edit_message_text(
-                chat_id=chat_id,
-                message_id=processing_msg.message_id,
-                text="🔄 <b>Synchronizing with Exchange...</b>\n\n✅ Step 1/5: Local data cleared\n✅ Step 2/5: Pending stop losses cleared\n✅ Step 3/5: Exchange state fetched\n✅ Step 4/5: Position tracking recreated\n⏳ Step 5/5: Updating balance...",
-                parse_mode='HTML'
-            )
-
-            # Step 5: Update current balance
-            current_balance = 0.0
-            try:
-                balance_data = self.trading_engine.get_balance()
-                if balance_data and balance_data.get('total'):
-                    current_balance = float(balance_data['total'].get('USDC', 0))
-                    if stats:
-                        await stats.record_balance_snapshot(current_balance, unrealized_pnl=0.0, notes="Post-sync balance")
-            except Exception as e:
-                logger.warning(f"Could not update balance after sync: {e}")
-
-            # Final success message
-            formatter = get_formatter()
-            success_message = f"""
-✅ <b>Synchronization Complete!</b>
-
-📊 <b>Sync Results:</b>
-• Positions Recreated: {positions_synced}
-• Orders Linked: {orders_synced}
-• Current Balance: {await formatter.format_price_with_symbol(current_balance)}
-
-🔄 <b>What was reset:</b>
-• Local trade tracking
-• Pending orders database
-• Order monitoring state
-
-🎯 <b>What was preserved:</b>
-• Historical performance data
-• Balance adjustment history
-• Completed trade statistics
-
-💡 The bot is now synchronized with your exchange state and ready for trading!
-            """
-
-            await context.bot.edit_message_text(
-                chat_id=chat_id,
-                message_id=processing_msg.message_id,
-                text=success_message.strip(),
-                parse_mode='HTML'
-            )
-
-            logger.info(f"🎉 Sync completed: {positions_synced} positions, {orders_synced} orders linked")
+            # Format results message
+            message = await self._format_sync_results(sync_results, force)
+            await self._reply(update, message)
 
         except Exception as e:
-            error_message = f"❌ <b>Sync Failed</b>\n\nError: {str(e)}\n\n💡 Check logs for details."
-            try:
-                await context.bot.edit_message_text(
-                    chat_id=chat_id,
-                    message_id=processing_msg.message_id,
-                    text=error_message,
-                    parse_mode='HTML'
-                )
-            except:
-                await context.bot.send_message(chat_id=chat_id, text=error_message, parse_mode='HTML')
-            
             logger.error(f"Error in sync command: {e}", exc_info=True)
+            await self._reply(update, "❌ Error during synchronization.")
+
+    async def _format_sync_results(self, sync_results: Dict[str, Any], force: bool) -> str:
+        """Format synchronization results for display."""
+        
+        if 'error' in sync_results:
+            return f"❌ Synchronization failed: {sync_results['error']}"
+        
+        new_orders = sync_results.get('new_orders_added', 0)
+        updated_orders = sync_results.get('orders_updated', 0)
+        cancelled_orders = sync_results.get('orphaned_orders_cancelled', 0)
+        errors = sync_results.get('errors', [])
+        
+        message_parts = ["✅ <b>Order Synchronization Complete</b>\n"]
+        
+        # Results summary
+        message_parts.append("📊 <b>Sync Results:</b>")
+        message_parts.append(f"   • {new_orders} new orders added to database")
+        message_parts.append(f"   • {updated_orders} existing orders updated")
+        message_parts.append(f"   • {cancelled_orders} orphaned orders cancelled")
+        
+        if errors:
+            message_parts.append(f"   • {len(errors)} errors encountered")
+            message_parts.append("")
+            message_parts.append("⚠️ <b>Errors:</b>")
+            for error in errors[:3]:  # Show first 3 errors
+                message_parts.append(f"   • {error}")
+            if len(errors) > 3:
+                message_parts.append(f"   • ... and {len(errors) - 3} more errors")
+        
+        message_parts.append("")
+        
+        # Status messages
+        if new_orders == 0 and updated_orders == 0 and cancelled_orders == 0:
+            message_parts.append("✅ All orders are already synchronized")
+        else:
+            message_parts.append("🔄 Database now matches exchange state")
+        
+        # Help text
+        message_parts.append("")
+        message_parts.append("💡 <b>About Sync:</b>")
+        message_parts.append("• Orders placed directly on exchange are now tracked")
+        message_parts.append("• Bot-placed orders remain tracked as before")
+        message_parts.append("• Sync runs automatically every 30 seconds")
+        message_parts.append("• Use /orders to see all synchronized orders")
+        
+        return "\n".join(message_parts)
 
     async def deposit_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /deposit command to record a deposit."""

+ 226 - 0
src/monitoring/exchange_order_sync.py

@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+"""
+Exchange Order Synchronization Manager
+Syncs orders from the exchange to the database for tracking
+"""
+
+import logging
+from typing import Dict, List, Optional, Any
+from datetime import datetime, timezone
+import uuid
+
+logger = logging.getLogger(__name__)
+
+class ExchangeOrderSync:
+    """Manages synchronization of exchange orders with database"""
+    
+    def __init__(self, hl_client, trading_stats):
+        self.hl_client = hl_client
+        self.trading_stats = trading_stats
+        
+    def sync_exchange_orders_to_database(self) -> Dict[str, Any]:
+        """Sync all exchange orders to database for tracking"""
+        try:
+            # Get current exchange orders
+            exchange_orders = self.hl_client.get_open_orders() or []
+            
+            # Get current database orders
+            db_orders = []
+            db_orders.extend(self.trading_stats.get_orders_by_status('open', limit=100))
+            db_orders.extend(self.trading_stats.get_orders_by_status('submitted', limit=100))
+            db_orders.extend(self.trading_stats.get_orders_by_status('pending_trigger', limit=100))
+            
+            # Track what we sync
+            sync_results = {
+                'new_orders_added': 0,
+                'orders_updated': 0,
+                'orphaned_orders_cancelled': 0,
+                'errors': []
+            }
+            
+            # Sync exchange orders to database
+            self._sync_exchange_to_db(exchange_orders, db_orders, sync_results)
+            
+            # Clean up orphaned database orders
+            self._cleanup_orphaned_db_orders(exchange_orders, db_orders, sync_results)
+            
+            logger.info(f"Order sync complete: {sync_results['new_orders_added']} added, "
+                       f"{sync_results['orders_updated']} updated, "
+                       f"{sync_results['orphaned_orders_cancelled']} orphaned")
+            
+            return sync_results
+            
+        except Exception as e:
+            logger.error(f"Error syncing exchange orders: {e}")
+            return {'error': str(e)}
+    
+    def _sync_exchange_to_db(self, exchange_orders: List[Dict], db_orders: List[Dict], sync_results: Dict):
+        """Sync exchange orders to database"""
+        
+        # Create lookup of existing database orders by exchange ID
+        db_orders_by_exchange_id = {
+            order.get('exchange_order_id'): order 
+            for order in db_orders 
+            if order.get('exchange_order_id')
+        }
+        
+        for exchange_order in exchange_orders:
+            try:
+                exchange_order_id = exchange_order.get('id')
+                if not exchange_order_id:
+                    continue
+                
+                # Check if order already exists in database
+                if exchange_order_id in db_orders_by_exchange_id:
+                    # Update existing order
+                    self._update_existing_order(exchange_order, db_orders_by_exchange_id[exchange_order_id], sync_results)
+                else:
+                    # Add new order to database
+                    self._add_new_order_to_database(exchange_order, sync_results)
+                    
+            except Exception as e:
+                sync_results['errors'].append(f"Error syncing order {exchange_order.get('id', 'unknown')}: {e}")
+                logger.error(f"Error syncing exchange order: {e}")
+    
+    def _add_new_order_to_database(self, exchange_order: Dict, sync_results: Dict):
+        """Add a new exchange order to the database"""
+        try:
+            symbol = exchange_order.get('symbol', '')
+            side = exchange_order.get('side', '')
+            order_type = exchange_order.get('type', '')
+            amount = float(exchange_order.get('amount', 0))
+            price = float(exchange_order.get('price', 0)) if exchange_order.get('price') else None
+            exchange_order_id = exchange_order.get('id', '')
+            
+            # Generate bot order ref ID for tracking
+            bot_order_ref_id = f"sync_{uuid.uuid4().hex[:8]}"
+            
+            # Determine order status
+            status = self._map_exchange_status_to_db_status(exchange_order)
+            
+            # Check if it's a reduce-only order
+            is_reduce_only = exchange_order.get('reduceOnly', False) or exchange_order.get('info', {}).get('reduceOnly', False)
+            
+            # Skip zero-size orders (position placeholders)
+            if amount == 0:
+                logger.debug(f"Skipping zero-size order {exchange_order_id} (position placeholder)")
+                return
+            
+            # Insert into database
+            insert_query = """
+                INSERT INTO orders (
+                    bot_order_ref_id, exchange_order_id, symbol, side, type, 
+                    amount_requested, amount_filled, price, status, 
+                    timestamp_created, timestamp_updated
+                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+            """
+            
+            timestamp = datetime.now(timezone.utc).isoformat()
+            amount_filled = float(exchange_order.get('filled', 0))
+            
+            params = (
+                bot_order_ref_id, exchange_order_id, symbol, side, order_type,
+                amount, amount_filled, price, status,
+                timestamp, timestamp
+            )
+            
+            self.trading_stats.db_manager._execute_query(insert_query, params)
+            sync_results['new_orders_added'] += 1
+            
+            logger.info(f"✅ Added exchange order to database: {symbol} {side} {order_type} (ID: {exchange_order_id})")
+            
+        except Exception as e:
+            logger.error(f"Error adding exchange order to database: {e}")
+            sync_results['errors'].append(f"Error adding order {exchange_order.get('id', 'unknown')}: {e}")
+    
+    def _update_existing_order(self, exchange_order: Dict, db_order: Dict, sync_results: Dict):
+        """Update existing database order with exchange data"""
+        try:
+            exchange_order_id = exchange_order.get('id', '')
+            new_status = self._map_exchange_status_to_db_status(exchange_order)
+            new_amount_filled = float(exchange_order.get('filled', 0))
+            
+            # Check if status or filled amount changed
+            old_status = db_order.get('status', '')
+            old_amount_filled = float(db_order.get('amount_filled', 0))
+            
+            if new_status != old_status or abs(new_amount_filled - old_amount_filled) > 0.0001:
+                # Update the order
+                update_query = """
+                    UPDATE orders 
+                    SET status = ?, amount_filled = ?, timestamp_updated = ?
+                    WHERE exchange_order_id = ?
+                """
+                
+                timestamp = datetime.now(timezone.utc).isoformat()
+                params = (new_status, new_amount_filled, timestamp, exchange_order_id)
+                
+                self.trading_stats.db_manager._execute_query(update_query, params)
+                sync_results['orders_updated'] += 1
+                
+                logger.debug(f"Updated order {exchange_order_id}: status {old_status} -> {new_status}, "
+                           f"filled {old_amount_filled} -> {new_amount_filled}")
+            
+        except Exception as e:
+            logger.error(f"Error updating exchange order in database: {e}")
+            sync_results['errors'].append(f"Error updating order {exchange_order.get('id', 'unknown')}: {e}")
+    
+    def _cleanup_orphaned_db_orders(self, exchange_orders: List[Dict], db_orders: List[Dict], sync_results: Dict):
+        """Clean up database orders that no longer exist on exchange"""
+        
+        # Get exchange order IDs
+        exchange_order_ids = {order.get('id') for order in exchange_orders if order.get('id')}
+        
+        for db_order in db_orders:
+            try:
+                exchange_order_id = db_order.get('exchange_order_id')
+                if not exchange_order_id:
+                    continue  # Skip orders without exchange ID
+                
+                # If database order not found on exchange, mark as cancelled
+                if exchange_order_id not in exchange_order_ids:
+                    self._mark_order_as_cancelled(db_order, sync_results)
+                    
+            except Exception as e:
+                logger.error(f"Error checking orphaned order: {e}")
+                sync_results['errors'].append(f"Error checking orphaned order: {e}")
+    
+    def _mark_order_as_cancelled(self, db_order: Dict, sync_results: Dict):
+        """Mark a database order as cancelled (no longer on exchange)"""
+        try:
+            order_id = db_order.get('id')
+            exchange_order_id = db_order.get('exchange_order_id', '')
+            symbol = db_order.get('symbol', '')
+            
+            update_query = """
+                UPDATE orders 
+                SET status = 'cancelled', timestamp_updated = ?
+                WHERE id = ?
+            """
+            
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (timestamp, order_id)
+            
+            self.trading_stats.db_manager._execute_query(update_query, params)
+            sync_results['orphaned_orders_cancelled'] += 1
+            
+            logger.info(f"🗑️ Marked orphaned order as cancelled: {symbol} (Exchange ID: {exchange_order_id})")
+            
+        except Exception as e:
+            logger.error(f"Error marking order as cancelled: {e}")
+    
+    def _map_exchange_status_to_db_status(self, exchange_order: Dict) -> str:
+        """Map exchange order status to database status"""
+        exchange_status = exchange_order.get('status', '').lower()
+        
+        status_mapping = {
+            'open': 'open',
+            'pending': 'submitted',
+            'closed': 'filled',
+            'canceled': 'cancelled',
+            'cancelled': 'cancelled',
+            'expired': 'expired',
+            'rejected': 'failed'
+        }
+        
+        return status_mapping.get(exchange_status, 'unknown') 

+ 55 - 0
src/monitoring/monitoring_coordinator.py

@@ -10,6 +10,7 @@ from .position_tracker import PositionTracker
 from .pending_orders_manager import PendingOrdersManager
 from .risk_manager import RiskManager
 from .alarm_manager import AlarmManager
+from .exchange_order_sync import ExchangeOrderSync
 # DrawdownMonitor and RsiMonitor will be lazy-loaded to avoid circular imports
 
 logger = logging.getLogger(__name__)
@@ -32,6 +33,9 @@ class MonitoringCoordinator:
         self.risk_manager = RiskManager(hl_client, notification_manager, config)
         self.alarm_manager = AlarmManager()  # AlarmManager only needs alarms_file (defaults to data/price_alarms.json)
         
+        # Exchange order synchronization (will be initialized with trading stats)
+        self.exchange_order_sync = None
+        
         # DrawdownMonitor and RSIMonitor will be lazy-loaded to avoid circular imports
         self.drawdown_monitor = None
         self.rsi_monitor = None
@@ -54,6 +58,9 @@ class MonitoringCoordinator:
             await self.risk_manager.start()
             # AlarmManager doesn't have start() method - it's always ready
             
+            # Initialize exchange order sync with trading stats
+            self._init_exchange_order_sync()
+            
             # Lazy-load optional monitors to avoid circular imports
             self._init_optional_monitors()
             
@@ -63,6 +70,9 @@ class MonitoringCoordinator:
             if hasattr(self.rsi_monitor, 'start'):
                 await self.rsi_monitor.start()
             
+            # Start order synchronization loop
+            asyncio.create_task(self._order_sync_loop())
+            
             logger.info("All monitoring components started successfully")
             
         except Exception as e:
@@ -92,6 +102,51 @@ class MonitoringCoordinator:
         
         logger.info("Monitoring system stopped")
         
+    def _init_exchange_order_sync(self):
+        """Initialize exchange order synchronization"""
+        try:
+            # Get trading stats from position tracker
+            if hasattr(self.position_tracker, 'trading_stats') and self.position_tracker.trading_stats:
+                self.exchange_order_sync = ExchangeOrderSync(
+                    self.hl_client, 
+                    self.position_tracker.trading_stats
+                )
+                logger.info("✅ Exchange order sync initialized")
+            else:
+                logger.warning("⚠️ Trading stats not available, exchange order sync disabled")
+        except Exception as e:
+            logger.error(f"Error initializing exchange order sync: {e}")
+    
+    async def _order_sync_loop(self):
+        """Periodic order synchronization loop"""
+        logger.info("🔄 Starting exchange order synchronization loop")
+        sync_interval = 30  # Sync every 30 seconds
+        loop_count = 0
+        
+        while self.is_running:
+            try:
+                loop_count += 1
+                
+                # Run sync every 30 seconds (6 cycles with 5s heartbeat)
+                if loop_count % 6 == 0 and self.exchange_order_sync:
+                    logger.debug(f"🔄 Running exchange order sync (loop #{loop_count})")
+                    sync_results = self.exchange_order_sync.sync_exchange_orders_to_database()
+                    
+                    # Log results if there were changes
+                    if sync_results.get('new_orders_added', 0) > 0 or sync_results.get('orders_updated', 0) > 0:
+                        logger.info(f"📊 Order sync: +{sync_results.get('new_orders_added', 0)} new, "
+                                  f"~{sync_results.get('orders_updated', 0)} updated, "
+                                  f"-{sync_results.get('orphaned_orders_cancelled', 0)} cancelled")
+                
+                # Wait for next cycle
+                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
+                
+            except Exception as e:
+                logger.error(f"Error in order sync loop: {e}")
+                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
+        
+        logger.info("🛑 Exchange order sync loop stopped")
+    
     def _init_optional_monitors(self):
         """Initialize optional monitors with lazy loading to avoid circular imports"""
         try:

+ 61 - 7
src/monitoring/position_tracker.py

@@ -25,6 +25,12 @@ class PositionTracker:
         self.current_positions: Dict[str, Dict] = {}
         self.is_running = False
         
+        # Market data cache to prevent overfetching
+        # Previously: Every 5s cycle fetched market data 2-4 times per symbol
+        # Now: Market data is cached for 30s, reducing API calls by ~75%
+        self._market_data_cache: Dict[str, Dict] = {}  # symbol -> {data, timestamp}
+        self._cache_ttl_seconds = 30  # Cache market data for 30 seconds
+        
     async def start(self):
         """Start position tracking"""
         if self.is_running:
@@ -65,9 +71,10 @@ class PositionTracker:
                 logger.debug(f"📊 Position tracker loop #{loop_count} - checking for position changes...")
                 await self._check_position_changes()
                 
-                # Log periodically to show it's alive
-                if loop_count % 12 == 0:  # Every 12 loops (60 seconds with 5s heartbeat)
-                    logger.info(f"📊 Position tracker alive - loop #{loop_count}, {len(self.current_positions)} positions tracked")
+                # Clean stale cache entries every 12 loops (60 seconds with 5s heartbeat)
+                if loop_count % 12 == 0:
+                    self._clear_stale_cache_entries()
+                    logger.info(f"📊 Position tracker alive - loop #{loop_count}, {len(self.current_positions)} positions tracked, {len(self._market_data_cache)} cached symbols")
                 
                 await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)  # Use config heartbeat
             except Exception as e:
@@ -215,7 +222,7 @@ class PositionTracker:
                         # Get current market price for mark price and position value calculation
                         current_mark_price = 0.0
                         try:
-                            market_data = self.hl_client.get_market_data(symbol)
+                            market_data = self._get_cached_market_data(symbol)
                             if market_data and market_data.get('ticker'):
                                 current_mark_price = float(market_data['ticker'].get('last', 0))
                         except Exception as e:
@@ -402,7 +409,7 @@ class PositionTracker:
             side = "Long" if position['size'] > 0 else "Short"
             
             # Get current market price for exit calculation
-            market_data = self.hl_client.get_market_data(full_symbol)
+            market_data = self._get_cached_market_data(full_symbol)
             if not market_data:
                 logger.error(f"Could not get market data for {full_symbol}")
                 return
@@ -486,7 +493,7 @@ class PositionTracker:
             # Get current market price for more accurate value calculation
             try:
                 full_symbol = f"{symbol}/USDC:USDC"
-                market_data = self.hl_client.get_market_data(full_symbol)
+                market_data = self._get_cached_market_data(full_symbol)
                 current_market_price = float(market_data.get('ticker', {}).get('last', current['entry_px'])) if market_data else current['entry_px']
             except Exception:
                 current_market_price = current['entry_px']  # Fallback to entry price
@@ -591,4 +598,51 @@ class PositionTracker:
             logger.info(f"Saved stats for {symbol}: PnL ${pnl:.3f}, lifecycle_id: {lifecycle_id}")
             
         except Exception as e:
-            logger.error(f"Error saving position stats for {symbol}: {e}") 
+            logger.error(f"Error saving position stats for {symbol}: {e}")
+
+    def _get_cached_market_data(self, symbol: str) -> Optional[Dict[str, Any]]:
+        """Get market data from cache if fresh, otherwise fetch and cache it."""
+        now = datetime.now(timezone.utc)
+        
+        # Check if we have cached data for this symbol
+        if symbol in self._market_data_cache:
+            cached_entry = self._market_data_cache[symbol]
+            cache_age = (now - cached_entry['timestamp']).total_seconds()
+            
+            # Return cached data if it's still fresh
+            if cache_age < self._cache_ttl_seconds:
+                logger.debug(f"📋 Using cached market data for {symbol} (age: {cache_age:.1f}s)")
+                return cached_entry['data']
+            else:
+                logger.debug(f"🗑️ Cached data for {symbol} is stale (age: {cache_age:.1f}s), will fetch fresh data")
+        
+        # Fetch fresh market data
+        logger.debug(f"🔄 Fetching fresh market data for {symbol}")
+        market_data = self.hl_client.get_market_data(symbol)
+        
+        if market_data:
+            # Cache the fresh data
+            self._market_data_cache[symbol] = {
+                'data': market_data,
+                'timestamp': now
+            }
+            logger.debug(f"💾 Cached market data for {symbol}")
+        
+        return market_data
+        
+    def _clear_stale_cache_entries(self):
+        """Remove stale entries from market data cache."""
+        now = datetime.now(timezone.utc)
+        stale_symbols = []
+        
+        for symbol, cached_entry in self._market_data_cache.items():
+            cache_age = (now - cached_entry['timestamp']).total_seconds()
+            if cache_age >= self._cache_ttl_seconds:
+                stale_symbols.append(symbol)
+        
+        for symbol in stale_symbols:
+            del self._market_data_cache[symbol]
+            logger.debug(f"🗑️ Removed stale cache entry for {symbol}")
+        
+        if stale_symbols:
+            logger.debug(f"🧹 Cleaned {len(stale_symbols)} stale cache entries") 

+ 1 - 1
trading_bot.py

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