浏览代码

Increment BOT_VERSION to 2.2.144 and refactor monitoring components for enhanced trade management.

- Updated BOT_VERSION for the upcoming release.
- Introduced new modules for external event monitoring, order fill processing, position synchronization, and risk cleanup management, improving modularity and clarity.
- Refactored MarketMonitor to act as a coordinator, delegating tasks to specialized processors for better organization and maintainability.
- Enhanced documentation to reflect the new project structure and functionalities, ensuring clarity for future development.
Carles Sentis 1 天之前
父节点
当前提交
16b440b8bb

+ 89 - 26
docs/project-structure.md

@@ -25,8 +25,12 @@ trader_hyperliquid/
 │   │   └── 📝 logging_config.py # Logging setup (240 lines)
 │   │   └── 📝 logging_config.py # Logging setup (240 lines)
 │   ├── 📁 monitoring/          # Market monitoring
 │   ├── 📁 monitoring/          # Market monitoring
 │   │   ├── 📄 __init__.py      # Module init
 │   │   ├── 📄 __init__.py      # Module init
-│   │   ├── 🔔 alarm_manager.py # Price alarms
-│   │   └── 📊 market_monitor.py # External trades & monitoring (1,124 lines)
+│   │   ├── 🔔 alarm_manager.py # Price alarms (246 lines)
+│   │   ├── 👁️ external_event_monitor.py # Monitors external trades and price alarms (NEW - 414 lines)
+│   │   ├── 🔄 order_fill_processor.py # Processes order fills and disappeared orders (NEW - 324 lines)
+│   │   ├── ⚖️ position_synchronizer.py # Synchronizes bot/exchange position states (NEW - 487 lines)
+│   │   ├── 🛡️ risk_cleanup_manager.py # Handles risk management, cleanup of orphaned/pending orders (NEW - 501 lines)
+│   │   └── 📊 market_monitor.py # Main coordinator for monitoring activities (NEW - 392 lines)
 │   ├── 📁 notifications/       # Telegram notifications
 │   ├── 📁 notifications/       # Telegram notifications
 │   │   ├── 📄 __init__.py      # Module init
 │   │   ├── 📄 __init__.py      # Module init
 │   │   └── 📱 notification_manager.py # Rich notifications (343 lines)
 │   │   └── 📱 notification_manager.py # Rich notifications (343 lines)
@@ -222,34 +226,93 @@ The bot has been refactored from a monolithic 4,627-line file into a professiona
 - `get_recent_fills()` - External trade detection
 - `get_recent_fills()` - External trade detection
 
 
 ### **📊 src/monitoring/market_monitor.py**
 ### **📊 src/monitoring/market_monitor.py**
-**🔥 Enhanced external trade monitoring and market events (1,124 lines)**
-- Real-time order fill detection
-- **Smart stop loss edge case handling**
-- External trade monitoring with order linking
-- Price alarm checking
-- Position change tracking
-- **Bulletproof order cancellation detection**
-- Automatic notifications
-
-**🆕 NEW: Enhanced Stop Loss Management**
-- **3-second grace period** for external cancellations
-- **Double-check fill detection** before cancelling stop losses
-- **Recent fills analysis** to prevent premature cancellation
-- **Smart order state reconciliation** for simultaneous cancel/fill scenarios
+**🔥 Main coordinator for all market monitoring activities (Refactored - 392 lines)**
+- Delegates tasks to specialized processors: `OrderFillProcessor`, `PositionSynchronizer`, `ExternalEventMonitor`, `RiskCleanupManager`.
+- Manages a shared `MarketMonitorCache` for efficient data access.
+- Initializes and orchestrates the main `_monitor_loop`.
+- Handles loading/saving of minimal specific state (e.g., `last_processed_trade_time_helper`).
+- Provides an interface for `AlarmManager`.
 
 
 **Key Classes:**
 **Key Classes:**
-- `MarketMonitor` - Market event monitor
+- `MarketMonitorCache` - Holds shared cached data.
+- `MarketMonitor` - Coordinates monitoring by delegating to specialized handlers.
 
 
 **Key Methods:**
 **Key Methods:**
-- `start()` - Initialize monitoring loop
-- `_check_order_fills()` - Order execution detection
-- `_check_price_alarms()` - Price alert monitoring
-- `_check_external_trades()` - External trade detection
-- `_process_disappeared_orders()` - **Enhanced** order disappearance handling
-- `_check_for_recent_fills_for_order()` - **NEW** Recent fill verification
-- `_activate_pending_stop_losses()` - Stop loss activation
-- `_check_pending_triggers()` - Stop loss trigger monitoring
-- `_cleanup_orphaned_stop_losses()` - Orphaned order cleanup
+- `start()` / `stop()` - Manages the monitoring lifecycle.
+- `_monitor_loop()` - Main loop that orchestrates calls to delegate handlers.
+- `_update_cached_data()` - Updates the shared `MarketMonitorCache`.
+- `_initialize_tracking()` - Sets up initial cache state and runs startup sync.
+
+### **👁️ src/monitoring/external_event_monitor.py (NEW - 414 lines)**
+- Checks for and processes trades made outside the bot (external fills).
+- Manages `last_processed_trade_time` for external fill processing.
+- Handles price alarm checking by utilizing `AlarmManager`.
+- Sends notifications for triggered alarms and processed external trades.
+
+**Key Classes:**
+- `ExternalEventMonitor`
+
+**Key Methods:**
+- `_check_price_alarms()`
+- `_send_alarm_notification()`
+- `_check_external_trades()`
+
+### **🔄 src/monitoring/order_fill_processor.py (NEW - 324 lines)**
+- Processes order fills and activations.
+- Detects and handles orders that disappear from the exchange.
+- Activates pending stop-loss orders when their primary entry orders are filled.
+- Manages `last_known_orders` in the cache for detecting disappeared orders.
+- Uses `last_processed_trade_time_helper` for its `_check_for_recent_fills_for_order` helper.
+
+**Key Classes:**
+- `OrderFillProcessor`
+
+**Key Methods:**
+- `_check_order_fills()`
+- `_process_disappeared_orders()`
+- `_activate_pending_stop_losses_from_trades()`
+- `_check_for_recent_fills_for_order()`
+
+### **⚖️ src/monitoring/position_synchronizer.py (NEW - 487 lines)**
+- Synchronizes the bot's understanding of positions with the actual state on the exchange.
+- Handles "orphaned positions":
+    - Exchange has a position, bot does not (creates a new lifecycle).
+    - Bot has a `position_opened` lifecycle, exchange does not (closes the lifecycle).
+- Performs an immediate position sync on startup.
+- Estimates entry prices for orphaned positions if necessary.
+
+**Key Classes:**
+- `PositionSynchronizer`
+
+**Key Methods:**
+- `_auto_sync_orphaned_positions()`
+- `_immediate_startup_auto_sync()`
+- `_estimate_entry_price_for_orphaned_position()`
+- `_send_startup_auto_sync_notification()`
+
+### **🛡️ src/monitoring/risk_cleanup_manager.py (NEW - 501 lines)**
+- Manages various risk and cleanup routines.
+- Checks and executes legacy pending SL/TP triggers.
+- Implements automatic stop-loss based on `Config.STOP_LOSS_PERCENTAGE`.
+- Cleans up orphaned `stop_limit_trigger` orders.
+- Detects, tracks, and cleans up externally placed stop-loss orders (via `shared_state['external_stop_losses']`).
+- Cleans up `pending_sl_activation` orders if their parent entry order is gone and no position exists.
+
+**Key Classes:**
+- `RiskCleanupManager`
+
+**Key Methods:**
+- `_check_pending_triggers()`
+- `_check_automatic_risk_management()`
+- `_cleanup_orphaned_stop_losses()`
+- `_check_external_stop_loss_orders()`
+- `_cleanup_external_stop_loss_tracking()`
+- `_cleanup_orphaned_pending_sl_activations()`
+
+### **🔔 src/monitoring/alarm_manager.py (246 lines)**
+- Manages price alarms (add, remove, trigger).
+- Persists alarms to `price_alarms.json`.
+- Provides methods to get alarms by user or all active alarms.
 
 
 ### **📱 src/notifications/notification_manager.py**
 ### **📱 src/notifications/notification_manager.py**
 **Rich Telegram notifications (343 lines)**
 **Rich Telegram notifications (343 lines)**

+ 414 - 0
src/monitoring/external_event_monitor.py

@@ -0,0 +1,414 @@
+#!/usr/bin/env python3
+"""
+Monitors external events like trades made outside the bot and price alarms.
+"""
+
+import logging
+import asyncio
+from datetime import datetime, timedelta, timezone
+from typing import Optional, Dict, Any, List
+
+# Assuming AlarmManager will be moved here or imported appropriately
+# from .alarm_manager import AlarmManager 
+from src.monitoring.alarm_manager import AlarmManager # Keep if AlarmManager stays in its own file as per original structure
+from src.utils.token_display_formatter import get_formatter
+
+logger = logging.getLogger(__name__)
+
+class ExternalEventMonitor:
+    def __init__(self, trading_engine, notification_manager, alarm_manager, market_monitor_cache, shared_state):
+        self.trading_engine = trading_engine
+        self.notification_manager = notification_manager
+        self.alarm_manager = alarm_manager
+        self.market_monitor_cache = market_monitor_cache
+        self.shared_state = shared_state # Expected to contain {'external_stop_losses': ...}
+        self.last_processed_trade_time: Optional[datetime] = None
+        # Add necessary initializations, potentially loading last_processed_trade_time
+
+    async def _check_price_alarms(self):
+        """Check price alarms and trigger notifications."""
+        try:
+            active_alarms = self.alarm_manager.get_all_active_alarms()
+            
+            if not active_alarms:
+                return
+            
+            tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
+            
+            for token in tokens_to_check:
+                try:
+                    symbol = f"{token}/USDC:USDC"
+                    market_data = self.trading_engine.get_market_data(symbol)
+                    
+                    if not market_data or not market_data.get('ticker'):
+                        continue
+                    
+                    current_price = float(market_data['ticker'].get('last', 0))
+                    if current_price <= 0:
+                        continue
+                    
+                    token_alarms = [alarm for alarm in active_alarms if alarm['token'] == token]
+                    
+                    for alarm in token_alarms:
+                        target_price = alarm['target_price']
+                        direction = alarm['direction']
+                        
+                        should_trigger = False
+                        if direction == 'above' and current_price >= target_price:
+                            should_trigger = True
+                        elif direction == 'below' and current_price <= target_price:
+                            should_trigger = True
+                        
+                        if should_trigger:
+                            triggered_alarm = self.alarm_manager.trigger_alarm(alarm['id'], current_price)
+                            if triggered_alarm:
+                                await self._send_alarm_notification(triggered_alarm)
+                
+                except Exception as e:
+                    logger.error(f"Error checking alarms for {token}: {e}")
+                    
+        except Exception as e:
+            logger.error("❌ Error checking price alarms: {e}")
+    
+    async def _send_alarm_notification(self, alarm: Dict[str, Any]):
+        """Send notification for triggered alarm."""
+        try:
+            if self.notification_manager:
+                await self.notification_manager.send_alarm_triggered_notification(
+                    alarm['token'], 
+                    alarm['target_price'], 
+                    alarm['triggered_price'], 
+                    alarm['direction']
+                )
+            else:
+                logger.info(f"🔔 ALARM TRIGGERED: {alarm['token']} @ ${alarm['triggered_price']:,.2f}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error sending alarm notification: {e}")
+    
+    async def _check_external_trades(self):
+        """Check for trades made outside the Telegram bot and update stats."""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                logger.warning("TradingStats not available in _check_external_trades. Skipping.")
+                return
+
+            external_trades_processed = 0
+            symbols_with_fills = set()
+
+            recent_fills = self.trading_engine.get_recent_fills()
+            if not recent_fills:
+                logger.debug("No recent fills data available")
+                return
+
+            if not hasattr(self, 'last_processed_trade_time') or self.last_processed_trade_time is None:
+                try:
+                    # Ensure this metadata key is the one used by MarketMonitor for saving this state.
+                    last_time_str = stats._get_metadata('market_monitor_last_processed_trade_time') 
+                    if last_time_str:
+                        self.last_processed_trade_time = datetime.fromisoformat(last_time_str).replace(tzinfo=timezone.utc)
+                    else:
+                        self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
+                except Exception: 
+                     self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
+
+            for fill in recent_fills:
+                try:
+                    trade_id = fill.get('id')
+                    timestamp_ms = fill.get('timestamp')
+                    symbol_from_fill = fill.get('symbol')
+                    side_from_fill = fill.get('side')
+                    amount_from_fill = float(fill.get('amount', 0))
+                    price_from_fill = float(fill.get('price', 0))
+                    
+                    timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) if timestamp_ms else datetime.now(timezone.utc)
+                    
+                    if self.last_processed_trade_time and timestamp_dt <= self.last_processed_trade_time:
+                        continue
+                    
+                    fill_processed_this_iteration = False
+                    
+                    if not (symbol_from_fill and side_from_fill and amount_from_fill > 0 and price_from_fill > 0):
+                        logger.warning(f"Skipping fill with incomplete data: {fill}")
+                        continue
+
+                    full_symbol = symbol_from_fill
+                    token = symbol_from_fill.split('/')[0] if '/' in symbol_from_fill else symbol_from_fill.split(':')[0]
+
+                    exchange_order_id_from_fill = fill.get('info', {}).get('oid')
+
+                    if exchange_order_id_from_fill:
+                        pending_lc = stats.get_lifecycle_by_entry_order_id(exchange_order_id_from_fill, status='pending')
+                        if pending_lc and pending_lc.get('symbol') == full_symbol:
+                            success = stats.update_trade_position_opened(
+                                lifecycle_id=pending_lc['trade_lifecycle_id'],
+                                entry_price=price_from_fill,
+                                entry_amount=amount_from_fill,
+                                exchange_fill_id=trade_id
+                            )
+                            if success:
+                                logger.info(f"📈 Lifecycle ENTRY: {pending_lc['trade_lifecycle_id']} for {full_symbol} updated by fill {trade_id}.")
+                                symbols_with_fills.add(token)
+                                order_in_db_for_entry = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+                                if order_in_db_for_entry:
+                                    stats.update_order_status(order_db_id=order_in_db_for_entry['id'], new_status='filled', amount_filled_increment=amount_from_fill)
+                            fill_processed_this_iteration = True
+                    
+                    if not fill_processed_this_iteration:
+                        active_lc = None
+                        closure_reason_action_type = None
+                        bot_order_db_id_to_update = None
+
+                        if exchange_order_id_from_fill:
+                            bot_order_for_fill = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+                            if bot_order_for_fill and bot_order_for_fill.get('symbol') == full_symbol:
+                                order_type = bot_order_for_fill.get('type')
+                                order_side = bot_order_for_fill.get('side')
+                                if order_type == 'market':
+                                    potential_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                                    if potential_lc:
+                                        lc_pos_side = potential_lc.get('position_side')
+                                        if (lc_pos_side == 'long' and order_side == 'sell' and side_from_fill == 'sell') or \
+                                           (lc_pos_side == 'short' and order_side == 'buy' and side_from_fill == 'buy'):
+                                            active_lc = potential_lc
+                                            closure_reason_action_type = f"bot_exit_{lc_pos_side}_close"
+                                            bot_order_db_id_to_update = bot_order_for_fill.get('id')
+                                            logger.info(f"ℹ️ Lifecycle BOT EXIT: Fill {trade_id} (OID {exchange_order_id_from_fill}) for {full_symbol} matches bot exit for lifecycle {active_lc['trade_lifecycle_id']}.")
+                                
+                                if not active_lc:
+                                    lc_by_sl = stats.get_lifecycle_by_sl_order_id(exchange_order_id_from_fill, status='position_opened')
+                                    if lc_by_sl and lc_by_sl.get('symbol') == full_symbol:
+                                        active_lc = lc_by_sl
+                                        closure_reason_action_type = f"sl_{active_lc.get('position_side')}_close"
+                                        bot_order_db_id_to_update = bot_order_for_fill.get('id') 
+                                        logger.info(f"ℹ️ Lifecycle SL: Fill {trade_id} for OID {exchange_order_id_from_fill} matches SL for lifecycle {active_lc['trade_lifecycle_id']}.")
+                                    
+                                    if not active_lc: 
+                                        lc_by_tp = stats.get_lifecycle_by_tp_order_id(exchange_order_id_from_fill, status='position_opened')
+                                        if lc_by_tp and lc_by_tp.get('symbol') == full_symbol:
+                                            active_lc = lc_by_tp
+                                            closure_reason_action_type = f"tp_{active_lc.get('position_side')}_close"
+                                            bot_order_db_id_to_update = bot_order_for_fill.get('id') 
+                                            logger.info(f"ℹ️ Lifecycle TP: Fill {trade_id} for OID {exchange_order_id_from_fill} matches TP for lifecycle {active_lc['trade_lifecycle_id']}.")
+
+                        if not active_lc:
+                            potential_lc_external = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                            if potential_lc_external:
+                                lc_pos_side = potential_lc_external.get('position_side')
+                                if (lc_pos_side == 'long' and side_from_fill == 'sell') or \
+                                   (lc_pos_side == 'short' and side_from_fill == 'buy'):
+                                    active_lc = potential_lc_external
+                                    closure_reason_action_type = f"external_{lc_pos_side}_close"
+                                    logger.info(f"ℹ️ Lifecycle EXTERNAL CLOSE: Fill {trade_id} for {full_symbol} (no matching bot OID) for lifecycle {active_lc['trade_lifecycle_id']}.")
+                        
+                        if active_lc and closure_reason_action_type:
+                            lc_id = active_lc['trade_lifecycle_id']
+                            lc_entry_price = active_lc.get('entry_price', 0)
+                            lc_position_side = active_lc.get('position_side')
+
+                            realized_pnl = 0
+                            if lc_position_side == 'long':
+                                realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price)
+                            elif lc_position_side == 'short':
+                                realized_pnl = amount_from_fill * (lc_entry_price - price_from_fill)
+                            
+                            success = stats.update_trade_position_closed(
+                                lifecycle_id=lc_id, exit_price=price_from_fill,
+                                realized_pnl=realized_pnl, exchange_fill_id=trade_id
+                            )
+                            if success:
+                                pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                                formatter = get_formatter()
+                                logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({closure_reason_action_type}). PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
+                                symbols_with_fills.add(token)
+                                if self.notification_manager:
+                                    await self.notification_manager.send_external_trade_notification(
+                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
+                                        closure_reason_action_type, timestamp_dt.isoformat()
+                                    )
+                                stats._migrate_trade_to_aggregated_stats(lc_id)
+                                if bot_order_db_id_to_update:
+                                    stats.update_order_status(order_db_id=bot_order_db_id_to_update, new_status='filled', amount_filled_increment=amount_from_fill)
+                                fill_processed_this_iteration = True
+
+                    if not fill_processed_this_iteration:
+                        # Access external_stop_losses via self.shared_state
+                        if (exchange_order_id_from_fill and 
+                            self.shared_state.get('external_stop_losses') and
+                            exchange_order_id_from_fill in self.shared_state['external_stop_losses']):
+                            stop_loss_info = self.shared_state['external_stop_losses'][exchange_order_id_from_fill]
+                            formatter = get_formatter()
+                            logger.info(f"🛑 External SL (MM Tracking): {token} Order {exchange_order_id_from_fill} filled @ {formatter.format_price_with_symbol(price_from_fill, token)}")
+                            
+                            sl_active_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                            if sl_active_lc:
+                                lc_id = sl_active_lc['trade_lifecycle_id']
+                                lc_entry_price = sl_active_lc.get('entry_price', 0)
+                                lc_pos_side = sl_active_lc.get('position_side')
+                                realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price) if lc_pos_side == 'long' else amount_from_fill * (lc_entry_price - price_from_fill)
+                                
+                                success = stats.update_trade_position_closed(lc_id, price_from_fill, realized_pnl, trade_id)
+                                if success:
+                                    pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                                    logger.info(f"{pnl_emoji} Lifecycle CLOSED by External SL (MM): {lc_id}. PNL: {formatter.format_price_with_symbol(realized_pnl)}")
+                                    if self.notification_manager:
+                                        await self.notification_manager.send_stop_loss_execution_notification(
+                                            stop_loss_info, full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
+                                            f'{lc_pos_side}_closed_external_sl', timestamp_dt.isoformat(), realized_pnl
+                                        )
+                                    stats._migrate_trade_to_aggregated_stats(lc_id)
+                                    # Modify shared state carefully
+                                    if exchange_order_id_from_fill in self.shared_state['external_stop_losses']:
+                                        del self.shared_state['external_stop_losses'][exchange_order_id_from_fill]
+                                    fill_processed_this_iteration = True
+                            else:
+                                logger.warning(f"⚠️ External SL (MM) {exchange_order_id_from_fill} for {full_symbol}, but no active lifecycle found.")
+                    
+                    if not fill_processed_this_iteration:
+                        existing_open_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                        if existing_open_lc:
+                            lc_id = existing_open_lc['trade_lifecycle_id']
+                            lc_entry_price = existing_open_lc.get('entry_price', 0)
+                            lc_position_side = existing_open_lc.get('position_side')
+                            lc_current_size_before_fill = existing_open_lc.get('current_position_size', 0)
+
+                            is_potentially_closing_external_fill = False
+                            if lc_position_side == 'long' and side_from_fill.lower() == 'sell':
+                                is_potentially_closing_external_fill = True
+                            elif lc_position_side == 'short' and side_from_fill.lower() == 'buy':
+                                is_potentially_closing_external_fill = True
+                            
+                            if is_potentially_closing_external_fill:
+                                logger.info(f"ℹ️ Detected potentially closing external fill {trade_id} for {full_symbol} (Lifecycle: {lc_id}). Verifying exchange position state...")
+                                
+                                fresh_positions_after_fill = self.trading_engine.get_positions() or []
+                                position_on_exchange_after_fill = None
+                                for pos in fresh_positions_after_fill:
+                                    if pos.get('symbol') == full_symbol:
+                                        position_on_exchange_after_fill = pos
+                                        break
+                                
+                                position_is_closed_on_exchange = False
+                                if position_on_exchange_after_fill is None:
+                                    position_is_closed_on_exchange = True
+                                    logger.info(f"✅ Exchange Verification: Position for {full_symbol} (Lifecycle: {lc_id}) not found after fill {trade_id}. Confirming closure.")
+                                elif abs(float(position_on_exchange_after_fill.get('contracts', 0))) < 1e-9:
+                                    position_is_closed_on_exchange = True
+                                    logger.info(f"✅ Exchange Verification: Position for {full_symbol} (Lifecycle: {lc_id}) has zero size on exchange after fill {trade_id}. Confirming closure.")
+
+                                if position_is_closed_on_exchange:
+                                    amount_for_pnl_calc = amount_from_fill
+                                    if abs(lc_current_size_before_fill - amount_from_fill) < 0.000001 * amount_from_fill:
+                                         amount_for_pnl_calc = lc_current_size_before_fill
+
+                                    logger.info(f"ℹ️ Attempting to close lifecycle {lc_id} for {full_symbol} via confirmed external fill {trade_id}.")
+                                    realized_pnl = 0
+                                    if lc_position_side == 'long':
+                                        realized_pnl = amount_for_pnl_calc * (price_from_fill - lc_entry_price)
+                                    elif lc_position_side == 'short':
+                                        realized_pnl = amount_for_pnl_calc * (lc_entry_price - price_from_fill)
+                                    
+                                    success = stats.update_trade_position_closed(
+                                        lifecycle_id=lc_id, 
+                                        exit_price=price_from_fill, 
+                                        realized_pnl=realized_pnl, 
+                                        exchange_fill_id=trade_id
+                                    )
+                                    if success:
+                                        pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                                        formatter = get_formatter()
+                                        logger.info(f"{pnl_emoji} Lifecycle CLOSED (Verified External): {lc_id}. PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
+                                        symbols_with_fills.add(token)
+                                        if self.notification_manager:
+                                            await self.notification_manager.send_external_trade_notification(
+                                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
+                                                f"verified_external_{lc_position_side}_close",
+                                                timestamp_dt.isoformat()
+                                            )
+                                        stats._migrate_trade_to_aggregated_stats(lc_id)
+                                        fill_processed_this_iteration = True
+                                    else:
+                                        logger.error(f"❌ Failed to close lifecycle {lc_id} via verified external fill {trade_id}.")
+                                else:
+                                    current_size_on_exchange = float(position_on_exchange_after_fill.get('contracts', 0)) if position_on_exchange_after_fill else 'Unknown'
+                                    logger.warning(f"⚠️ External fill {trade_id} for {full_symbol} (Lifecycle: {lc_id}, Amount: {amount_from_fill}) did NOT fully close position. Exchange size now: {current_size_on_exchange}. Lifecycle remains open. Fill will be recorded as 'external_unmatched'.")
+
+                    if not fill_processed_this_iteration:
+                        # Access cached_positions via self.market_monitor_cache for diagnostics
+                        current_positions_from_cache_map = {
+                            pos.get('symbol'): pos for pos in (self.market_monitor_cache.cached_positions or [])
+                            if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
+                        }
+                        all_open_positions_in_db = stats.get_open_positions()
+                        db_open_symbols = {pos_db.get('symbol') for pos_db in all_open_positions_in_db}
+
+                        if full_symbol in db_open_symbols:
+                            logger.error(f"🚨 DIAGNOSTIC: Contradiction for {full_symbol}! get_open_positions() includes it, but get_trade_by_symbol_and_status('{full_symbol}', 'position_opened') failed to find it within _check_external_trades context for fill {trade_id}. This needs investigation into TradingStats symbol querying.")
+                        
+                        potential_match_failure_logged = False
+                        if not stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened'):
+                            logger.warning(f"⚠️ DIAGNOSTIC for UNMATCHED FILL {trade_id} ({full_symbol}):")
+                            logger.warning(f"   Fill details: Side={side_from_fill}, Amount={amount_from_fill}, Price={price_from_fill}")
+                            logger.warning(f"   Attempted lookup with full_symbol='{full_symbol}' and status='position_opened' found NO active lifecycle.")
+                            if all_open_positions_in_db:
+                                logger.warning(f"   However, DB currently has these 'position_opened' lifecycles (symbol - lifecycle_id):")
+                                for db_pos in all_open_positions_in_db:
+                                    logger.warning(f"     - '{db_pos.get('symbol')}' - ID: {db_pos.get('trade_lifecycle_id')}")
+                                base_token_fill = full_symbol.split('/')[0].split(':')[0]
+                                near_matches = [db_s for db_s in db_open_symbols if base_token_fill in db_s]
+                                if near_matches:
+                                    logger.warning(f"   Possible near matches in DB for base token '{base_token_fill}': {near_matches}")
+                                else:
+                                    logger.warning(f"   No near matches found in DB for base token '{base_token_fill}'.")
+                            else:
+                                logger.warning("   DB has NO 'position_opened' lifecycles at all right now.")
+                            potential_match_failure_logged = True
+                        
+                        linked_order_db_id = None
+                        if exchange_order_id_from_fill:
+                            order_in_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+                            if order_in_db:
+                                linked_order_db_id = order_in_db.get('id')
+                                logger.info(f"🔗 Fallback: Fill {trade_id} for OID {exchange_order_id_from_fill} (DB ID {linked_order_db_id}) not tied to active lifecycle step.")
+                                current_status = order_in_db.get('status', '')
+                                if current_status in ['open', 'partially_filled', 'pending_submission']:
+                                    amt_req = float(order_in_db.get('amount_requested', 0))
+                                    amt_filled_so_far = float(order_in_db.get('amount_filled',0))
+                                    new_status = 'partially_filled'
+                                    if (amt_filled_so_far + amount_from_fill) >= amt_req - 1e-9:
+                                        new_status = 'filled'
+                                    stats.update_order_status(
+                                        order_db_id=linked_order_db_id, new_status=new_status,
+                                        amount_filled_increment=amount_from_fill
+                                    )
+                                    logger.info(f"📊 Updated bot order {linked_order_db_id} (fallback): {current_status} → {new_status}")
+                        
+                        if not (hasattr(stats, 'get_trade_by_exchange_fill_id') and stats.get_trade_by_exchange_fill_id(trade_id)):
+                            stats.record_trade(
+                                full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
+                                exchange_fill_id=trade_id, trade_type="external_unmatched",
+                                timestamp=timestamp_dt.isoformat(),
+                                linked_order_table_id_to_link=linked_order_db_id 
+                            )
+                            logger.info(f"📋 Recorded trade via FALLBACK: {trade_id} (Unmatched External Fill)")
+                        fill_processed_this_iteration = True
+
+                    if fill_processed_this_iteration:
+                        external_trades_processed += 1
+                        if self.last_processed_trade_time is None or timestamp_dt > self.last_processed_trade_time:
+                           self.last_processed_trade_time = timestamp_dt
+                        
+                except Exception as e:
+                    logger.error(f"Error processing fill {fill.get('id', 'N/A')}: {e}", exc_info=True)
+                    continue
+            
+            if external_trades_processed > 0:
+                stats._set_metadata('market_monitor_last_processed_trade_time', self.last_processed_trade_time.isoformat())
+                logger.info(f"💾 Saved MarketMonitor state (last_processed_trade_time) to DB: {self.last_processed_trade_time.isoformat()}")
+                logger.info(f"📊 Processed {external_trades_processed} external trades")
+                if symbols_with_fills:
+                    logger.info(f"ℹ️ Symbols with processed fills this cycle: {list(symbols_with_fills)}")
+                
+        except Exception as e:
+            logger.error(f"❌ Error checking external trades: {e}", exc_info=True) 

+ 266 - 1988
src/monitoring/market_monitor.py

@@ -1,61 +1,91 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 """
 """
-Market Monitor - Handles external trade monitoring and heartbeat functionality.
+Market Monitor - Main coordinator for monitoring market events, orders, and positions.
 """
 """
 
 
 import logging
 import logging
 import asyncio
 import asyncio
 from datetime import datetime, timedelta, timezone
 from datetime import datetime, timedelta, timezone
 from typing import Optional, Dict, Any, List
 from typing import Optional, Dict, Any, List
-import os
-import json
-
-from telegram.ext import CallbackContext
 
 
 from src.config.config import Config
 from src.config.config import Config
 from src.monitoring.alarm_manager import AlarmManager
 from src.monitoring.alarm_manager import AlarmManager
 from src.utils.token_display_formatter import get_formatter
 from src.utils.token_display_formatter import get_formatter
 
 
+# New processor/monitor/manager classes
+from src.monitoring.order_fill_processor import OrderFillProcessor
+from src.monitoring.position_synchronizer import PositionSynchronizer
+from src.monitoring.external_event_monitor import ExternalEventMonitor
+from src.monitoring.risk_cleanup_manager import RiskCleanupManager
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+class MarketMonitorCache:
+    """Simple data class to hold cached data for MarketMonitor and its delegates."""
+    def __init__(self):
+        self.cached_positions: List[Dict[str, Any]] = []
+        self.cached_orders: List[Dict[str, Any]] = []
+        self.cached_balance: Optional[Dict[str, Any]] = None
+        self.last_cache_update: Optional[datetime] = None
+        
+        # last_known_orders is used by OrderFillProcessor to detect disappeared orders
+        self.last_known_orders: set = set() 
+        # last_known_positions is used by MarketMonitor._update_cached_data for logging count changes
+        self.last_known_positions: Dict[str, Any] = {}
+
+        # last_processed_trade_time_helper is used by OrderFillProcessor._check_for_recent_fills_for_order
+        # This is distinct from ExternalEventMonitor.last_processed_trade_time which is for the main external fill processing
+        self.last_processed_trade_time_helper: Optional[datetime] = None
+
+
 class MarketMonitor:
 class MarketMonitor:
-    """Handles external trade monitoring and market events."""
+    """Coordinates monitoring activities by delegating to specialized processors and managers."""
     
     
     def __init__(self, trading_engine, notification_manager=None):
     def __init__(self, trading_engine, notification_manager=None):
-        """Initialize market monitor."""
         self.trading_engine = trading_engine
         self.trading_engine = trading_engine
         self.notification_manager = notification_manager
         self.notification_manager = notification_manager
         self._monitoring_active = False
         self._monitoring_active = False
         self._monitor_task = None
         self._monitor_task = None
         
         
-        # Enhanced tracking for Phase 3+
-        self.last_known_orders = set()  # Set of order IDs we know about
-        self.last_known_positions = {}  # Dict mapping symbol -> position data
+        self.alarm_manager = AlarmManager() # AlarmManager is standalone
         
         
-        # Price alarms tracking
-        self.price_alarms = {}  # Dict mapping alarm_id -> alarm_data
-        self.next_alarm_id = 1
+        # Cache and Shared State
+        self.cache = MarketMonitorCache()
+        # Shared state for data that might be written by one manager and read by another
+        # For now, primarily for external_stop_losses (written by RiskCleanupManager, read by ExternalEventMonitor)
+        self.shared_state = {
+            'external_stop_losses': {} # Managed by RiskCleanupManager
+        }
+
+        # Initialize specialized handlers
+        self.order_fill_processor = OrderFillProcessor(
+            trading_engine=self.trading_engine,
+            notification_manager=self.notification_manager,
+            market_monitor_cache=self.cache 
+        )
+        self.position_synchronizer = PositionSynchronizer(
+            trading_engine=self.trading_engine,
+            notification_manager=self.notification_manager,
+            market_monitor_cache=self.cache
+        )
+        self.external_event_monitor = ExternalEventMonitor(
+            trading_engine=self.trading_engine,
+            notification_manager=self.notification_manager,
+            alarm_manager=self.alarm_manager,
+            market_monitor_cache=self.cache,
+            shared_state=self.shared_state 
+        )
+        self.risk_cleanup_manager = RiskCleanupManager(
+            trading_engine=self.trading_engine,
+            notification_manager=self.notification_manager,
+            market_monitor_cache=self.cache,
+            shared_state=self.shared_state # Pass shared_state here
+        )
         
         
-        # External stop loss tracking
-        self.external_stop_losses = {}  # symbol -> {order_id, stop_price, side, amount}
-        
-        # 🆕 CONTINUOUS DATA CACHE: Keep bot state updated
-        self.cached_positions = []  # Fresh exchange positions
-        self.cached_orders = []     # Fresh exchange orders
-        self.cached_balance = None  # Fresh balance data
-        self.last_cache_update = None
-        
-        # External trade monitoring
-        self.last_processed_trade_time: Optional[datetime] = None
-        
-        # Alarm management
-        self.alarm_manager = AlarmManager()
-        
-        # Load persistent state
+        # Load minimal persistent state if any (most state is now in delegates or transient)
         self._load_state()
         self._load_state()
         
         
     async def start(self):
     async def start(self):
-        """Start the market monitor."""
         if self._monitoring_active:
         if self._monitoring_active:
             logger.warning("Market monitor is already active")
             logger.warning("Market monitor is already active")
             return
             return
@@ -63,19 +93,14 @@ class MarketMonitor:
         self._monitoring_active = True
         self._monitoring_active = True
         logger.info("🔄 Market monitor started")
         logger.info("🔄 Market monitor started")
         
         
-        # Initialize tracking
         await self._initialize_tracking()
         await self._initialize_tracking()
-        
-        # Start the monitoring loop
         self._monitor_task = asyncio.create_task(self._monitor_loop())
         self._monitor_task = asyncio.create_task(self._monitor_loop())
     
     
     async def stop(self):
     async def stop(self):
-        """Stop the market monitor."""
         if not self._monitoring_active:
         if not self._monitoring_active:
             return
             return
         
         
         self._monitoring_active = False
         self._monitoring_active = False
-        
         if self._monitor_task:
         if self._monitor_task:
             self._monitor_task.cancel()
             self._monitor_task.cancel()
             try:
             try:
@@ -83,2032 +108,285 @@ class MarketMonitor:
             except asyncio.CancelledError:
             except asyncio.CancelledError:
                 pass
                 pass
         
         
-        self._save_state()
+        self._save_state() # Save minimal state if any
         logger.info("🛑 Market monitor stopped")
         logger.info("🛑 Market monitor stopped")
     
     
     def _load_state(self):
     def _load_state(self):
-        """Load market monitor state from SQLite DB via TradingStats."""
+        """Load minimal MarketMonitor-specific state if necessary. Most state is now managed by delegates."""
+        # Example: If AlarmManager needed to load state and was not self-sufficient
+        # self.alarm_manager.load_alarms() 
+        
+        # last_processed_trade_time is loaded by ExternalEventMonitor itself.
+        # external_stop_losses are in-memory in RiskCleanupManager for now.
+        # last_processed_trade_time_helper for OrderFillProcessor can also be loaded if persisted.
         stats = self.trading_engine.get_stats()
         stats = self.trading_engine.get_stats()
         if not stats:
         if not stats:
-            logger.warning("⚠️ TradingStats not available, cannot load MarketMonitor state.")
-            self.last_processed_trade_time = None
+            logger.warning("⚠️ TradingStats not available, cannot load MarketMonitor helper states.")
             return
             return
-
         try:
         try:
-            last_time_str = stats._get_metadata('market_monitor_last_processed_trade_time')
-            if last_time_str:
-                self.last_processed_trade_time = datetime.fromisoformat(last_time_str)
-                # Ensure it's timezone-aware (UTC)
-                if self.last_processed_trade_time.tzinfo is None:
-                    self.last_processed_trade_time = self.last_processed_trade_time.replace(tzinfo=timezone.utc)
-                else:
-                    self.last_processed_trade_time = self.last_processed_trade_time.astimezone(timezone.utc)
-                logger.info(f"🔄 Loaded MarketMonitor state from DB: last_processed_trade_time = {self.last_processed_trade_time.isoformat()}")
-            else:
-                logger.info("💨 No MarketMonitor state (last_processed_trade_time) found in DB. Will start with fresh external trade tracking.")
-                self.last_processed_trade_time = None
+            helper_time_str = stats._get_metadata('order_fill_processor_last_processed_trade_time_helper')
+            if helper_time_str:
+                dt_obj = datetime.fromisoformat(helper_time_str)
+                self.cache.last_processed_trade_time_helper = dt_obj.replace(tzinfo=timezone.utc) if dt_obj.tzinfo is None else dt_obj.astimezone(timezone.utc)
+                logger.info(f"🔄 Loaded OrderFillProcessor helper state: last_processed_trade_time_helper = {self.cache.last_processed_trade_time_helper.isoformat()}")
         except Exception as e:
         except Exception as e:
-            logger.error(f"Error loading MarketMonitor state from DB: {e}. Proceeding with default state.")
-            self.last_processed_trade_time = None
+            logger.error(f"Error loading OrderFillProcessor helper state: {e}")
+
+        logger.info("MarketMonitor _load_state: Minimal state loaded (most state handled by delegates).")
 
 
     def _save_state(self):
     def _save_state(self):
-        """Save market monitor state to SQLite DB via TradingStats."""
+        """Save minimal MarketMonitor-specific state if necessary."""
+        # self.alarm_manager.save_alarms()
         stats = self.trading_engine.get_stats()
         stats = self.trading_engine.get_stats()
         if not stats:
         if not stats:
-            logger.warning("⚠️ TradingStats not available, cannot save MarketMonitor state.")
+            logger.warning("⚠️ TradingStats not available, cannot save MarketMonitor helper states.")
             return
             return
-
         try:
         try:
-            if self.last_processed_trade_time:
-                # Ensure timestamp is UTC before saving
-                lptt_utc = self.last_processed_trade_time
-                if lptt_utc.tzinfo is None:
-                    lptt_utc = lptt_utc.replace(tzinfo=timezone.utc)
-                else:
-                    lptt_utc = lptt_utc.astimezone(timezone.utc)
-                
-                stats._set_metadata('market_monitor_last_processed_trade_time', lptt_utc.isoformat())
-                logger.info(f"💾 Saved MarketMonitor state (last_processed_trade_time) to DB: {lptt_utc.isoformat()}")
-            else:
-                # If it's None, we might want to remove the key or save it as an empty string
-                # For now, let's assume we only save if there is a time. Or remove it.
-                stats._set_metadata('market_monitor_last_processed_trade_time', '') # Or handle deletion
-                logger.info("💾 MarketMonitor state (last_processed_trade_time) is None, saved as empty in DB.")
+            if self.cache.last_processed_trade_time_helper:
+                lptt_helper_utc = self.cache.last_processed_trade_time_helper.astimezone(timezone.utc)
+                stats._set_metadata('order_fill_processor_last_processed_trade_time_helper', lptt_helper_utc.isoformat())
+                logger.info(f"💾 Saved OrderFillProcessor helper state (last_processed_trade_time_helper) to DB: {lptt_helper_utc.isoformat()}")
         except Exception as e:
         except Exception as e:
-            logger.error(f"Error saving MarketMonitor state to DB: {e}")
+            logger.error(f"Error saving OrderFillProcessor helper state: {e}")
+            
+        logger.info("MarketMonitor _save_state: Minimal state saved.")
     
     
     async def _initialize_tracking(self):
     async def _initialize_tracking(self):
-        """Initialize order and position tracking."""
+        """Initialize basic tracking for cache."""
         try:
         try:
-            # Initialize order tracking
-            try:
-                orders = self.trading_engine.get_orders() or []
-                self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
-                logger.info(f"📋 Initialized tracking with {len(orders)} open orders")
-            except Exception as e:
-                logger.error(f"❌ Failed to initialize order tracking: {e}")
-                self.last_known_orders = set()
-
-            # Initialize position tracking
-            try:
-                positions = self.trading_engine.get_positions() or []
-                self.last_known_positions = {
-                    pos.get('symbol'): pos for pos in positions 
-                    if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 0
-                }
-                logger.info(f"📊 Initialized tracking with {len(positions)} positions")
-                
-                # 🆕 IMMEDIATE AUTO-SYNC: Check for orphaned positions right after initialization
-                if positions:
-                    await self._immediate_startup_auto_sync()
+            orders = self.trading_engine.get_orders() or []
+            # Initialize cache.last_known_orders for the first cycle of OrderFillProcessor
+            self.cache.last_known_orders = {order.get('id') for order in orders if order.get('id')}
+            logger.info(f"📋 Initialized cache with {len(orders)} open orders for first cycle comparison")
+
+            positions = self.trading_engine.get_positions() or []
+            # Initialize cache.last_known_positions for the first cycle of _update_cached_data logging
+            self.cache.last_known_positions = {
+                pos.get('symbol'): pos for pos in positions 
+                if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9 # Use a small tolerance
+            }
+            logger.info(f"📊 Initialized cache with {len(positions)} positions for first cycle comparison")
+            
+            # Perform immediate startup sync using the PositionSynchronizer
+            # This fetches fresh positions itself, so no need to pass `positions`
+            await self.position_synchronizer._immediate_startup_auto_sync()
                     
                     
-            except Exception as e:
-                logger.error(f"❌ Failed to initialize position tracking: {e}")
-                self.last_known_positions = {}
-
         except Exception as e:
         except Exception as e:
-            logger.error(f"❌ Failed to initialize tracking: {e}")
-            self.last_known_orders = set()
-            self.last_known_positions = {}
+            logger.error(f"❌ Failed to initialize tracking: {e}", exc_info=True)
+            self.cache.last_known_orders = set()
+            self.cache.last_known_positions = {}
     
     
     async def _monitor_loop(self):
     async def _monitor_loop(self):
-        """Main monitoring loop that runs every BOT_HEARTBEAT_SECONDS."""
         try:
         try:
             loop_count = 0
             loop_count = 0
             while self._monitoring_active:
             while self._monitoring_active:
-                # 🆕 CONTINUOUS UPDATE: Cache fresh exchange data every heartbeat
-                await self._update_cached_data()
+                await self._update_cached_data() # Updates self.cache
                 
                 
-                # 🆕 PHASE 4: Check trades table for pending stop loss activation first (highest priority)
-                await self._activate_pending_stop_losses_from_trades()
+                # Order of operations:
+                # 1. Process fills and activate pending SLs based on fills (OrderFillProcessor)
+                # 2. Check for external trades/events (ExternalEventMonitor)
+                # 3. Check for various triggers and risk conditions (RiskCleanupManager)
+                # 4. Periodic cleanup and sync tasks (RiskCleanupManager, PositionSynchronizer)
+
+                await self.order_fill_processor._activate_pending_stop_losses_from_trades()
+                await self.order_fill_processor._check_order_fills() 
+                
+                await self.external_event_monitor._check_price_alarms()
+                await self.external_event_monitor._check_external_trades()
                 
                 
-                await self._check_order_fills()
-                await self._check_price_alarms()
-                await self._check_external_trades()
-                await self._check_pending_triggers()
-                await self._check_automatic_risk_management()
-                await self._check_external_stop_loss_orders()
+                await self.risk_cleanup_manager._check_pending_triggers()
+                await self.risk_cleanup_manager._check_automatic_risk_management()
+                await self.risk_cleanup_manager._check_external_stop_loss_orders()
                 
                 
-                # Run orphaned stop loss cleanup every 10 heartbeats (less frequent but regular)
                 loop_count += 1
                 loop_count += 1
-                if loop_count % 10 == 0:
-                    await self._cleanup_orphaned_stop_losses()
-                    await self._cleanup_external_stop_loss_tracking()
+                if loop_count >= Config.MARKET_MONITOR_CLEANUP_INTERVAL_HEARTBEATS: # Use a config value
+                    logger.info(f"Running periodic cleanup and sync tasks (Loop count: {loop_count})")
+                    await self.risk_cleanup_manager._cleanup_orphaned_stop_losses()
+                    await self.risk_cleanup_manager._cleanup_external_stop_loss_tracking()
+                    await self.risk_cleanup_manager._cleanup_orphaned_pending_sl_activations() # User's requested new method
                     
                     
-                    # 🆕 AUTO-SYNC: Check for orphaned positions every 10 heartbeats
-                    await self._auto_sync_orphaned_positions()
+                    await self.position_synchronizer._auto_sync_orphaned_positions()
                     
                     
-                    loop_count = 0  # Reset counter to prevent overflow
+                    loop_count = 0
                 
                 
                 await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
                 await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
         except asyncio.CancelledError:
         except asyncio.CancelledError:
             logger.info("Market monitor loop cancelled")
             logger.info("Market monitor loop cancelled")
             raise
             raise
         except Exception as e:
         except Exception as e:
-            logger.error(f"Error in market monitor loop: {e}")
-            # Restart after error
-            if self._monitoring_active:
-                await asyncio.sleep(5)
-                await self._monitor_loop()
-    
+            logger.error(f"Error in market monitor loop: {e}", exc_info=True)
+            if self._monitoring_active: # Attempt to restart loop after a delay
+                logger.info("Attempting to restart market monitor loop after error...")
+                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS * 2) # Wait longer before restart
+                # Re-create task to ensure clean start if previous one errored out badly.
+                # This assumes _monitor_loop itself doesn't recurse on all exceptions.
+                if self._monitoring_active: # Check again, stop() might have been called
+                     self._monitor_task = asyncio.create_task(self._monitor_loop())
+
     async def _update_cached_data(self):
     async def _update_cached_data(self):
-        """🆕 Continuously update cached exchange data every heartbeat."""
+        """Continuously update cached exchange data for all components to use."""
         try:
         try:
-            # Fetch fresh data from exchange
             fresh_positions_list = self.trading_engine.get_positions() or []
             fresh_positions_list = self.trading_engine.get_positions() or []
             fresh_orders_list = self.trading_engine.get_orders() or []
             fresh_orders_list = self.trading_engine.get_orders() or []
             fresh_balance = self.trading_engine.get_balance()
             fresh_balance = self.trading_engine.get_balance()
             
             
-            # Update primary cache immediately
-            self.cached_positions = fresh_positions_list
-            self.cached_orders = fresh_orders_list
-            self.cached_balance = fresh_balance
-            self.last_cache_update = datetime.now(timezone.utc)
+            # Update cache object
+            self.cache.cached_positions = fresh_positions_list
+            self.cache.cached_orders = fresh_orders_list
+            self.cache.cached_balance = fresh_balance
+            self.cache.last_cache_update = datetime.now(timezone.utc)
             
             
-            logger.debug(f"🔄 Fetched fresh cache: {len(fresh_positions_list)} positions, {len(fresh_orders_list)} orders")
+            logger.debug(f"🔄 Cache updated: {len(fresh_positions_list)} positions, {len(fresh_orders_list)} orders")
 
 
-            # Prepare current state maps for comparison and for updating last_known state
             current_exchange_position_map = {
             current_exchange_position_map = {
                 pos.get('symbol'): pos for pos in fresh_positions_list
                 pos.get('symbol'): pos for pos in fresh_positions_list
                 if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
                 if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
             }
             }
             current_exchange_order_ids = {order.get('id') for order in fresh_orders_list if order.get('id')}
             current_exchange_order_ids = {order.get('id') for order in fresh_orders_list if order.get('id')}
 
 
-            # Log changes by comparing with the state from the end of the previous cycle
-            if len(current_exchange_position_map) != len(self.last_known_positions):
-                logger.info(f"📊 Position count changed: {len(self.last_known_positions)} → {len(current_exchange_position_map)}")
+            if len(current_exchange_position_map) != len(self.cache.last_known_positions):
+                logger.info(f"📊 Position count changed: {len(self.cache.last_known_positions)} → {len(current_exchange_position_map)}")
             
             
-            if len(current_exchange_order_ids) != len(self.last_known_orders):
-                logger.info(f"📋 Order count changed: {len(self.last_known_orders)} → {len(current_exchange_order_ids)}")
+            if len(current_exchange_order_ids) != len(self.cache.last_known_orders):
+                logger.info(f"📋 Order count changed: {len(self.cache.last_known_orders)} → {len(current_exchange_order_ids)}")
 
 
-            # Update last_known_xxx to the current exchange state for the *next* cycle's comparison
-            self.last_known_positions = current_exchange_position_map
-            self.last_known_orders = current_exchange_order_ids
+            # Update last_known_xxx in the cache for the *next* cycle's comparison
+            # OrderFillProcessor uses last_known_orders for its own logic.
+            # MarketMonitor uses last_known_positions for logging changes here.
+            self.cache.last_known_positions = current_exchange_position_map
+            # self.cache.last_known_orders is updated by OrderFillProcessor after it processes fills.
+            # For the first run of _check_order_fills, it needs the initial set from _initialize_tracking.
+            # Subsequent runs, it will use the state from its previous run.
+            # So, MarketMonitor should not overwrite self.cache.last_known_orders here after initialization.
             
             
-            # 💹 Update unrealized P&L and mark price in DB for open positions
             stats = self.trading_engine.get_stats()
             stats = self.trading_engine.get_stats()
             if stats and fresh_positions_list:
             if stats and fresh_positions_list:
-                for ex_pos in fresh_positions_list:
+                for ex_pos in fresh_positions_list: # Changed pos_data to ex_pos to match original
                     symbol = ex_pos.get('symbol')
                     symbol = ex_pos.get('symbol')
-                    if not symbol:
+                    if not symbol: 
                         continue
                         continue
 
 
+                    # Attempt to get the lifecycle_id for this symbol
                     db_trade = stats.get_trade_by_symbol_and_status(symbol, status='position_opened')
                     db_trade = stats.get_trade_by_symbol_and_status(symbol, status='position_opened')
-                    
                     if db_trade:
                     if db_trade:
                         lifecycle_id = db_trade.get('trade_lifecycle_id')
                         lifecycle_id = db_trade.get('trade_lifecycle_id')
                         if not lifecycle_id:
                         if not lifecycle_id:
-                            continue
-
-                        # Extract all relevant data from exchange position (ex_pos)
-                        # Ensure to handle cases where keys might be missing or values are None/empty strings
-                        
-                        current_size_from_ex = ex_pos.get('contracts') # Usually 'contracts' in CCXT
-                        if current_size_from_ex is not None:
-                            try: current_position_size = float(current_size_from_ex) 
-                            except (ValueError, TypeError): current_position_size = None
-                        else: current_position_size = None
-
-                        entry_price_from_ex = ex_pos.get('entryPrice') or ex_pos.get('entryPx')
-                        if entry_price_from_ex is not None:
-                            try: entry_price = float(entry_price_from_ex)
-                            except (ValueError, TypeError): entry_price = None
-                        else: entry_price = None
-
-                        mark_price_from_ex = ex_pos.get('markPrice') or ex_pos.get('markPx')
-                        if mark_price_from_ex is not None:
-                            try: mark_price = float(mark_price_from_ex)
-                            except (ValueError, TypeError): mark_price = None
-                        else: mark_price = None
-
-                        unrealized_pnl_from_ex = ex_pos.get('unrealizedPnl')
-                        if unrealized_pnl_from_ex is not None:
-                            try: unrealized_pnl = float(unrealized_pnl_from_ex)
-                            except (ValueError, TypeError): unrealized_pnl = None
-                        else: unrealized_pnl = None
-
-                        liquidation_price_from_ex = ex_pos.get('liquidationPrice')
-                        if liquidation_price_from_ex is not None:
-                            try: liquidation_price = float(liquidation_price_from_ex)
-                            except (ValueError, TypeError): liquidation_price = None
-                        else: liquidation_price = None
-
-                        margin_used_from_ex = ex_pos.get('marginUsed') # Or other keys like 'initialMargin', 'maintenanceMargin' depending on exchange
-                        if margin_used_from_ex is not None:
-                            try: margin_used = float(margin_used_from_ex)
-                            except (ValueError, TypeError): margin_used = None
-                        else: margin_used = None
-
-                        leverage_from_ex = ex_pos.get('leverage')
-                        if leverage_from_ex is not None:
-                            try: leverage = float(leverage_from_ex)
-                            except (ValueError, TypeError): leverage = None
-                        else: leverage = None
-
-                        position_value_from_ex = ex_pos.get('notional') # 'notional' is common for position value
-                        if position_value_from_ex is not None:
-                            try: position_value = float(position_value_from_ex)
-                            except (ValueError, TypeError): position_value = None
-                        else: position_value = None
-                        
-                        # Fallback for position_value if notional is not available but mark_price and size are
-                        if position_value is None and mark_price is not None and current_position_size is not None:
-                            position_value = abs(current_position_size) * mark_price
-
-                        # 🆕 Get P&L percentage from exchange if available
-                        roe_from_ex = ex_pos.get('percentage') # CCXT often uses 'percentage' for ROE
-                        if roe_from_ex is not None:
-                            try: unrealized_pnl_percentage_val = float(roe_from_ex) 
-                            except (ValueError, TypeError): unrealized_pnl_percentage_val = None
-                        else: unrealized_pnl_percentage_val = None
-
-                        stats.update_trade_market_data(
-                            trade_lifecycle_id=lifecycle_id, 
-                            unrealized_pnl=unrealized_pnl, 
-                            mark_price=mark_price,
-                            current_position_size=current_position_size,
-                            entry_price=entry_price, # This will update the entry price if the exchange provides a new average
-                            liquidation_price=liquidation_price,
-                            margin_used=margin_used,
-                            leverage=leverage,
-                            position_value=position_value,
-                            unrealized_pnl_percentage=unrealized_pnl_percentage_val # Pass the new field
-                        )
-            
-            # 🆕 Detect immediate changes for responsive notifications
-            if len(fresh_positions_list) != len(self.last_known_positions):
-                logger.info(f"📊 Position count changed: {len(self.last_known_positions)} → {len(current_exchange_position_map)}")
-            
-            if len(fresh_orders_list) != len(self.last_known_orders):
-                logger.info(f"📋 Order count changed: {len(self.last_known_orders)} → {len(current_exchange_order_ids)}")
-            
-        except Exception as e:
-            logger.error(f"❌ Error updating cached data: {e}")
-    
-    def get_cached_positions(self):
-        """Get cached positions (updated every heartbeat)."""
-        return self.cached_positions or []
-    
-    def get_cached_orders(self):
-        """Get cached orders (updated every heartbeat)."""
-        return self.cached_orders or []
-    
-    def get_cached_balance(self):
-        """Get cached balance (updated every heartbeat)."""
-        return self.cached_balance
-    
-    def get_cache_age_seconds(self):
-        """Get age of cached data in seconds."""
-        if not self.last_cache_update:
-            return float('inf')
-        return (datetime.now(timezone.utc) - self.last_cache_update).total_seconds()
-    
-    async def _check_order_fills(self):
-        """Check for filled orders and send notifications."""
-        try:
-            # Get current orders and positions
-            current_orders = self.cached_orders or [] # Use cache
-            current_positions = self.cached_positions or [] # Use cache
-            
-            # Get current order IDs
-            current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
-            
-            # Find filled orders (orders that were in last_known_orders but not in current_orders)
-            disappeared_order_ids = self.last_known_orders - current_order_ids
-            
-            if disappeared_order_ids:
-                logger.info(f"🎯 Detected {len(disappeared_order_ids)} bot orders no longer open: {list(disappeared_order_ids)}. Corresponding fills (if any) are processed by external trade checker.")
-                await self._process_disappeared_orders(disappeared_order_ids)
-            
-            # Update tracking data for open bot orders
-            self.last_known_orders = current_order_ids
-            # Position state is primarily managed by TradingStats based on all fills.
-            # This local tracking can provide supplementary logging if needed.
-            # await self._update_position_tracking(current_positions) 
-            
-        except Exception as e:
-            logger.error(f"❌ Error checking order fills: {e}")
-    
-    async def _process_filled_orders(self, filled_order_ids: set, current_positions: list):
-        """Process filled orders using enhanced position tracking."""
-        try:
-            # For bot-initiated orders, we'll detect changes in position size
-            # and send appropriate notifications using the enhanced system
-            
-            # This method will be triggered when orders placed through the bot are filled
-            # The external trade monitoring will handle trades made outside the bot
-            
-            # Update position tracking based on current positions
-            await self._update_position_tracking(current_positions)
-                
-        except Exception as e:
-            logger.error(f"❌ Error processing filled orders: {e}")
-    
-    async def _update_position_tracking(self, current_positions: list):
-        """Update position tracking and calculate P&L changes."""
-        try:
-            new_position_map = {}
-            formatter = get_formatter() # Get formatter
-            
-            for position in current_positions:
-                symbol = position.get('symbol')
-                contracts = float(position.get('contracts', 0))
-                entry_price = float(position.get('entryPx', 0))
-                
-                if symbol and contracts != 0:
-                    new_position_map[symbol] = {
-                        'contracts': contracts,
-                        'entry_price': entry_price
-                    }
-            
-            # Compare with previous positions to detect changes
-            for symbol, new_data in new_position_map.items():
-                old_data = self.last_known_positions.get(symbol)
-                token = symbol.split('/')[0] if '/' in symbol else symbol # Extract token
-                
-                if not old_data:
-                    # New position opened
-                    amount_str = formatter.format_amount(new_data['contracts'], token)
-                    price_str = formatter.format_price_with_symbol(new_data['entry_price'], token)
-                    logger.info(f"📈 New position detected (observed by MarketMonitor): {symbol} {amount_str} @ {price_str}. TradingStats is the definitive source.")
-                elif abs(new_data['contracts'] - old_data['contracts']) > 0.000001:
-                    # Position size changed
-                    change = new_data['contracts'] - old_data['contracts']
-                    change_str = formatter.format_amount(change, token)
-                    logger.info(f"📊 Position change detected (observed by MarketMonitor): {symbol} {change_str} contracts. TradingStats is the definitive source.")
-            
-            # Check for closed positions
-            for symbol in self.last_known_positions:
-                if symbol not in new_position_map:
-                    logger.info(f"📉 Position closed (observed by MarketMonitor): {symbol}. TradingStats is the definitive source.")
-            
-            # Update tracking
-            self.last_known_positions = new_position_map
-            
-        except Exception as e:
-            logger.error(f"❌ Error updating position tracking: {e}")
-    
-    async def _process_disappeared_orders(self, disappeared_order_ids: set):
-        """Log and investigate bot orders that have disappeared from the exchange."""
-        stats = self.trading_engine.get_stats()
-        if not stats:
-            logger.warning("⚠️ TradingStats not available in _process_disappeared_orders.")
-            return
-
-        try:
-            total_linked_cancelled = 0
-            external_cancellations = []
-            
-            for exchange_oid in disappeared_order_ids:
-                order_in_db = stats.get_order_by_exchange_id(exchange_oid)
-                
-                if order_in_db:
-                    last_status = order_in_db.get('status', 'unknown')
-                    order_type = order_in_db.get('type', 'unknown')
-                    symbol = order_in_db.get('symbol', 'unknown')
-                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    
-                    logger.info(f"Order {exchange_oid} was in our DB with status '{last_status}' but has now disappeared from exchange.")
-                    
-                    # Check if this was an unexpected disappearance (likely external cancellation)
-                    active_statuses = ['open', 'submitted', 'partially_filled', 'pending_submission']
-                    if last_status in active_statuses:
-                        logger.warning(f"⚠️ EXTERNAL CANCELLATION: Order {exchange_oid} with status '{last_status}' was likely cancelled externally on Hyperliquid")
-                        stats.update_order_status(exchange_order_id=exchange_oid, new_status='cancelled_externally')
-                        
-                        # Track external cancellations for notification
-                        external_cancellations.append({
-                            'exchange_oid': exchange_oid,
-                            'token': token,
-                            'type': order_type,
-                            'last_status': last_status
-                        })
-                        
-                        # Send notification about external cancellation
-                        if self.notification_manager:
-                            await self.notification_manager.send_generic_notification(
-                                f"⚠️ <b>External Order Cancellation Detected</b>\n\n"
-                                f"Token: {token}\n"
-                                f"Order Type: {order_type.replace('_', ' ').title()}\n"
-                                f"Exchange Order ID: <code>{exchange_oid[:8]}...</code>\n"
-                                f"Previous Status: {last_status.replace('_', ' ').title()}\n"
-                                f"Source: Cancelled directly on Hyperliquid\n"
-                                f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                                f"🤖 Bot status updated automatically"
-                            )
-                            
-                        # If the order was externally cancelled, its status in DB is now 'cancelled_externally'.
-                        # We rely on TradingStats to correctly update order status if a fill came through.
-                        # If the status is still 'cancelled_externally' (or any other non-fill status), then proceed.
-                        order_after_external_check = stats.get_order_by_exchange_id(exchange_oid)
-                        if order_after_external_check and order_after_external_check.get('status') == 'cancelled_externally':
-                            # NEW: Check and cancel corresponding pending trade lifecycle
-                            pending_lc = stats.get_lifecycle_by_entry_order_id(exchange_oid, status='pending')
-                            if pending_lc:
-                                lc_id_to_cancel = pending_lc.get('trade_lifecycle_id')
-                                if lc_id_to_cancel:
-                                    cancel_reason = f"entry_order_{exchange_oid[:8]}_disappeared_externally"
-                                    cancelled_lc_success = stats.update_trade_cancelled(lc_id_to_cancel, reason=cancel_reason)
-                                    if cancelled_lc_success:
-                                        logger.info(f"🔗 Trade lifecycle {lc_id_to_cancel} also cancelled for disappeared entry order {exchange_oid}.")
-                                        if self.notification_manager:
-                                            await self.notification_manager.send_generic_notification(
-                                                f"🔗 <b>Trade Lifecycle Cancelled</b>\n\n"
-                                                f"Token: {token}\n"
-                                                f"Lifecycle ID: {lc_id_to_cancel[:8]}...\\n"
-                                                f"Reason: Entry order {exchange_oid[:8]}... cancelled externally.\\n"
-                                                f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                            )
-                                    else:
-                                        logger.error(f"❌ Failed to cancel trade lifecycle {lc_id_to_cancel} for entry order {exchange_oid}.")
-                            # Continue to check for linked SL orders regardless of lifecycle cancellation outcome for this parent order
-
-                        elif order_after_external_check and order_after_external_check.get('status') in ['filled', 'partially_filled']:
-                            logger.info(f"ℹ️ Order {exchange_oid} was ultimately found to be '{order_after_external_check.get('status')}' despite initial disappearance. Stop losses will not be cancelled by this path. Lifecycle should be active.")
-                            continue # Skip SL cancellation if the order is now considered filled/partially_filled
-
-                    else:
-                        # Normal completion/cancellation - update status
-                        # This path is for orders that disappeared but their last known status wasn't active.
-                        # Example: bot cancelled it, then it disappeared.
-                        # If stats hasn't already marked it (e.g. 'cancelled_manually'), we mark it disappeared.
-                        if last_status not in ['filled', 'partially_filled', 'cancelled_manually', 'cancelled_by_bot', 'failed_submission', 'cancelled_externally']:
-                            stats.update_order_status(exchange_order_id=exchange_oid, new_status='disappeared_from_exchange')
-                        
-                    # Cancel any pending stop losses linked to this order (only if not filled)
-                    # The Trade object lifecycle (managed by TradingStats) is the source of truth.
-                    # If the parent order (order_in_db) is NOT considered filled, then its SLs can be cancelled.
-                    if order_in_db.get('bot_order_ref_id'):
-                        # Use the most recent state of the order from the database.
-                        parent_order_current_state = stats.get_order_by_exchange_id(exchange_oid)
-
-                        if parent_order_current_state and parent_order_current_state.get('status') not in ['filled', 'partially_filled']:
-                            logger.info(f"Cancelling stop losses for order {exchange_oid} (status: {parent_order_current_state.get('status')}) as it is not considered filled.")
-                            cancelled_sl_count = stats.cancel_linked_orders(
-                                parent_bot_order_ref_id=order_in_db['bot_order_ref_id'],
-                                new_status='cancelled_parent_disappeared_or_not_filled' # More descriptive status
-                            )
-                            total_linked_cancelled += cancelled_sl_count
-                        
-                            if cancelled_sl_count > 0:
-                                logger.info(f"Cancelled {cancelled_sl_count} pending stop losses linked to disappeared/non-filled order {exchange_oid}")
-                            
-                                if self.notification_manager:
-                                    await self.notification_manager.send_generic_notification(
-                                        f"🛑 <b>Linked Stop Losses Cancelled</b>\n\n"
-                                        f"Token: {token}\n"
-                                        f"Cancelled: {cancelled_sl_count} stop loss(es)\n"
-                                        f"Reason: Parent order {exchange_oid[:8]}... disappeared or was not filled\n"
-                                        f"Parent Status: {parent_order_current_state.get('status', 'N/A').replace('_', ' ').title()}\n"
-                                        f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                    )
-                        else:
-                            logger.info(f"ℹ️ Stop losses for order {exchange_oid} (status: {parent_order_current_state.get('status') if parent_order_current_state else 'N/A'}) preserved as parent order is considered filled or partially filled.")
-                            continue # Explicitly continue to the next disappeared_order_id
-                else:
-                    logger.warning(f"Order {exchange_oid} disappeared from exchange but was not found in our DB. This might be an order placed externally.")
-
-            # Send summary notification if multiple external cancellations occurred
-            if len(external_cancellations) > 1:
-                tokens_affected = list(set(item['token'] for item in external_cancellations))
-                
-                if self.notification_manager:
-                    await self.notification_manager.send_generic_notification(
-                        f"⚠️ <b>Multiple External Cancellations Detected</b>\n\n"
-                        f"Orders Cancelled: {len(external_cancellations)}\n"
-                        f"Tokens Affected: {', '.join(tokens_affected)}\n"
-                        f"Source: Direct cancellation on Hyperliquid\n"
-                        f"Linked Stop Losses Cancelled: {total_linked_cancelled}\n"
-                        f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                        f"💡 Check individual orders for details"
-                    )
-
-        except Exception as e:
-            logger.error(f"❌ Error processing disappeared orders: {e}", exc_info=True)
-    
-    async def _check_price_alarms(self):
-        """Check price alarms and trigger notifications."""
-        try:
-            active_alarms = self.alarm_manager.get_all_active_alarms()
-            
-            if not active_alarms:
-                return
-            
-            # Group alarms by token to minimize API calls
-            tokens_to_check = list(set(alarm['token'] for alarm in active_alarms))
-            
-            for token in tokens_to_check:
-                try:
-                    # Get current market price
-                    symbol = f"{token}/USDC:USDC"
-                    market_data = self.trading_engine.get_market_data(symbol)
-                    
-                    if not market_data or not market_data.get('ticker'):
-                        continue
-                    
-                    current_price = float(market_data['ticker'].get('last', 0))
-                    if current_price <= 0:
-                        continue
-                    
-                    # Check alarms for this token
-                    token_alarms = [alarm for alarm in active_alarms if alarm['token'] == token]
-                    
-                    for alarm in token_alarms:
-                        target_price = alarm['target_price']
-                        direction = alarm['direction']
-                        
-                        # Check if alarm should trigger
-                        should_trigger = False
-                        if direction == 'above' and current_price >= target_price:
-                            should_trigger = True
-                        elif direction == 'below' and current_price <= target_price:
-                            should_trigger = True
-                        
-                        if should_trigger:
-                            # Trigger the alarm
-                            triggered_alarm = self.alarm_manager.trigger_alarm(alarm['id'], current_price)
-                            if triggered_alarm:
-                                await self._send_alarm_notification(triggered_alarm)
-                
-                except Exception as e:
-                    logger.error(f"Error checking alarms for {token}: {e}")
-                    
-        except Exception as e:
-            logger.error(f"❌ Error checking price alarms: {e}")
-    
-    async def _send_alarm_notification(self, alarm: Dict[str, Any]):
-        """Send notification for triggered alarm."""
-        try:
-            # Send through notification manager if available
-            if self.notification_manager:
-                await self.notification_manager.send_alarm_triggered_notification(
-                    alarm['token'], 
-                    alarm['target_price'], 
-                    alarm['triggered_price'], 
-                    alarm['direction']
-                )
-            else:
-                # Fallback to logging if notification manager not available
-                logger.info(f"🔔 ALARM TRIGGERED: {alarm['token']} @ ${alarm['triggered_price']:,.2f}")
-            
-        except Exception as e:
-            logger.error(f"❌ Error sending alarm notification: {e}")
-    
-    async def _check_external_trades(self):
-        """Check for trades made outside the Telegram bot and update stats."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                logger.warning("TradingStats not available in _check_external_trades. Skipping.")
-                return
-
-            external_trades_processed = 0
-            symbols_with_fills = set() # Keep track of symbols with new fills for potential actions
-
-            # Get recent fills from exchange
-            recent_fills = self.trading_engine.get_recent_fills()
-            if not recent_fills:
-                logger.debug("No recent fills data available")
-                return
-
-            # Corrected to use self.last_processed_trade_time (no leading underscore)
-            if not hasattr(self, 'last_processed_trade_time') or self.last_processed_trade_time is None:
-                try:
-                    last_time_str = self.trading_engine.stats._get_metadata('last_processed_trade_time')
-                    if last_time_str:
-                        self.last_processed_trade_time = datetime.fromisoformat(last_time_str)
-                    else:
-                        # If no last processed time, start from 1 hour ago to avoid processing too much history
-                        self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
-                except Exception: # Fallback on error
-                     self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
-
-
-            for fill in recent_fills:
-                try:
-                    trade_id = fill.get('id')
-                    timestamp_ms = fill.get('timestamp')
-                    symbol_from_fill = fill.get('symbol')
-                    side_from_fill = fill.get('side')
-                    amount_from_fill = float(fill.get('amount', 0))
-                    price_from_fill = float(fill.get('price', 0))
-                    
-                    timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) if timestamp_ms else datetime.now(timezone.utc)
-                    
-                    if timestamp_dt <= self.last_processed_trade_time:
-                        continue
-                    
-                    fill_processed_this_iteration = False # Tracks if this specific fill is handled
-                    
-                    if not (symbol_from_fill and side_from_fill and amount_from_fill > 0 and price_from_fill > 0):
-                        logger.warning(f"Skipping fill with incomplete data: {fill}")
-                        continue
-
-                    full_symbol = symbol_from_fill
-                    token = symbol_from_fill.split('/')[0] if '/' in symbol_from_fill else symbol_from_fill.split(':')[0]
-                    # symbols_with_fills.add(token) # This was here, let's see if it should be moved or is used later
-
-                    exchange_order_id_from_fill = fill.get('info', {}).get('oid')
-
-                    # --- Lifecycle Processing ---
-
-                    # 1. Check if fill matches a PENDING trade lifecycle (entry fill)
-                    if exchange_order_id_from_fill:
-                        pending_lc = stats.get_lifecycle_by_entry_order_id(exchange_order_id_from_fill, status='pending')
-                        if pending_lc and pending_lc.get('symbol') == full_symbol:
-                            success = stats.update_trade_position_opened(
-                                lifecycle_id=pending_lc['trade_lifecycle_id'],
-                                entry_price=price_from_fill,
-                                entry_amount=amount_from_fill,
-                                exchange_fill_id=trade_id
-                            )
-                            if success:
-                                logger.info(f"📈 Lifecycle ENTRY: {pending_lc['trade_lifecycle_id']} for {full_symbol} updated by fill {trade_id}.")
-                                symbols_with_fills.add(token)
-                                order_in_db_for_entry = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
-                                if order_in_db_for_entry:
-                                    stats.update_order_status(order_db_id=order_in_db_for_entry['id'], new_status='filled', amount_filled_increment=amount_from_fill)
-                            fill_processed_this_iteration = True
-                    
-                    # 2. Check if fill matches an OPENED trade lifecycle (Bot Exit, SL/TP by OID, or External Close)
-                    if not fill_processed_this_iteration:
-                        active_lc = None
-                        closure_reason_action_type = None # e.g., "bot_exit_long_close", "sl_long_close", "external_long_close"
-                        bot_order_db_id_to_update = None # DB ID of the bot's order (exit, SL, TP) that this fill corresponds to
-
-                        if exchange_order_id_from_fill:
-                            # Attempt to link fill to a specific bot order (exit, SL, TP) first
-                            bot_order_for_fill = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
-
-                            if bot_order_for_fill and bot_order_for_fill.get('symbol') == full_symbol:
-                                order_type = bot_order_for_fill.get('type')
-                                order_side = bot_order_for_fill.get('side') # Side of the bot's order
-
-                                # A. Check if this fill corresponds to a bot-initiated EXIT order
-                                if order_type == 'market': # Common for /exit, add other explicit exit types if any
-                                    potential_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                                    if potential_lc:
-                                        lc_pos_side = potential_lc.get('position_side')
-                                        # Ensure bot's market order & this fill are closing the position
-                                        if (lc_pos_side == 'long' and order_side == 'sell' and side_from_fill == 'sell') or \
-                                           (lc_pos_side == 'short' and order_side == 'buy' and side_from_fill == 'buy'):
-                                            active_lc = potential_lc
-                                            closure_reason_action_type = f"bot_exit_{lc_pos_side}_close"
-                                            bot_order_db_id_to_update = bot_order_for_fill.get('id')
-                                            logger.info(f"ℹ️ Lifecycle BOT EXIT: Fill {trade_id} (OID {exchange_order_id_from_fill}) for {full_symbol} matches bot exit for lifecycle {active_lc['trade_lifecycle_id']}.")
-                                
-                                # B. If not a bot exit, check if it's an SL or TP order linked to a lifecycle
-                                if not active_lc: # Check only if not already matched as bot_exit
-                                    # Check SL by OID (exchange_order_id_from_fill should be the SL's actual exchange OID)
-                                    lc_by_sl = stats.get_lifecycle_by_sl_order_id(exchange_order_id_from_fill, status='position_opened')
-                                    if lc_by_sl and lc_by_sl.get('symbol') == full_symbol:
-                                        active_lc = lc_by_sl
-                                        closure_reason_action_type = f"sl_{active_lc.get('position_side')}_close"
-                                        # bot_order_for_fill is the SL order itself in this context
-                                        bot_order_db_id_to_update = bot_order_for_fill.get('id') 
-                                        logger.info(f"ℹ️ Lifecycle SL: Fill {trade_id} for OID {exchange_order_id_from_fill} matches SL for lifecycle {active_lc['trade_lifecycle_id']}.")
-                                    
-                                    if not active_lc: # Check TP only if not SL (and not bot exit)
-                                        lc_by_tp = stats.get_lifecycle_by_tp_order_id(exchange_order_id_from_fill, status='position_opened')
-                                        if lc_by_tp and lc_by_tp.get('symbol') == full_symbol:
-                                            active_lc = lc_by_tp
-                                            closure_reason_action_type = f"tp_{active_lc.get('position_side')}_close"
-                                            # bot_order_for_fill is the TP order itself here
-                                            bot_order_db_id_to_update = bot_order_for_fill.get('id') 
-                                            logger.info(f"ℹ️ Lifecycle TP: Fill {trade_id} for OID {exchange_order_id_from_fill} matches TP for lifecycle {active_lc['trade_lifecycle_id']}.")
-
-                        # C. If fill was not matched to a specific bot order by OID, check for generic external closure
-                        if not active_lc:
-                            potential_lc_external = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                            if potential_lc_external:
-                                lc_pos_side = potential_lc_external.get('position_side')
-                                # Ensure fill side is opposite to position side
-                                if (lc_pos_side == 'long' and side_from_fill == 'sell') or \
-                                   (lc_pos_side == 'short' and side_from_fill == 'buy'):
-                                    active_lc = potential_lc_external
-                                    closure_reason_action_type = f"external_{lc_pos_side}_close"
-                                    logger.info(f"ℹ️ Lifecycle EXTERNAL CLOSE: Fill {trade_id} for {full_symbol} (no matching bot OID) for lifecycle {active_lc['trade_lifecycle_id']}.")
-                        
-                        # If a lifecycle was identified for closure by any of the above means
-                        if active_lc and closure_reason_action_type:
-                            lc_id = active_lc['trade_lifecycle_id']
-                            lc_entry_price = active_lc.get('entry_price', 0)
-                            lc_position_side = active_lc.get('position_side') # From the identified active_lc
-
-                            realized_pnl = 0
-                            if lc_position_side == 'long':
-                                realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price)
-                            elif lc_position_side == 'short':
-                                realized_pnl = amount_from_fill * (lc_entry_price - price_from_fill)
-                            
-                            success = stats.update_trade_position_closed(
-                                lifecycle_id=lc_id, exit_price=price_from_fill,
-                                realized_pnl=realized_pnl, exchange_fill_id=trade_id
-                            )
-                            if success:
-                                pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                                formatter = get_formatter()
-                                logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({closure_reason_action_type}). PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
-                                symbols_with_fills.add(token)
-                                if self.notification_manager:
-                                    await self.notification_manager.send_external_trade_notification(
-                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
-                                        closure_reason_action_type, timestamp_dt.isoformat()
-                                    )
-                                stats._migrate_trade_to_aggregated_stats(lc_id)
-                                if bot_order_db_id_to_update:
-                                    stats.update_order_status(order_db_id=bot_order_db_id_to_update, new_status='filled', amount_filled_increment=amount_from_fill)
-                                fill_processed_this_iteration = True
-
-                    # 3. Handle external stop loss executions (MarketMonitor's separate tracking)
-                    if not fill_processed_this_iteration:
-                        if exchange_order_id_from_fill and exchange_order_id_from_fill in self.external_stop_losses:
-                            stop_loss_info = self.external_stop_losses[exchange_order_id_from_fill]
-                            formatter = get_formatter()
-                            logger.info(f"🛑 External SL (MM Tracking): {token} Order {exchange_order_id_from_fill} filled @ {formatter.format_price_with_symbol(price_from_fill, token)}")
-                            
-                            sl_active_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                            if sl_active_lc:
-                                lc_id = sl_active_lc['trade_lifecycle_id']
-                                lc_entry_price = sl_active_lc.get('entry_price', 0)
-                                lc_pos_side = sl_active_lc.get('position_side')
-                                realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price) if lc_pos_side == 'long' else amount_from_fill * (lc_entry_price - price_from_fill)
-                                
-                                success = stats.update_trade_position_closed(lc_id, price_from_fill, realized_pnl, trade_id)
-                                if success:
-                                    pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                                    logger.info(f"{pnl_emoji} Lifecycle CLOSED by External SL (MM): {lc_id}. PNL: {formatter.format_price_with_symbol(realized_pnl)}")
-                                    if self.notification_manager:
-                                        await self.notification_manager.send_stop_loss_execution_notification(
-                                            stop_loss_info, full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
-                                            f'{lc_pos_side}_closed_external_sl', timestamp_dt.isoformat(), realized_pnl
-                                        )
-                                    # MIGRATE STATS
-                                    stats._migrate_trade_to_aggregated_stats(lc_id)
-                                    del self.external_stop_losses[exchange_order_id_from_fill]
-                                    fill_processed_this_iteration = True
-                            else:
-                                logger.warning(f"⚠️ External SL (MM) {exchange_order_id_from_fill} for {full_symbol}, but no active lifecycle found.")
-                    
-                    # --- Fallback for Fills Not Handled by Lifecycle Logic Above ---
-                    if not fill_processed_this_iteration:
-                        # NEW: Attempt to match this fill to close an existing open position
-                        # This handles cases where an order disappeared from DB, then its fill is processed as external
-                        existing_open_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        if existing_open_lc:
-                            lc_id = existing_open_lc['trade_lifecycle_id']
-                            lc_entry_price = existing_open_lc.get('entry_price', 0)
-                            lc_position_side = existing_open_lc.get('position_side')
-                            lc_current_size_before_fill = existing_open_lc.get('current_position_size', 0) # Size before this fill
-
-                            is_potentially_closing_external_fill = False
-                            if lc_position_side == 'long' and side_from_fill.lower() == 'sell':
-                                is_potentially_closing_external_fill = True
-                            elif lc_position_side == 'short' and side_from_fill.lower() == 'buy':
-                                is_potentially_closing_external_fill = True
-                            
-                            if is_potentially_closing_external_fill:
-                                logger.info(f"ℹ️ Detected potentially closing external fill {trade_id} for {full_symbol} (Lifecycle: {lc_id}). Verifying exchange position state...")
-                                
-                                # Fetch fresh position data from the exchange to confirm closure
-                                fresh_positions_after_fill = self.trading_engine.get_positions() or []
-                                position_on_exchange_after_fill = None
-                                for pos in fresh_positions_after_fill:
-                                    if pos.get('symbol') == full_symbol:
-                                        position_on_exchange_after_fill = pos
-                                        break
-                                
-                                position_is_closed_on_exchange = False
-                                if position_on_exchange_after_fill is None:
-                                    position_is_closed_on_exchange = True
-                                    logger.info(f"✅ Exchange Verification: Position for {full_symbol} (Lifecycle: {lc_id}) not found after fill {trade_id}. Confirming closure.")
-                                elif abs(float(position_on_exchange_after_fill.get('contracts', 0))) < 1e-9: # Using a small tolerance for float comparison to zero
-                                    position_is_closed_on_exchange = True
-                                    logger.info(f"✅ Exchange Verification: Position for {full_symbol} (Lifecycle: {lc_id}) has zero size on exchange after fill {trade_id}. Confirming closure.")
-
-                                if position_is_closed_on_exchange:
-                                    # Position is confirmed closed on the exchange.
-                                    # P&L should be calculated based on the size that was closed by this fill,
-                                    # which we assume is lc_current_size_before_fill if the position is now entirely gone.
-                                    # If the fill amount (amount_from_fill) is less than lc_current_size_before_fill
-                                    # but the position is still gone, it implies other fills might have also occurred.
-                                    # For simplicity here, we use amount_from_fill for P&L calculation relating to this specific fill,
-                                    # assuming it's the one that effectively zeroed out the position or was the last part of it.
-                                    # A more robust P&L would use lc_current_size_before_fill if that was the true amount closed.
-                                    # Let's use lc_current_size_before_fill if the fill amount is very close to it, otherwise amount_from_fill.
-                                    
-                                    amount_for_pnl_calc = amount_from_fill
-                                    # If the position is fully closed, and this fill's amount is very close to the total size,
-                                    # assume this fill closed the entire remaining position.
-                                    if abs(lc_current_size_before_fill - amount_from_fill) < 0.000001 * amount_from_fill:
-                                         amount_for_pnl_calc = lc_current_size_before_fill
-
-
-                                    logger.info(f"ℹ️ Attempting to close lifecycle {lc_id} for {full_symbol} via confirmed external fill {trade_id}.")
-                                    realized_pnl = 0
-                                    if lc_position_side == 'long':
-                                        realized_pnl = amount_for_pnl_calc * (price_from_fill - lc_entry_price)
-                                    elif lc_position_side == 'short':
-                                        realized_pnl = amount_for_pnl_calc * (lc_entry_price - price_from_fill)
-                                    
-                                    success = stats.update_trade_position_closed(
-                                        lifecycle_id=lc_id, 
-                                        exit_price=price_from_fill, # Price of this specific fill
-                                        realized_pnl=realized_pnl, 
-                                        exchange_fill_id=trade_id
-                                    )
-                                    if success:
-                                        pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                                        formatter = get_formatter()
-                                        logger.info(f"{pnl_emoji} Lifecycle CLOSED (Verified External): {lc_id}. PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
-                                        symbols_with_fills.add(token)
-                                        if self.notification_manager:
-                                            await self.notification_manager.send_external_trade_notification(
-                                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                                f"verified_external_{lc_position_side}_close",
-                                                timestamp_dt.isoformat()
-                                            )
-                                        # MIGRATE STATS
-                                        stats._migrate_trade_to_aggregated_stats(lc_id)
-                                        fill_processed_this_iteration = True
-                                    else:
-                                        logger.error(f"❌ Failed to close lifecycle {lc_id} via verified external fill {trade_id}.")
-                                else:
-                                    # Position still exists on exchange, so this fill was not a full closure.
-                                    # Do not mark fill_processed_this_iteration = True here.
-                                    # Let the original fallback `stats.record_trade` handle this fill as "external_unmatched".
-                                    # This is important so the fill is recorded, even if it does not close the lifecycle.
-                                    current_size_on_exchange = float(position_on_exchange_after_fill.get('contracts', 0)) if position_on_exchange_after_fill else 'Unknown'
-                                    logger.warning(f"⚠️ External fill {trade_id} for {full_symbol} (Lifecycle: {lc_id}, Amount: {amount_from_fill}) did NOT fully close position. Exchange size now: {current_size_on_exchange}. Lifecycle remains open. Fill will be recorded as 'external_unmatched'.")
-                                    # Future enhancement: Handle partial closure here by updating current_position_size and realizing partial P&L.
-
-                    # Original Fallback logic if still not processed
-                    if not fill_processed_this_iteration:
-                        # ---- START DIAGNOSTIC BLOCK ----
-                        # Log details if this fill *should* have matched an open lifecycle but didn't.
-                        # This condition checks if a position count recently changed, suggesting a closure.
-                        # We access _update_cached_data's effect by checking self.last_known_positions vs current from cache.
-                        # Note: This is an approximation. A more robust check might involve tracking position count changes more directly.
-                        
-                        # Get current positions from cache (reflects state after _update_cached_data this cycle)
-                        current_positions_from_cache_map = {
-                            pos.get('symbol'): pos for pos in (self.cached_positions or [])
-                            if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
-                        }
-                        # Get DB's view of open positions
-                        all_open_positions_in_db = stats.get_open_positions() # Fetches all with status='position_opened'
-                        db_open_symbols = {pos_db.get('symbol') for pos_db in all_open_positions_in_db}
-
-                        # Check if the fill's symbol *should* have been in the DB as open,
-                        # but wasn't found by get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        if full_symbol in db_open_symbols:
-                            # This is the critical contradiction: DB says it's open via get_open_positions,
-                            # but get_trade_by_symbol_and_status(full_symbol, ...) failed.
-                            # This should ideally not happen if queries are consistent.
-                            logger.error(f"🚨 DIAGNOSTIC: Contradiction for {full_symbol}! get_open_positions() includes it, but get_trade_by_symbol_and_status('{full_symbol}', 'position_opened') failed to find it within _check_external_trades context for fill {trade_id}. This needs investigation into TradingStats symbol querying.")
-                        
-                        # More general diagnostic: if a position disappeared from exchange but we couldn't match this fill
-                        # This is a heuristic. A position count change was logged by _update_cached_data.
-                        # If len(current_positions_from_cache_map) < len(self.last_known_positions_before_update_this_cycle) 
-                        # (hypothetical variable, self.last_known_positions is already updated)
-                        # For now, we just log if get_trade_by_symbol_and_status failed.
-                        
-                        potential_match_failure_logged = False
-                        if not stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened'): # Re-check to be sure
-                            logger.warning(f"⚠️ DIAGNOSTIC for UNMATCHED FILL {trade_id} ({full_symbol}):")
-                            logger.warning(f"   Fill details: Side={side_from_fill}, Amount={amount_from_fill}, Price={price_from_fill}")
-                            logger.warning(f"   Attempted lookup with full_symbol='{full_symbol}' and status='position_opened' found NO active lifecycle.")
-                            if all_open_positions_in_db:
-                                logger.warning(f"   However, DB currently has these 'position_opened' lifecycles (symbol - lifecycle_id):")
-                                for db_pos in all_open_positions_in_db:
-                                    logger.warning(f"     - '{db_pos.get('symbol')}' - ID: {db_pos.get('trade_lifecycle_id')}")
-                                # Check for near matches (e.g. base token match)
-                                base_token_fill = full_symbol.split('/')[0].split(':')[0]
-                                near_matches = [db_s for db_s in db_open_symbols if base_token_fill in db_s]
-                                if near_matches:
-                                    logger.warning(f"   Possible near matches in DB for base token '{base_token_fill}': {near_matches}")
-                                else:
-                                    logger.warning(f"   No near matches found in DB for base token '{base_token_fill}'.")
-                            else:
-                                logger.warning("   DB has NO 'position_opened' lifecycles at all right now.")
-                            potential_match_failure_logged = True
-                        # ---- END DIAGNOSTIC BLOCK ----
-                        
-                        linked_order_db_id = None
-                        if exchange_order_id_from_fill:
-                            order_in_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
-                            if order_in_db:
-                                linked_order_db_id = order_in_db.get('id')
-                                logger.info(f"🔗 Fallback: Fill {trade_id} for OID {exchange_order_id_from_fill} (DB ID {linked_order_db_id}) not tied to active lifecycle step.")
-                                current_status = order_in_db.get('status', '')
-                                if current_status in ['open', 'partially_filled', 'pending_submission']:
-                                    amt_req = float(order_in_db.get('amount_requested', 0))
-                                    amt_filled_so_far = float(order_in_db.get('amount_filled',0))
-                                    new_status = 'partially_filled'
-                                    if (amt_filled_so_far + amount_from_fill) >= amt_req - 1e-9:
-                                        new_status = 'filled'
-                                    stats.update_order_status(
-                                        order_db_id=linked_order_db_id, new_status=new_status,
-                                        amount_filled_increment=amount_from_fill
-                                    )
-                                    logger.info(f"📊 Updated bot order {linked_order_db_id} (fallback): {current_status} → {new_status}")
-                        
-                        if not (hasattr(stats, 'get_trade_by_exchange_fill_id') and stats.get_trade_by_exchange_fill_id(trade_id)):
-                            stats.record_trade( # Old record_trade for truly unassociated fills
-                                full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
-                                exchange_fill_id=trade_id, trade_type="external_unmatched",
-                                timestamp=timestamp_dt.isoformat(),
-                                linked_order_table_id_to_link=linked_order_db_id 
-                            )
-                            logger.info(f"📋 Recorded trade via FALLBACK: {trade_id} (Unmatched External Fill)")
-                        fill_processed_this_iteration = True # Fallback is also a form of processing for this fill
-
-                    # Update timestamp if any processing occurred for this fill
-                    if fill_processed_this_iteration:
-                        external_trades_processed += 1
-                        if self.last_processed_trade_time is None or timestamp_dt > self.last_processed_trade_time:
-                           self.last_processed_trade_time = timestamp_dt
-                        
-                except Exception as e:
-                    logger.error(f"Error processing fill {fill.get('id', 'N/A')}: {e}", exc_info=True)
-                    continue # Important to continue to next fill
-            
-            # Save the last processed timestamp to database
-            if external_trades_processed > 0:
-                # self.trading_engine.stats._set_metadata('last_processed_trade_time', self.last_processed_trade_time.isoformat()) # Corrected variable
-                stats._set_metadata('market_monitor_last_processed_trade_time', self.last_processed_trade_time.isoformat())
-                logger.info(f"💾 Saved MarketMonitor state (last_processed_trade_time) to DB: {self.last_processed_trade_time.isoformat()}")
-                logger.info(f"📊 Processed {external_trades_processed} external trades")
-                if symbols_with_fills: # Check if set is not empty
-                    logger.info(f"ℹ️ Symbols with processed fills this cycle: {list(symbols_with_fills)}")
-                
-        except Exception as e:
-            logger.error(f"❌ Error checking external trades: {e}", exc_info=True)
-
-    async def _check_pending_triggers(self):
-        """Check and process pending conditional triggers (e.g., SL/TP)."""
-        stats = self.trading_engine.get_stats()
-        if not stats:
-            logger.warning("⚠️ TradingStats not available in _check_pending_triggers.")
-            return
-
-        try:
-            # Fetch pending SL triggers (adjust type if TP triggers are different)
-            # For now, assuming 'STOP_LIMIT_TRIGGER' is the type used for SLs that become limit orders
-            pending_sl_triggers = stats.get_orders_by_status(status='pending_trigger', order_type_filter='stop_limit_trigger')
-            
-            if not pending_sl_triggers:
-                return
-
-            logger.debug(f"Found {len(pending_sl_triggers)} pending SL triggers to check.")
-
-            for trigger_order in pending_sl_triggers:
-                symbol = trigger_order['symbol']
-                trigger_price = trigger_order['price'] # This is the stop price
-                trigger_side = trigger_order['side'] # This is the side of the SL order (e.g., sell for a long position's SL)
-                order_db_id = trigger_order['id']
-                parent_ref_id = trigger_order.get('parent_bot_order_ref_id')
-
-                if not symbol or trigger_price is None:
-                    logger.warning(f"Invalid trigger order data for DB ID {order_db_id}, skipping: {trigger_order}")
-                    continue
-
-                market_data = self.trading_engine.get_market_data(symbol)
-                if not market_data or not market_data.get('ticker'):
-                    logger.warning(f"Could not fetch market data for {symbol} to check SL trigger {order_db_id}.")
-                    continue
-                
-                current_price = float(market_data['ticker'].get('last', 0))
-                if current_price <= 0:
-                    logger.warning(f"Invalid current price ({current_price}) for {symbol} checking SL trigger {order_db_id}.")
-                    continue
-
-                trigger_hit = False
-                if trigger_side.lower() == 'sell' and current_price <= trigger_price:
-                    trigger_hit = True
-                    logger.info(f"🔴 SL TRIGGER HIT (Sell): Order DB ID {order_db_id}, Symbol {symbol}, Trigger@ ${trigger_price:.4f}, Market@ ${current_price:.4f}")
-                elif trigger_side.lower() == 'buy' and current_price >= trigger_price:
-                    trigger_hit = True
-                    logger.info(f"🟢 SL TRIGGER HIT (Buy): Order DB ID {order_db_id}, Symbol {symbol}, Trigger@ ${trigger_price:.4f}, Market@ ${current_price:.4f}")
-                
-                if trigger_hit:
-                    logger.info(f"Attempting to execute actual stop order for triggered DB ID: {order_db_id} (Parent Bot Ref: {trigger_order.get('parent_bot_order_ref_id')})")
-                    
-                    execution_result = await self.trading_engine.execute_triggered_stop_order(original_trigger_order_db_id=order_db_id)
-                    
-                    notification_message_detail = ""
-
-                    if execution_result.get("success"):
-                        new_trigger_status = 'triggered_order_placed'
-                        placed_sl_details = execution_result.get("placed_sl_order_details", {})
-                        logger.info(f"Successfully placed actual SL order from trigger {order_db_id}. New SL Order DB ID: {placed_sl_details.get('order_db_id')}, Exchange ID: {placed_sl_details.get('exchange_order_id')}")
-                        notification_message_detail = f"Actual SL order placed (New DB ID: {placed_sl_details.get('order_db_id', 'N/A')})."
-                    else:
-                        new_trigger_status = 'trigger_execution_failed'
-                        error_msg = execution_result.get("error", "Unknown error during SL execution.")
-                        logger.error(f"Failed to execute actual SL order from trigger {order_db_id}: {error_msg}")
-                        notification_message_detail = f"Failed to place actual SL order: {error_msg}"
+                            logger.debug(f"No lifecycle_id for open position {symbol} in _update_cached_data, skipping detailed stats update.")
+                            continue # Skip if no lifecycle ID, though this should ideally not happen for an open position
 
 
-                    stats.update_order_status(order_db_id=order_db_id, new_status=new_trigger_status)
-                    
-                    if self.notification_manager:
-                        await self.notification_manager.send_generic_notification(
-                            f"🔔 Stop-Loss Update!\nSymbol: {symbol}\nSide: {trigger_side.upper()}\nTrigger Price: ${trigger_price:.4f}\nMarket Price: ${current_price:.4f}\n(Original Trigger DB ID: {order_db_id}, Parent: {parent_ref_id or 'N/A'})\nStatus: {new_trigger_status.replace('_', ' ').title()}\nDetails: {notification_message_detail}"
-                        )
-
-        except Exception as e:
-            logger.error(f"❌ Error checking pending SL triggers: {e}", exc_info=True)
-
-    async def _check_automatic_risk_management(self):
-        """Check for automatic stop loss triggers based on Config.STOP_LOSS_PERCENTAGE as safety net."""
-        try:
-            # Skip if risk management is disabled or percentage is 0
-            if not getattr(Config, 'RISK_MANAGEMENT_ENABLED', True) or Config.STOP_LOSS_PERCENTAGE <= 0:
-                return
-
-            # Get current positions
-            positions = self.cached_positions or [] # Use cache
-            if not positions:
-                # If no positions exist, clean up any orphaned pending stop losses
-                await self._cleanup_orphaned_stop_losses()
-                return
-
-            for position in positions:
-                try:
-                    symbol = position.get('symbol', '')
-                    contracts = float(position.get('contracts', 0))
-                    entry_price = float(position.get('entryPx', 0))
-                    mark_price = float(position.get('markPx', 0))
-                    unrealized_pnl = float(position.get('unrealizedPnl', 0))
-                    
-                    # Skip if no position or missing data
-                    if contracts == 0 or entry_price <= 0 or mark_price <= 0:
-                        continue
-
-                    # Calculate PnL percentage based on entry value
-                    entry_value = abs(contracts) * entry_price
-                    if entry_value <= 0:
-                        continue
-                        
-                    pnl_percentage = (unrealized_pnl / entry_value) * 100
-
-                    # Check if loss exceeds the safety threshold
-                    if pnl_percentage <= -Config.STOP_LOSS_PERCENTAGE:
-                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                        position_side = "LONG" if contracts > 0 else "SHORT"
-                        
-                        # Fetch the active trade lifecycle for logging context
-                        stats = self.trading_engine.get_stats()
-                        lifecycle_id_str = "N/A"
-                        if stats:
-                            active_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
-                            if active_trade_lc:
-                                lifecycle_id_str = active_trade_lc.get('trade_lifecycle_id', "N/A")[:8] + "..."
-
-                        logger.warning(f"🚨 AUTOMATIC STOP LOSS TRIGGERED: {token} {position_side} position (Lifecycle: {lifecycle_id_str}) has {pnl_percentage:.2f}% loss (threshold: -{Config.STOP_LOSS_PERCENTAGE}%)")
-                        
-                        # Send notification before attempting exit
-                        if self.notification_manager:
-                            await self.notification_manager.send_generic_notification(
-                                f"🚨 AUTOMATIC STOP LOSS TRIGGERED!\\n"
-                                f"Token: {token}\\n"
-                                f"Lifecycle ID: {lifecycle_id_str}\\n"
-                                f"Position: {position_side} {abs(contracts):.6f}\\n"
-                                f"Entry Price: ${entry_price:.4f}\\n"
-                                f"Current Price: ${mark_price:.4f}\\n"
-                                f"Unrealized P&L: ${unrealized_pnl:.2f} ({pnl_percentage:.2f}%)\\n"
-                                f"Safety Threshold: -{Config.STOP_LOSS_PERCENTAGE}%\\n"
-                                f"Action: Executing emergency exit order..."
-                            )
-
-                        # Execute emergency exit order
-                        exit_result = await self.trading_engine.execute_exit_order(token)
-                        
-                        if exit_result.get('success'):
-                            placed_order_details = exit_result.get('order_placed_details', {})
-                            logger.info(f"✅ Emergency exit order placed for {token} (Lifecycle: {lifecycle_id_str}). Order details: {placed_order_details}")
-                            
-                            # Cancel any pending stop losses for this symbol since position is now closed
-                            # stats object is already fetched above
-                            if stats:
-                                cancelled_sl_count = stats.cancel_pending_stop_losses_by_symbol(
-                                    symbol=symbol,
-                                    new_status='cancelled_auto_exit'
-                                )
-                                if cancelled_sl_count > 0:
-                                    logger.info(f"🛑 Cancelled {cancelled_sl_count} pending stop losses for {symbol} (Lifecycle: {lifecycle_id_str}) after automatic exit")
-                            
-                            if self.notification_manager:
-                                await self.notification_manager.send_generic_notification(
-                                    f"✅ <b>Emergency Exit Initiated</b>\\n\\n"
-                                    f"📊 <b>Position:</b> {token} {position_side}\\n"
-                                    f"🆔 <b>Lifecycle ID:</b> {lifecycle_id_str}\\n"
-                                    f"📉 <b>Loss at Trigger:</b> {pnl_percentage:.2f}% (${unrealized_pnl:.2f})\\n"
-                                    f"⚠️ <b>Threshold:</b> -{Config.STOP_LOSS_PERCENTAGE}%\\n"
-                                    f"✅ <b>Action:</b> Market exit order placed successfully\\n"
-                                    f"🆔 <b>Exit Order ID:</b> {placed_order_details.get('exchange_order_id', 'N/A')}\\n"
-                                    f"{f'🛑 <b>Cleanup:</b> Cancelled {cancelled_sl_count} other pending stop losses' if cancelled_sl_count > 0 else ''}\\n\\n"
-                                    f"🛡️ The system will confirm closure and P&L once the exit order fill is processed."
-                                )
-                        else:
-                            error_msg = exit_result.get('error', 'Unknown error')
-                            logger.error(f"❌ Failed to execute emergency exit order for {token} (Lifecycle: {lifecycle_id_str}): {error_msg}")
-                            
-                            if self.notification_manager:
-                                await self.notification_manager.send_generic_notification(
-                                    f"❌ <b>CRITICAL: Emergency Exit Failed!</b>\\n\\n"
-                                    f"📊 <b>Position:</b> {token} {position_side}\\n"
-                                    f"🆔 <b>Lifecycle ID:</b> {lifecycle_id_str}\\n"
-                                    f"📉 <b>Loss:</b> {pnl_percentage:.2f}%\\n"
-                                    f"❌ <b>Error Placing Order:</b> {error_msg}\\n\\n"
-                                    f"⚠️ <b>MANUAL INTERVENTION REQUIRED</b>\\n"
-                                    f"Please close this position manually via /exit {token}"
-                                )
-
-                except Exception as pos_error:
-                    logger.error(f"Error processing position for automatic stop loss: {pos_error}")
-                    continue
-
-        except Exception as e:
-            logger.error(f"❌ Error in automatic risk management check: {e}", exc_info=True)
-
-    async def _cleanup_orphaned_stop_losses(self):
-        """Clean up pending stop losses that no longer have corresponding positions OR whose parent orders have been cancelled/failed."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                return
-
-            pending_stop_losses = stats.get_orders_by_status('pending_trigger', 'stop_limit_trigger')
-            
-            if not pending_stop_losses:
-                return
-
-            logger.debug(f"Checking {len(pending_stop_losses)} pending stop losses for orphaned orders")
-
-            current_positions = self.cached_positions or [] 
-            position_symbols = set()
-            
-            if current_positions:
-                for pos in current_positions:
-                    symbol = pos.get('symbol')
-                    contracts = float(pos.get('contracts', 0))
-                    if symbol and contracts != 0:
-                        position_symbols.add(symbol)
-
-            orphaned_count = 0
-            for sl_order in pending_stop_losses:
-                symbol = sl_order.get('symbol')
-                order_db_id = sl_order.get('id')
-                parent_bot_ref_id = sl_order.get('parent_bot_order_ref_id')
-                
-                should_cancel = False
-                cancel_reason = ""
-                
-                if parent_bot_ref_id:
-                    parent_order = stats.get_order_by_bot_ref_id(parent_bot_ref_id)
-                    if parent_order:
-                        parent_status = parent_order.get('status', '').lower()
-                        
-                        if parent_order.get('exchange_order_id'):
-                            entry_oid = parent_order['exchange_order_id']
-                            lc_pending = stats.get_lifecycle_by_entry_order_id(entry_oid, status='pending')
-                            lc_cancelled = stats.get_lifecycle_by_entry_order_id(entry_oid, status='cancelled')
-                            lc_opened = stats.get_lifecycle_by_entry_order_id(entry_oid, status='position_opened')
-
-                            if parent_status == 'cancelled_externally':
-                                if lc_cancelled:
-                                    should_cancel = True
-                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and lifecycle explicitly cancelled"
-                                elif not lc_pending and not lc_opened:
-                                    should_cancel = True
-                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no active/pending/cancelled lifecycle found"
-                                else:
-                                    current_lc_status = "N/A"
-                                    if lc_pending: current_lc_status = lc_pending.get('status')
-                                    elif lc_opened: current_lc_status = lc_opened.get('status')
-                                    logger.info(f"SL {order_db_id} for parent {parent_bot_ref_id} (status {parent_status}) - lifecycle is '{current_lc_status}'. SL not cancelled by this rule.")
-                                    should_cancel = False
-                            
-                            elif parent_status in ['failed_submission', 'failed_submission_no_data', 'cancelled_manually', 'disappeared_from_exchange']:
-                                if not lc_opened: 
-                                    should_cancel = True
-                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no 'position_opened' lifecycle"
-                                else:
-                                    logger.info(f"SL {order_db_id} for parent {parent_bot_ref_id} (status {parent_status}) - lifecycle is 'position_opened'. SL not cancelled by this rule.")
-                                    should_cancel = False
-                            elif parent_status == 'filled':
-                                if symbol not in position_symbols: 
-                                    should_cancel = True
-                                    cancel_reason = "parent filled but actual position no longer exists"
-                            # If parent_status is 'open' and has exchange_order_id, it falls through, should_cancel remains False (correct).
-                            
-                        else: # Parent order does not have an exchange_order_id in the fetched DB record
-                            # Possible states for parent_status here: 'pending_submission', 'open' (anomalous), or other non-live states.
-                            if parent_status in ['open', 'pending_submission', 'submitted']: # 'submitted' can be another pre-fill active state
-                                logger.info(f"SL Cleanup: Parent order {parent_order.get('id')} (status '{parent_status}') is missing exchange_id in DB record. SL {sl_order.get('id')} will NOT be cancelled by this rule, assuming parent is still active or pending.")
-                                should_cancel = False # Preserve SL if parent is in an active or attempting-to-be-active state
-                                if parent_status == 'open': # This specific case is a data anomaly
-                                    logger.warning(f"SL Cleanup: DATA ANOMALY - Parent order {parent_order.get('id')} status is 'open' but fetched DB record shows no exchange_id. Investigate DB state for order {parent_order.get('id')}.")
-                            else:
-                                # Parent is in a non-live/non-pending status (e.g., 'failed_submission' before getting an ID)
-                                # and never got an exchange_id. It's likely safe to cancel the SL.
-                                should_cancel = True
-                                cancel_reason = f"parent order {parent_status} (no exch_id) and status indicates it's not live/pending."
-                    else: # Parent order not found in DB by bot_order_ref_id
-                        should_cancel = True
-                        cancel_reason = "parent order not found in database"
-                else:
-                    # No parent_bot_ref_id on the SL order itself.
-                    # This SL is not tied to a bot order, so cancel if no position for the symbol.
-                    if symbol not in position_symbols:
-                        should_cancel = True
-                        cancel_reason = "no position exists and no parent reference"
-                
-                if should_cancel:
-                    success = stats.update_order_status(
-                        order_db_id=order_db_id,
-                        new_status='cancelled_orphaned'
-                    )
-                    
-                    if success:
-                        orphaned_count += 1
-                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                        logger.info(f"🧹 Cancelled orphaned stop loss for {token} (Order DB ID: {order_db_id}) - Reason: {cancel_reason}")
-
-            if orphaned_count > 0:
-                logger.info(f"🧹 Cleanup completed: Cancelled {orphaned_count} orphaned stop loss order(s)")
-                
-                if self.notification_manager:
-                    await self.notification_manager.send_generic_notification(
-                        f"🧹 <b>Cleanup Completed</b>\n\n"
-                        f"Cancelled {orphaned_count} orphaned stop loss order(s)\n"
-                        f"Reason: Parent orders invalid or positions closed externally\n"
-                        f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                        f"💡 This ensures stop losses sync with actual orders/positions."
-                    )
-
-        except Exception as e:
-            logger.error(f"❌ Error cleaning up orphaned stop losses: {e}", exc_info=True)
-
-
-    async def _activate_pending_stop_losses_from_trades(self):
-        """🆕 PHASE 4: Check trades table for pending stop loss activation first (highest priority)"""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                return
-            
-            formatter = get_formatter() # Get formatter
-            trades_needing_sl = stats.get_pending_stop_loss_activations()
-            
-            if not trades_needing_sl:
-                return
-            
-            logger.debug(f"🆕 Found {len(trades_needing_sl)} open positions needing stop loss activation")
-            
-            for position_trade in trades_needing_sl:
-                try:
-                    symbol = position_trade['symbol']
-                    # Ensure token is derived correctly for formatter, handling potential None symbol early
-                    token = symbol.split('/')[0] if symbol and '/' in symbol else (symbol if symbol else "TOKEN")
-                    stop_loss_price = position_trade['stop_loss_price']
-                    position_side = position_trade['position_side'] # 'long' or 'short'
-                    current_amount = position_trade.get('current_position_size', 0)
-                    lifecycle_id = position_trade['trade_lifecycle_id']
-
-                    if not all([symbol, stop_loss_price, position_side, abs(current_amount) > 1e-9, lifecycle_id]):
-                        logger.warning(f"Skipping SL activation for lifecycle {lifecycle_id} due to incomplete data: sym={symbol}, sl_price={stop_loss_price}, side={position_side}, amt={current_amount}")
-                        continue
-                    
-                    # MODIFICATION: Remove immediate market execution logic.
-                    # Always proceed to place the limit stop-loss order via the new engine method.
-                    # current_price = None
-                    # try:
-                    #     market_data = self.trading_engine.get_market_data(symbol)
-                    #     if market_data and market_data.get('ticker'):
-                    #         current_price = float(market_data['ticker'].get('last', 0))
-                    # except Exception as price_error:
-                    #     logger.warning(f"Could not fetch current price for {symbol} for SL activation of {lifecycle_id}: {price_error}")
-                    
-                    # sl_side = 'sell' if position_side == 'long' else 'buy'
-                    # trigger_already_hit = False
-                    # trigger_reason = ""
-                    # if current_price and current_price > 0 and stop_loss_price and stop_loss_price > 0:
-                    #     # ... (old trigger_already_hit logic removed)
-                    
-                    # if trigger_already_hit:
-                    #     # ... (old immediate market exit logic removed)
-                    # else:
-                    # Normal activation - place SL limit order using the new engine method
-                    
-                    logger.info(f"Attempting to place LIMIT stop loss for lifecycle {lifecycle_id} ({position_side} {token} @ SL {formatter.format_price(stop_loss_price, symbol)})")
-                    sl_result = await self.trading_engine.place_limit_stop_for_lifecycle(
-                        lifecycle_id=lifecycle_id,
-                        symbol=symbol,
-                        sl_price=stop_loss_price,
-                        position_side=position_side,
-                        amount_to_cover=abs(current_amount) # Ensure positive amount
-                    )
-                        
-                    if sl_result.get('success'):
-                        placed_sl_details = sl_result.get('order_placed_details', {})
-                        sl_exchange_order_id = placed_sl_details.get('exchange_order_id')
-                        sl_db_order_id = placed_sl_details.get('order_db_id')
-                        stop_loss_price_str_log = formatter.format_price_with_symbol(stop_loss_price, token)
-
-                        logger.info(f"✅ Successfully processed SL request for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price {stop_loss_price_str_log}, Exchange SL Order ID: {sl_exchange_order_id or 'N/A'}, DB ID: {sl_db_order_id or 'N/A'}")
-                        
-                        if self.notification_manager and sl_exchange_order_id:
-                            # Fetch current price for notification context if possible
-                            current_price_for_notification = None
-                            try:
-                                market_data_notify = self.trading_engine.get_market_data(symbol)
-                                if market_data_notify and market_data_notify.get('ticker'):
-                                    current_price_for_notification = float(market_data_notify['ticker'].get('last', 0))
-                            except:
-                                pass # Ignore errors fetching price for notification
-
-                            current_price_str_notify = formatter.format_price_with_symbol(current_price_for_notification, token) if current_price_for_notification else 'Unknown'
-                            stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token)
-                            
-                            await self.notification_manager.send_generic_notification(
-                                f"🛡️ <b>Stop Loss LIMIT Order Placed</b>\n\n"
-                                f"Token: {token}\n"
-                                f"Lifecycle ID: {lifecycle_id[:8]}...\n"
-                                f"Position Type: {position_side.upper()}\n"
-                                f"Stop Loss Price: {stop_loss_price_str_notify}\n"
-                                f"Amount: {formatter.format_amount(abs(current_amount), token)}\n"
-                                f"Current Price: {current_price_str_notify}\n"
-                                f"Exchange SL Order ID: {sl_exchange_order_id}\n"
-                                f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                            )
-                        elif not sl_exchange_order_id:
-                             logger.warning(f"SL Limit order for {token} (Lifecycle: {lifecycle_id[:8]}) placed in DB (ID: {sl_db_order_id}) but no exchange ID returned immediately.")
-
-                    else:
-                        logger.error(f"❌ Failed to place SL limit order for {token} (Lifecycle: {lifecycle_id[:8]}): {sl_result.get('error')}")
-
-                except Exception as trade_error:
-                    logger.error(f"❌ Error processing position trade for SL activation (Lifecycle: {position_trade.get('trade_lifecycle_id','N/A')}): {trade_error}")
-            
-        except Exception as e:
-            logger.error(f"❌ Error activating pending stop losses from trades table: {e}", exc_info=True)
-
-    async def _check_for_recent_fills_for_order(self, exchange_oid, order_in_db):
-        """Check for very recent fills that might match this order."""
-        try:
-            # Get recent fills from exchange
-            recent_fills = self.trading_engine.get_recent_fills()
-            if not recent_fills:
-                return False
-
-            # Corrected to use self.last_processed_trade_time
-            if not hasattr(self, 'last_processed_trade_time') or self.last_processed_trade_time is None:
-                # Attempt to load if not present (should generally be pre-loaded)
-                try:
-                    last_time_str = self.trading_engine.stats._get_metadata('last_processed_trade_time')
-                    if last_time_str:
-                        self.last_processed_trade_time = datetime.fromisoformat(last_time_str)
-                    else:
-                        self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
-                except Exception: # Fallback on error
-                     self.last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
-
-
-            for fill in recent_fills:
-                try:
-                    trade_id = fill.get('id')
-                    timestamp_ms = fill.get('timestamp')
-                    symbol_from_fill = fill.get('symbol')
-                    side_from_fill = fill.get('side')
-                    amount_from_fill = float(fill.get('amount', 0))
-                    price_from_fill = float(fill.get('price', 0))
-                    
-                    timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) if timestamp_ms else datetime.now(timezone.utc)
-                    
-                    # Skip if already processed by the main external trade checker logic
-                    if timestamp_dt <= self.last_processed_trade_time:
-                        continue
-                    
-                    if symbol_from_fill and side_from_fill and amount_from_fill > 0 and price_from_fill > 0:
-                        exchange_order_id_from_fill = fill.get('info', {}).get('oid')
-                        
-                        if exchange_order_id_from_fill == exchange_oid:
-                            # Check if this fill matches the order details (symbol, side, approx amount)
-                            if order_in_db.get('symbol') == symbol_from_fill and \
-                               order_in_db.get('side') == side_from_fill and \
-                               abs(float(order_in_db.get('amount_requested', 0)) - amount_from_fill) < 0.01 * amount_from_fill : # Allow 1% tolerance
-                                logger.info(f"✅ Found recent matching fill {trade_id} for order {exchange_oid}. Not cancelling stop losses.")
-                                return True
-                
-                except Exception as e:
-                    logger.error(f"Error processing fill {fill.get('id','N/A')} in _check_for_recent_fills_for_order: {e}")
-                    continue
-            
-            return False
-
-        except Exception as e:
-            logger.error(f"❌ Error in _check_for_recent_fills_for_order for OID {exchange_oid}: {e}", exc_info=True)
-            return False 
-
-    async def _check_external_stop_loss_orders(self):
-        """Check for externally placed stop loss orders and track them."""
-        try:
-            # Get current open orders 
-            open_orders = self.cached_orders or [] # Use cache
-            if not open_orders:
-                return
-                
-            # Get current positions to understand what could be stop losses
-            positions = self.cached_positions or [] # Use cache
-            if not positions:
-                return
-                
-            # Create a map of current positions
-            position_map = {}
-            for position in positions:
-                symbol = position.get('symbol')
-                contracts = float(position.get('contracts', 0))
-                if symbol and contracts != 0:
-                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    position_map[token] = {
-                        'symbol': symbol,
-                        'contracts': contracts,
-                        'side': 'long' if contracts > 0 else 'short',
-                        'entry_price': float(position.get('entryPx', 0))
-                    }
-            
-            # Check each order to see if it could be a stop loss
-            newly_detected = 0
-            for order in open_orders:
-                try:
-                    exchange_order_id = order.get('id')
-                    symbol = order.get('symbol')
-                    side = order.get('side')  # 'buy' or 'sell'
-                    amount = float(order.get('amount', 0))
-                    price = float(order.get('price', 0))
-                    # order_type = order.get('type', '').lower() # Not strictly needed for this detection logic
-                    
-                    if not all([exchange_order_id, symbol, side, amount, price]):
-                        continue
-                        
-                    # Skip if we're already tracking this order
-                    if exchange_order_id in self.external_stop_losses:
-                        continue
-                        
-                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                    
-                    if token not in position_map:
-                        continue
-                        
-                    position_data = position_map[token]
-                    
-                    is_potential_stop_loss = False
-                    # For a LONG position, a SELL order below current entry could be a stop.
-                    if position_data['side'] == 'long' and side == 'sell' and price < position_data['entry_price']:
-                        is_potential_stop_loss = True
-                    # For a SHORT position, a BUY order above current entry could be a stop.
-                    elif position_data['side'] == 'short' and side == 'buy' and price > position_data['entry_price']:
-                        is_potential_stop_loss = True
-                    
-                    if is_potential_stop_loss:
-                        self.external_stop_losses[exchange_order_id] = {
-                            'token': token,
-                            'symbol': symbol,
-                            'trigger_price': price, # This is the order's price, acting as trigger
-                            'side': side, # Side of the SL order itself
-                            'amount': amount,
-                            'position_side': position_data['side'], # Side of the position it's protecting
-                            'detected_at': datetime.now(timezone.utc),
-                            'entry_price': position_data['entry_price'] # Entry of the protected position
-                        }
-                        newly_detected += 1
-                        logger.info(f"🛑 Detected potential external stop loss: {token} {side.upper()} {amount} @ ${price:.2f} (protecting {position_data['side'].upper()} position)")
-                        
-                except Exception as e:
-                    logger.error(f"Error analyzing order for stop loss detection: {e}")
-                    continue
-                    
-            if newly_detected > 0:
-                logger.info(f"🔍 Detected {newly_detected} new potential external stop loss orders")
-                
-        except Exception as e:
-            logger.error(f"❌ Error checking external stop loss orders: {e}")
-            
-    async def _cleanup_external_stop_loss_tracking(self):
-        """Clean up external stop loss orders that are no longer active."""
-        try:
-            if not self.external_stop_losses:
-                return
-                
-            # Get current open orders
-            open_orders = self.cached_orders or [] # Use cache
-            if not open_orders:
-                removed_count = len(self.external_stop_losses)
-                if removed_count > 0:
-                    logger.info(f"🧹 Cleared {removed_count} external stop loss orders (no open orders on exchange)")
-                    self.external_stop_losses.clear()
-                return
-                
-            current_order_ids = {order.get('id') for order in open_orders if order.get('id')}
-            
-            to_remove = [order_id for order_id in self.external_stop_losses if order_id not in current_order_ids]
-                    
-            for order_id in to_remove:
-                stop_loss_info = self.external_stop_losses.pop(order_id) # Use pop to remove and get value
-                logger.info(f"🗑️ Removed external stop loss tracking for {stop_loss_info['token']} order {order_id} (no longer open)")
-                
-            if to_remove:
-                logger.info(f"🧹 Cleaned up {len(to_remove)} disappeared external stop loss orders from tracking")
-                
-        except Exception as e:
-            logger.error(f"❌ Error cleaning up external stop loss tracking: {e}") 
+                        try:
+                            # Extract all relevant data from exchange position (ex_pos)
+                            current_size_from_ex = ex_pos.get('contracts')
+                            current_position_size = float(current_size_from_ex) if current_size_from_ex is not None else None
 
 
-    async def _auto_sync_orphaned_positions(self):
-        """Automatically detect and sync orphaned positions (positions on exchange without trade lifecycle records)."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                return
+                            entry_price_from_ex = ex_pos.get('entryPrice') or ex_pos.get('entryPx')
+                            entry_price = float(entry_price_from_ex) if entry_price_from_ex is not None else None
 
 
-            formatter = get_formatter()
+                            mark_price_from_ex = ex_pos.get('markPrice') or ex_pos.get('markPx')
+                            mark_price = float(mark_price_from_ex) if mark_price_from_ex is not None else None
 
 
-            exchange_positions = self.cached_positions or [] # Use fresh cache
-            synced_count = 0
+                            unrealized_pnl_from_ex = ex_pos.get('unrealizedPnl')
+                            unrealized_pnl = float(unrealized_pnl_from_ex) if unrealized_pnl_from_ex is not None else None
 
 
-            for exchange_pos in exchange_positions:
-                symbol = exchange_pos.get('symbol')
-                contracts_abs = abs(float(exchange_pos.get('contracts', 0))) 
-                
-                if not (symbol and contracts_abs > 1e-9): # Ensure position is substantial
-                    continue
+                            liquidation_price_from_ex = ex_pos.get('liquidationPrice')
+                            liquidation_price = float(liquidation_price_from_ex) if liquidation_price_from_ex is not None else None
 
 
-                # Check if we have an active trade lifecycle record for this position
-                # A more robust check would be against a specific exchange identifier for the position if available
-                existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
-                
-                if not existing_trade:
-                    entry_price_from_exchange = float(exchange_pos.get('entryPrice', 0)) or float(exchange_pos.get('entryPx', 0))
-                    
-                    position_side, order_side = '', ''
-                    ccxt_side = exchange_pos.get('side', '').lower()
-                    if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
-                    elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
-                    
-                    if not position_side: # Fallback to raw info
-                        raw_info = exchange_pos.get('info', {}).get('position', {})
-                        if isinstance(raw_info, dict):
-                            szi_str = raw_info.get('szi')
-                            if szi_str is not None:
-                                try: szi_val = float(szi_str)
-                                except ValueError: szi_val = 0
-                                if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
-                                elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
-                    
-                    if not position_side: # Final fallback
-                        contracts_val = float(exchange_pos.get('contracts',0))
-                        if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
-                        elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell' # Assumes negative for short
-                        else:
-                            logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
-                            continue
-                    
-                    if not position_side:
-                        logger.error(f"AUTO-SYNC: CRITICAL - Could not determine position side for {symbol}. Data: {exchange_pos}. Skipping.")
-                        continue
+                            margin_used_from_ex = ex_pos.get('marginUsed')
+                            margin_used = float(margin_used_from_ex) if margin_used_from_ex is not None else None
 
 
-                    token = symbol.split('/')[0] if '/' in symbol else symbol
-                    actual_contracts_size = contracts_abs # Already absolute
+                            leverage_from_ex = ex_pos.get('leverage')
+                            leverage = float(leverage_from_ex) if leverage_from_ex is not None else None
 
 
-                    final_entry_price = entry_price_from_exchange
-                    price_source_log = "(exchange data)"
-                    if not final_entry_price or final_entry_price <= 0:
-                        estimated_entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, actual_contracts_size, position_side)
-                        if estimated_entry_price > 0:
-                            final_entry_price = estimated_entry_price
-                            price_source_log = "(estimated)"
-                        else:
-                            logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
-                            continue
-                    
-                    logger.info(f"🔄 AUTO-SYNC: Orphaned position detected - {symbol} {position_side.upper()} {actual_contracts_size} @ ${final_entry_price:.4f} {price_source_log}")
-                    
-                    lifecycle_id = stats.create_trade_lifecycle(
-                        symbol=symbol, side=order_side, # side of the entry order
-                        entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
-                        trade_type='external_sync' # Specific type for auto-synced trades
-                    )
-                    
-                    if lifecycle_id:
-                        success = stats.update_trade_position_opened(
-                            lifecycle_id, final_entry_price, actual_contracts_size,
-                            f"external_fill_sync_{int(datetime.now().timestamp())}"
-                        )
-                        
-                        if success:
-                            synced_count += 1
-                            logger.info(f"✅ AUTO-SYNC: Successfully synced orphaned position for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+                            position_value_from_ex = ex_pos.get('notional')
+                            position_value = float(position_value_from_ex) if position_value_from_ex is not None else None
                             
                             
-                            if self.notification_manager:
-                                unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
-                                pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-                                notification_text = (
-                                    f"🔄 <b>Position Auto-Synced</b>\n\n"
-                                    f"Token: {token}\n"
-                                    f"Lifecycle ID: {lifecycle_id[:8]}...\n"
-                                    f"Direction: {position_side.upper()}\n"
-                                    f"Size: {actual_contracts_size:.6f} {token}\n"
-                                    f"Entry Price: ${final_entry_price:,.4f} {price_source_log}\n"
-                                    f"{pnl_emoji} P&L (Unrealized): ${unrealized_pnl:,.2f}\n"
-                                    f"Reason: Position found on exchange without bot record.\n"
-                                    f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                                    f"✅ Position now tracked. Use /sl or /tp if needed."
-                                )
-                                await self.notification_manager.send_generic_notification(notification_text)
-                        else:
-                            logger.error(f"❌ AUTO-SYNC: Failed to update lifecycle to 'position_opened' for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
-                    else:
-                        logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for orphaned position {symbol}.")
-
-            if synced_count > 0:
-                logger.info(f"🔄 AUTO-SYNC: Synced {synced_count} orphaned position(s) this cycle (Exchange had position, Bot did not).")
-
-            # --- NEW LOGIC: Bot thinks position is open, but exchange does not --- #
-            bot_open_lifecycles = stats.get_trades_by_status('position_opened')
-            if not bot_open_lifecycles:
-                return # No open lifecycles according to the bot, nothing to check here.
-
-            # Create a map of current exchange positions for quick lookup: symbol -> position_data
-            current_exchange_positions_map = {}
-            for ex_pos in (self.cached_positions or []): # Use cached, recently updated positions
-                if ex_pos.get('symbol') and abs(float(ex_pos.get('contracts', 0))) > 1e-9:
-                    current_exchange_positions_map[ex_pos.get('symbol')] = ex_pos
-            
-            closed_due_to_discrepancy = 0
-            for lc in bot_open_lifecycles:
-                symbol = lc.get('symbol')
-                lc_id = lc.get('trade_lifecycle_id')
-                token = symbol.split('/')[0] if '/' in symbol else symbol
-
-                if symbol not in current_exchange_positions_map:
-                    # Bot has an open lifecycle, but no corresponding position found on exchange.
-                    logger.warning(f"🔄 AUTO-SYNC (Discrepancy): Bot lifecycle {lc_id} for {symbol} is 'position_opened', but NO position found on exchange. Closing lifecycle.")
-                    
-                    entry_price = lc.get('entry_price', 0)
-                    position_side = lc.get('position_side')
-                    position_size_for_pnl = lc.get('current_position_size', 0)
-                    exit_price_for_calc = 0
-                    price_source_info = "unknown"
-
-                    # Attempt to find a recent closing fill from the exchange
-                    try:
-                        # Fetch all recent fills for the account, then filter by symbol
-                        all_recent_fills = self.trading_engine.get_recent_fills() # Increased limit slightly
-                        if all_recent_fills:
-                            symbol_specific_fills = [f for f in all_recent_fills if f.get('symbol') == symbol]
-                            if symbol_specific_fills:
-                                closing_side = 'sell' if position_side == 'long' else 'buy'
-                                relevant_fills = sorted(
-                                    [f for f in symbol_specific_fills if f.get('side') == closing_side], # Already filtered by symbol
-                                    key=lambda f: f.get('timestamp'), reverse=True # Most recent first
-                                )
-                                if relevant_fills:
-                                    last_closing_fill = relevant_fills[0]
-                                    exit_price_for_calc = float(last_closing_fill.get('price', 0))
-                                    fill_timestamp = datetime.fromtimestamp(last_closing_fill.get('timestamp')/1000, tz=timezone.utc).isoformat() if last_closing_fill.get('timestamp') else "N/A"
-                                    price_source_info = f"last exchange fill ({formatter.format_price(exit_price_for_calc, symbol)} @ {fill_timestamp})"
-                                    logger.info(f"AUTO-SYNC: Using exit price {price_source_info} for {symbol} lifecycle {lc_id}.")
-                    except Exception as e:
-                        logger.warning(f"AUTO-SYNC: Error fetching recent fills for {symbol} to determine exit price: {e}")
-
-                    if not exit_price_for_calc or exit_price_for_calc <= 0:
-                        # Fallback to mark_price from lifecycle if available
-                        mark_price_from_lc = lc.get('mark_price')
-                        if mark_price_from_lc and float(mark_price_from_lc) > 0:
-                            exit_price_for_calc = float(mark_price_from_lc)
-                            price_source_info = "lifecycle mark_price"
-                            logger.info(f"AUTO-SYNC: No recent fill found. Using exit price from lifecycle mark_price: {formatter.format_price(exit_price_for_calc, symbol)} for {symbol} lifecycle {lc_id}.")
-                        else:
-                            # Last resort: use entry_price (implies 0 PNL for this closure action)
-                            exit_price_for_calc = entry_price
-                            price_source_info = "lifecycle entry_price (0 PNL)"
-                            logger.info(f"AUTO-SYNC: No recent fill or mark_price. Using entry_price: {formatter.format_price(exit_price_for_calc, symbol)} for {symbol} lifecycle {lc_id}.")
-                    
-                    realized_pnl = 0
-                    if position_side == 'long':
-                        realized_pnl = position_size_for_pnl * (exit_price_for_calc - entry_price)
-                    elif position_side == 'short':
-                        realized_pnl = position_size_for_pnl * (entry_price - exit_price_for_calc)
-                    
-                    success = stats.update_trade_position_closed(
-                        lifecycle_id=lc_id,
-                        exit_price=exit_price_for_calc, 
-                        realized_pnl=realized_pnl,
-                        exchange_fill_id=f"auto_sync_flat_{int(datetime.now().timestamp())}"
-                    )
-                    
-                    if success:
-                        closed_due_to_discrepancy += 1
-                        logger.info(f"✅ AUTO-SYNC (Discrepancy): Successfully closed bot lifecycle {lc_id} for {symbol}.")
-                        # MIGRATE STATS
-                        stats._migrate_trade_to_aggregated_stats(lc_id)
-                        if self.notification_manager:
-                            pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                            # formatter is already defined in the outer scope of _auto_sync_orphaned_positions
-                            notification_text = (
-                                f"🔄 <b>Position Auto-Closed (Discrepancy)</b>\n\n"
-                                f"Token: {token}\n"
-                                f"Lifecycle ID: {lc_id[:8]}...\n"
-                                f"Reason: Bot showed open position, but no corresponding position found on exchange.\n"
-                                f"Assumed Exit Price: {formatter.format_price(exit_price_for_calc, symbol)}\n"
-                                f"{pnl_emoji} Realized P&L for this closure: {formatter.format_price_with_symbol(realized_pnl)}\n"
-                                f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                                f"ℹ️ Bot state synchronized with exchange."
+                            if position_value is None and mark_price is not None and current_position_size is not None:
+                                position_value = abs(current_position_size) * mark_price
+
+                            roe_from_ex = ex_pos.get('percentage')
+                            unrealized_pnl_percentage_val = float(roe_from_ex) if roe_from_ex is not None else None
+
+                            stats.update_trade_market_data(
+                                trade_lifecycle_id=lifecycle_id, 
+                                unrealized_pnl=unrealized_pnl, 
+                                mark_price=mark_price,
+                                current_position_size=current_position_size,
+                                entry_price=entry_price, 
+                                liquidation_price=liquidation_price,
+                                margin_used=margin_used,
+                                leverage=leverage,
+                                position_value=position_value,
+                                unrealized_pnl_percentage=unrealized_pnl_percentage_val
                             )
                             )
-                            await self.notification_manager.send_generic_notification(notification_text)
+                        except (ValueError, TypeError) as e:
+                            logger.warning(f"Could not parse full market data for {symbol} (Lifecycle: {lifecycle_id}) from {ex_pos}: {e}")
                     else:
                     else:
-                        logger.error(f"❌ AUTO-SYNC (Discrepancy): Failed to close bot lifecycle {lc_id} for {symbol}.")
-            
-            if closed_due_to_discrepancy > 0:
-                logger.info(f"🔄 AUTO-SYNC: Closed {closed_due_to_discrepancy} lifecycle(s) due to discrepancy (Bot had position, Exchange did not).")
-
-        except Exception as e:
-            logger.error(f"❌ Error in auto-sync orphaned positions: {e}", exc_info=True)
-
-    async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float, side: str) -> float:
-        """Estimate entry price for an orphaned position by checking recent fills and market data."""
-        try:
-            entry_fill_side = 'buy' if side == 'long' else 'sell'
-            formatter = get_formatter()
-            token = symbol.split('/')[0] if '/' in symbol else symbol
-            all_recent_fills = self.trading_engine.get_recent_fills() # Removed symbol and limit arguments
-            recent_fills = [f for f in all_recent_fills if f.get('symbol') == symbol] # Filter by symbol
-
-            if recent_fills:
-                symbol_side_fills = [
-                    fill for fill in recent_fills 
-                    if fill.get('symbol') == symbol and fill.get('side') == entry_fill_side and float(fill.get('amount',0)) > 0
-                ]
-                if symbol_side_fills:
-                    symbol_side_fills.sort(key=lambda f: (
-                        datetime.fromtimestamp(f.get('timestamp') / 1000, tz=timezone.utc) if f.get('timestamp') else datetime.min.replace(tzinfo=timezone.utc),
-                        abs(float(f.get('amount',0)) - contracts)
-                        ), reverse=True)
-                    
-                    best_fill = symbol_side_fills[0]
-                    fill_price = float(best_fill.get('price', 0))
-                    fill_amount = float(best_fill.get('amount', 0))
-                    if fill_price > 0:
-                        logger.info(f"💡 AUTO-SYNC: Estimated entry for {side} {symbol} via recent {entry_fill_side} fill: {formatter.format_price_with_symbol(fill_price, token)} (Amount: {formatter.format_amount(fill_amount, token)})")
-                        return fill_price
-            
-            market_data = self.trading_engine.get_market_data(symbol)
-            if market_data and market_data.get('ticker'):
-                current_price = float(market_data['ticker'].get('last', 0))
-                if current_price > 0:
-                    logger.warning(f"⚠️ AUTO-SYNC: Using current market price as entry estimate for {side} {symbol}: {formatter.format_price_with_symbol(current_price, token)}")
-                    return current_price
+                        # This case means there's a position on the exchange, but no 'position_opened' lifecycle in the DB.
+                        # This should be handled by the _auto_sync_orphaned_positions logic in PositionSynchronizer.
+                        # For _update_cached_data, we only update market data for KNOWN lifecycles.
+                        logger.debug(f"No 'position_opened' lifecycle found for symbol {symbol} during _update_cached_data. Orphan sync should handle it.")
             
             
-            if market_data and market_data.get('ticker'):
-                bid = float(market_data['ticker'].get('bid', 0))
-                ask = float(market_data['ticker'].get('ask', 0))
-                if bid > 0 and ask > 0: return (bid + ask) / 2
-
-            logger.warning(f"AUTO-SYNC: Could not estimate entry price for {side} {symbol} through any method.")
-            return 0.0
         except Exception as e:
         except Exception as e:
-            logger.error(f"❌ Error estimating entry price for orphaned position {symbol}: {e}", exc_info=True)
-            return 0.0
-
-    async def _immediate_startup_auto_sync(self):
-        """🆕 Immediately check for and sync orphaned positions on startup."""
-        try:
-            logger.info("🔍 STARTUP: Checking for orphaned positions...")
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                logger.warning("⚠️ STARTUP: TradingStats not available for auto-sync.")
-                return
-
-            formatter = get_formatter() # Ensure formatter is available
-            exchange_positions = self.trading_engine.get_positions() or []
-            if not exchange_positions:
-                logger.info("✅ STARTUP: No positions found on exchange.")
-                return
-                
-            synced_count = 0
-            for exchange_pos in exchange_positions:
-                symbol = exchange_pos.get('symbol')
-                contracts_abs = abs(float(exchange_pos.get('contracts', 0)))
-                token_for_log = symbol.split('/')[0] if symbol and '/' in symbol else symbol # Prepare token for logging
-                
-                if not (symbol and contracts_abs > 1e-9): continue
-
-                existing_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
-                if not existing_trade_lc:
-                    position_side, order_side = '', ''
-                    ccxt_side = exchange_pos.get('side', '').lower()
-                    if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
-                    elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
-                    
-                    if not position_side:
-                        raw_info = exchange_pos.get('info', {}).get('position', {})
-                        if isinstance(raw_info, dict):
-                            szi_str = raw_info.get('szi')
-                            if szi_str is not None:
-                                try: szi_val = float(szi_str)
-                                except ValueError: szi_val = 0
-                                if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
-                                elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
-                    
-                    if not position_side:
-                        contracts_val = float(exchange_pos.get('contracts',0))
-                        if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
-                        elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell'
-                        else:
-                            logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
-                            continue
-                    
-                    if not position_side:
-                        logger.error(f"AUTO-SYNC: CRITICAL - Could not determine position side for {symbol}. Data: {exchange_pos}. Skipping.")
-                        continue
-
-                    entry_price = float(exchange_pos.get('entryPrice', 0)) or float(exchange_pos.get('entryPx', 0))
-                    price_source_log = "(exchange data)"
-                    if not entry_price or entry_price <= 0:
-                        estimated_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts_abs, position_side)
-                        if estimated_price > 0: 
-                            entry_price = estimated_price
-                            price_source_log = "(estimated)"
-                        else:
-                            logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
-                            continue
-                    
-                    logger.info(f"🔄 STARTUP: Auto-syncing orphaned position: {symbol} {position_side.upper()} {formatter.format_amount(contracts_abs, token_for_log)} @ {formatter.format_price_with_symbol(entry_price, token_for_log)} {price_source_log}")
-                    
-                    lifecycle_id = stats.create_trade_lifecycle(
-                        symbol=symbol, side=order_side,
-                        entry_order_id=f"startup_sync_{int(datetime.now().timestamp())}",
-                        trade_type='external_startup_sync'
-                    )
-                    
-                    if lifecycle_id:
-                        success = stats.update_trade_position_opened(
-                            lifecycle_id, entry_price, contracts_abs,
-                            f"startup_fill_sync_{int(datetime.now().timestamp())}"
-                        )
-                        if success:
-                            synced_count += 1
-                            logger.info(f"✅ STARTUP: Successfully synced orphaned position for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
-                            await self._send_startup_auto_sync_notification(exchange_pos, symbol, position_side, contracts_abs, entry_price, lifecycle_id, price_source_log)
-                        else: logger.error(f"❌ STARTUP: Failed to update lifecycle for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
-                    else: logger.error(f"❌ STARTUP: Failed to create lifecycle for {symbol}.")
-            
-            if synced_count == 0 and exchange_positions:
-                 logger.info("✅ STARTUP: All existing exchange positions are already tracked.")
-            elif synced_count > 0:
-                 logger.info(f"🎉 STARTUP: Auto-synced {synced_count} orphaned position(s) (Exchange had pos, Bot did not).")
-
-            # --- NEW LOGIC FOR STARTUP: Bot thinks position is open, but exchange does not --- #
-            logger.info("🔍 STARTUP: Checking for discrepancies (Bot has pos, Exchange does not)...")
-            bot_open_lifecycles = stats.get_trades_by_status('position_opened')
-            
-            # Create a map of current exchange positions for quick lookup: symbol -> position_data
-            current_exchange_positions_map = {}
-            for ex_pos in (exchange_positions or []): # Use the exchange_positions fetched at the start of this method
-                if ex_pos.get('symbol') and abs(float(ex_pos.get('contracts', 0))) > 1e-9:
-                    current_exchange_positions_map[ex_pos.get('symbol')] = ex_pos
-
-            closed_due_to_discrepancy_startup = 0
-            if bot_open_lifecycles:
-                for lc in bot_open_lifecycles:
-                    symbol = lc.get('symbol')
-                    lc_id = lc.get('trade_lifecycle_id')
-                    token_for_log_discrepancy = symbol.split('/')[0] if symbol and '/' in symbol else symbol
-
-                    if symbol not in current_exchange_positions_map:
-                        logger.warning(f"🔄 STARTUP (Discrepancy): Bot lifecycle {lc_id} for {symbol} is 'position_opened', but NO position found on exchange. Closing lifecycle.")
-                        
-                        entry_price = lc.get('entry_price', 0)
-                        position_side = lc.get('position_side')
-                        position_size_for_pnl = lc.get('current_position_size', 0)
-                        exit_price_for_calc = 0
-                        price_source_info = "unknown"
-
-                        try:
-                            # Fetch all recent fills, then filter by symbol
-                            all_recent_fills_for_startup_sync = self.trading_engine.get_recent_fills() # Fetch more to increase chance
-                            if all_recent_fills_for_startup_sync:
-                                symbol_specific_fills_startup = [f for f in all_recent_fills_for_startup_sync if f.get('symbol') == symbol]
-                                if symbol_specific_fills_startup:
-                                    closing_side = 'sell' if position_side == 'long' else 'buy'
-                                    relevant_fills = sorted(
-                                        [f for f in symbol_specific_fills_startup if f.get('side') == closing_side], # Already filtered by symbol
-                                        key=lambda f: f.get('timestamp'), reverse=True
-                                    )
-                                    if relevant_fills:
-                                        last_closing_fill = relevant_fills[0]
-                                        exit_price_for_calc = float(last_closing_fill.get('price', 0))
-                                        fill_ts_val = last_closing_fill.get('timestamp')
-                                        fill_timestamp_str = datetime.fromtimestamp(fill_ts_val/1000, tz=timezone.utc).isoformat() if fill_ts_val else "N/A"
-                                        price_source_info = f"last exchange fill ({formatter.format_price(exit_price_for_calc, symbol)} @ {fill_timestamp_str})"
-                                        logger.info(f"STARTUP SYNC: Using exit price {price_source_info} for {symbol} lifecycle {lc_id}.")
-                        except Exception as e:
-                            logger.warning(f"STARTUP SYNC: Error fetching recent fills for {symbol} to determine exit price: {e}")
-
-                        if not exit_price_for_calc or exit_price_for_calc <= 0:
-                            mark_price_from_lc = lc.get('mark_price')
-                            if mark_price_from_lc and float(mark_price_from_lc) > 0:
-                                exit_price_for_calc = float(mark_price_from_lc)
-                                price_source_info = "lifecycle mark_price"
-                            else:
-                                exit_price_for_calc = entry_price
-                                price_source_info = "lifecycle entry_price (0 PNL)"
-                        
-                        realized_pnl = 0
-                        if position_side == 'long':
-                            realized_pnl = position_size_for_pnl * (exit_price_for_calc - entry_price)
-                        elif position_side == 'short':
-                            realized_pnl = position_size_for_pnl * (entry_price - exit_price_for_calc)
-                        
-                        success_close = stats.update_trade_position_closed(
-                            lifecycle_id=lc_id,
-                            exit_price=exit_price_for_calc, 
-                            realized_pnl=realized_pnl,
-                            exchange_fill_id=f"startup_sync_flat_{int(datetime.now().timestamp())}"
-                        )
-                        
-                        if success_close:
-                            closed_due_to_discrepancy_startup += 1
-                            logger.info(f"✅ STARTUP (Discrepancy): Successfully closed bot lifecycle {lc_id} for {symbol}.")
-                            # MIGRATE STATS
-                            stats._migrate_trade_to_aggregated_stats(lc_id)
-                            if self.notification_manager:
-                                pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                                notification_text = (
-                                    f"🔄 <b>Position Auto-Closed (Startup Sync)</b>\n\n"
-                                    f"Token: {token_for_log_discrepancy}\n"
-                                    f"Lifecycle ID: {lc_id[:8]}...\n"
-                                    f"Reason: Bot startup - found open lifecycle, but no corresponding position on exchange.\n"
-                                    f"Assumed Exit Price: {formatter.format_price(exit_price_for_calc, symbol)} (Source: {price_source_info})\n"
-                                    f"{pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(realized_pnl)}\n"
-                                    f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                )
-                                await self.notification_manager.send_generic_notification(notification_text)
-                        else:
-                            logger.error(f"❌ STARTUP (Discrepancy): Failed to close bot lifecycle {lc_id} for {symbol}.")
-            
-            if closed_due_to_discrepancy_startup > 0:
-                logger.info(f"🎉 STARTUP: Auto-closed {closed_due_to_discrepancy_startup} lifecycle(s) due to discrepancy (Bot had pos, Exchange did not).")
-            else:
-                logger.info("✅ STARTUP: No discrepancies found where bot had position and exchange did not.")
-                
-        except Exception as e:
-            logger.error(f"❌ Error in startup auto-sync: {e}", exc_info=True)
-
-    async def _send_startup_auto_sync_notification(self, exchange_pos, symbol, position_side, contracts, entry_price, lifecycle_id, price_source_log):
-        """Send notification for positions auto-synced on startup."""
-        try:
-            if not self.notification_manager: return
-
-            formatter = get_formatter()
-            token = symbol.split('/')[0] if '/' in symbol else symbol
-            unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
-            pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-            
-            size_str = formatter.format_amount(contracts, token)
-            entry_price_str = formatter.format_price_with_symbol(entry_price, token)
-            pnl_str = formatter.format_price_with_symbol(unrealized_pnl)
-
-            notification_text_parts = [
-                f"🚨 <b>Bot Startup: Position Auto-Synced</b>\n",
-                f"Token: {token}",
-                f"Lifecycle ID: {lifecycle_id[:8]}...",
-                f"Direction: {position_side.upper()}",
-                f"Size: {size_str} {token}",
-                f"Entry Price: {entry_price_str} {price_source_log}",
-                f"{pnl_emoji} P&L (Unrealized): {pnl_str}",
-                f"Reason: Position found on exchange without bot record.",
-                # f"Time: {datetime.now().strftime('%H:%M:%S')}", # Time is in the main header of notification usually
-                "\n✅ Position now tracked. Use /sl or /tp if needed."
-            ]
-            
-            liq_price = float(exchange_pos.get('liquidationPrice', 0))
-            if liq_price > 0: 
-                liq_price_str = formatter.format_price_with_symbol(liq_price, token)
-                notification_text_parts.append(f"⚠️ Liquidation: {liq_price_str}")
-            
-            # Combined details into the main block
-            # notification_text_parts.append("\n📍 <b>Discovered on bot startup</b>")
-            # notification_text_parts.append(f"⏰ Time: {datetime.now().strftime('%H:%M:%S')}")
-            # notification_text_parts.append("\n✅ Position now tracked. Use /sl or /tp if needed.")
-            
-            await self.notification_manager.send_generic_notification("\n".join(notification_text_parts))
-            logger.info(f"📤 STARTUP: Sent auto-sync notification for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
-            
-        except Exception as e:
-            logger.error(f"❌ STARTUP: Failed to send auto-sync notification for {symbol}: {e}")
-
-# Note: The _activate_pending_stop_losses method was intentionally removed 
-# as its functionality is now covered by _activate_pending_stop_losses_from_trades 
-# and _check_pending_triggers, driven by the Trade Lifecycle.
+            logger.error(f"❌ Error updating cached data: {e}", exc_info=True)
+
+    # --- Cache Accessor Methods ---
+    def get_cached_positions(self) -> List[Dict[str, Any]]:
+        return self.cache.cached_positions
+
+    def get_cached_orders(self) -> List[Dict[str, Any]]:
+        return self.cache.cached_orders
+
+    def get_cached_balance(self) -> Optional[Dict[str, Any]]:
+        return self.cache.cached_balance
+
+    def get_cache_age_seconds(self) -> Optional[float]:
+        if self.cache.last_cache_update:
+            return (datetime.now(timezone.utc) - self.cache.last_cache_update).total_seconds()
+        return None
+
+    # --- Alarm Management (delegated to AlarmManager, but MarketMonitor provides interface) ---
+    def add_price_alarm(self, token: str, target_price: float, direction: str, user_id: int) -> Optional[int]:
+        alarm_id = self.alarm_manager.add_alarm(token, target_price, direction, user_id)
+        if alarm_id:
+            logger.info(f"Price alarm added: ID {alarm_id} for {token} {direction} ${target_price}")
+            # self._save_state() # AlarmManager should handle its own persistence if needed
+        return alarm_id
+
+    def remove_price_alarm(self, alarm_id: int, user_id: int) -> bool:
+        removed = self.alarm_manager.remove_alarm(alarm_id, user_id)
+        if removed:
+            logger.info(f"Price alarm {alarm_id} removed by user {user_id}")
+            # self._save_state()
+        return removed
+
+    def get_user_alarms(self, user_id: int) -> List[Dict[str, Any]]:
+        return self.alarm_manager.get_alarms_by_user(user_id)
+
+    def get_all_active_alarms(self) -> List[Dict[str, Any]]:
+        return self.alarm_manager.get_all_active_alarms()
+
+    # Methods that were moved are now removed from MarketMonitor.
+    # _check_order_fills -> OrderFillProcessor
+    # _process_disappeared_orders -> OrderFillProcessor
+    # _activate_pending_stop_losses_from_trades -> OrderFillProcessor
+    # _check_for_recent_fills_for_order -> OrderFillProcessor (helper)
+    # _auto_sync_orphaned_positions -> PositionSynchronizer
+    # _immediate_startup_auto_sync -> PositionSynchronizer
+    # _estimate_entry_price_for_orphaned_position -> PositionSynchronizer (helper)
+    # _send_startup_auto_sync_notification -> PositionSynchronizer (helper)
+    # _check_external_trades -> ExternalEventMonitor
+    # _check_price_alarms -> ExternalEventMonitor
+    # _send_alarm_notification -> ExternalEventMonitor (helper)
+    # _check_pending_triggers -> RiskCleanupManager
+    # _check_automatic_risk_management -> RiskCleanupManager
+    # _cleanup_orphaned_stop_losses -> RiskCleanupManager
+    # _check_external_stop_loss_orders -> RiskCleanupManager
+    # _cleanup_external_stop_loss_tracking -> RiskCleanupManager
+    # _cleanup_orphaned_pending_sl_activations -> RiskCleanupManager (new stub)
+
+    # Methods related to direct position/order processing like _process_filled_orders
+    # and _update_position_tracking are implicitly part of OrderFillProcessor's logic now.
+    # The complex internal logic of _check_external_trades for lifecycle updates is now within ExternalEventMonitor.
+    # The state for `external_stop_losses` is now managed by `RiskCleanupManager` via `shared_state`.
+    # The state for `last_processed_trade_time` for external fills is managed by `ExternalEventMonitor`.
+    # The state for `last_processed_trade_time_helper` for `_check_for_recent_fills_for_order` is in `MarketMonitorCache`.

+ 324 - 0
src/monitoring/order_fill_processor.py

@@ -0,0 +1,324 @@
+#!/usr/bin/env python3
+"""
+Handles processing of order fills and disappeared orders.
+"""
+
+import logging
+import asyncio
+from datetime import datetime, timedelta, timezone
+from typing import Optional, Dict, Any, List
+
+from src.utils.token_display_formatter import get_formatter
+
+logger = logging.getLogger(__name__)
+
+class OrderFillProcessor:
+    def __init__(self, trading_engine, notification_manager, market_monitor_cache):
+        self.trading_engine = trading_engine
+        self.notification_manager = notification_manager
+        self.market_monitor_cache = market_monitor_cache # To access cached orders/positions
+        # Add necessary initializations
+
+    # Methods like _check_order_fills, _process_disappeared_orders, _activate_pending_stop_losses_from_trades will go here
+    pass 
+
+    async def _check_order_fills(self):
+        """Check for filled orders and send notifications."""
+        try:
+            # Get current orders and positions
+            current_orders = self.market_monitor_cache.cached_orders or [] # Use cache
+            # current_positions = self.market_monitor_cache.cached_positions or [] # Use cache. Not directly used here.
+            
+            # Get current order IDs
+            current_order_ids = {order.get('id') for order in current_orders if order.get('id')}
+            
+            # Find filled orders (orders that were in last_known_orders but not in current_orders)
+            disappeared_order_ids = self.market_monitor_cache.last_known_orders - current_order_ids
+            
+            if disappeared_order_ids:
+                logger.info(f"🎯 Detected {len(disappeared_order_ids)} bot orders no longer open: {list(disappeared_order_ids)}. Corresponding fills (if any) are processed by external trade checker.")
+                await self._process_disappeared_orders(disappeared_order_ids)
+            
+            # Update tracking data for open bot orders
+            self.market_monitor_cache.last_known_orders = current_order_ids
+            
+        except Exception as e:
+            logger.error(f"❌ Error checking order fills: {e}")
+
+    async def _process_disappeared_orders(self, disappeared_order_ids: set):
+        """Log and investigate bot orders that have disappeared from the exchange."""
+        stats = self.trading_engine.get_stats()
+        if not stats:
+            logger.warning("⚠️ TradingStats not available in _process_disappeared_orders.")
+            return
+
+        try:
+            total_linked_cancelled = 0
+            external_cancellations = []
+            
+            for exchange_oid in disappeared_order_ids:
+                order_in_db = stats.get_order_by_exchange_id(exchange_oid)
+                
+                if order_in_db:
+                    last_status = order_in_db.get('status', 'unknown')
+                    order_type = order_in_db.get('type', 'unknown')
+                    symbol = order_in_db.get('symbol', 'unknown')
+                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                    
+                    logger.info(f"Order {exchange_oid} was in our DB with status '{last_status}' but has now disappeared from exchange.")
+                    
+                    active_statuses = ['open', 'submitted', 'partially_filled', 'pending_submission']
+                    if last_status in active_statuses:
+                        # Before declaring external cancellation, check for very recent fills for this OID
+                        # This is a quick check to see if a fill came through just before it disappeared
+                        # and _check_external_trades hasn't processed it yet.
+                        # This check is specific to this "disappeared orders" context.
+                        # fill_just_processed = await self._check_for_recent_fills_for_order(exchange_oid, order_in_db)
+                        # if fill_just_processed:
+                        #     logger.info(f"ℹ️ Order {exchange_oid} disappeared, but a recent fill was found. Assuming filled. Main fill processing will handle lifecycle.")
+                        #     # Potentially update order_in_db status here or rely on main fill processor
+                        #     continue # Skip to next disappeared_order_id
+
+                        # If no immediate fill found by the helper, proceed with external cancellation logic
+                        logger.warning(f"⚠️ EXTERNAL CANCELLATION: Order {exchange_oid} with status '{last_status}' was likely cancelled externally on Hyperliquid")
+                        stats.update_order_status(exchange_order_id=exchange_oid, new_status='cancelled_externally')
+                        
+                        external_cancellations.append({
+                            'exchange_oid': exchange_oid,
+                            'token': token,
+                            'type': order_type,
+                            'last_status': last_status
+                        })
+                        
+                        if self.notification_manager:
+                            await self.notification_manager.send_generic_notification(
+                                f"⚠️ <b>External Order Cancellation Detected</b>\n\n"
+                                f"Token: {token}\n"
+                                f"Order Type: {order_type.replace('_', ' ').title()}\n"
+                                f"Exchange Order ID: <code>{exchange_oid[:8]}...</code>\n"
+                                f"Previous Status: {last_status.replace('_', ' ').title()}\n"
+                                f"Source: Cancelled directly on Hyperliquid\n"
+                                f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}\n\n"
+                                f"🤖 Bot status updated automatically"
+                            )
+                            
+                        order_after_external_check = stats.get_order_by_exchange_id(exchange_oid)
+                        if order_after_external_check and order_after_external_check.get('status') == 'cancelled_externally':
+                            pending_lc = stats.get_lifecycle_by_entry_order_id(exchange_oid, status='pending')
+                            if pending_lc:
+                                lc_id_to_cancel = pending_lc.get('trade_lifecycle_id')
+                                if lc_id_to_cancel:
+                                    cancel_reason = f"entry_order_{exchange_oid[:8]}_disappeared_externally"
+                                    cancelled_lc_success = stats.update_trade_cancelled(lc_id_to_cancel, reason=cancel_reason)
+                                    if cancelled_lc_success:
+                                        logger.info(f"🔗 Trade lifecycle {lc_id_to_cancel} also cancelled for disappeared entry order {exchange_oid}.")
+                                        if self.notification_manager:
+                                            await self.notification_manager.send_generic_notification(
+                                                f"�� <b>Trade Lifecycle Cancelled</b>\n\n"
+                                                f"Token: {token}\n"
+                                                f"Lifecycle ID: {lc_id_to_cancel[:8]}...\n"
+                                                f"Reason: Entry order {exchange_oid[:8]}... cancelled externally.\n"
+                                                f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}"
+                                            )
+                                    else:
+                                        logger.error(f"❌ Failed to cancel trade lifecycle {lc_id_to_cancel} for entry order {exchange_oid}.")
+
+                        elif order_after_external_check and order_after_external_check.get('status') in ['filled', 'partially_filled']:
+                            logger.info(f"ℹ️ Order {exchange_oid} was ultimately found to be '{order_after_external_check.get('status')}' despite initial disappearance. Stop losses will not be cancelled by this path. Lifecycle should be active.")
+                            continue
+
+                    else:
+                        if last_status not in ['filled', 'partially_filled', 'cancelled_manually', 'cancelled_by_bot', 'failed_submission', 'cancelled_externally']:
+                            stats.update_order_status(exchange_order_id=exchange_oid, new_status='disappeared_from_exchange')
+                        
+                    if order_in_db.get('bot_order_ref_id'):
+                        parent_order_current_state = stats.get_order_by_exchange_id(exchange_oid)
+                        if parent_order_current_state and parent_order_current_state.get('status') not in ['filled', 'partially_filled']:
+                            logger.info(f"Cancelling stop losses for order {exchange_oid} (status: {parent_order_current_state.get('status')}) as it is not considered filled.")
+                            cancelled_sl_count = stats.cancel_linked_orders(
+                                parent_bot_order_ref_id=order_in_db['bot_order_ref_id'],
+                                new_status='cancelled_parent_disappeared_or_not_filled'
+                            )
+                            total_linked_cancelled += cancelled_sl_count
+                        
+                            if cancelled_sl_count > 0:
+                                logger.info(f"Cancelled {cancelled_sl_count} pending stop losses linked to disappeared/non-filled order {exchange_oid}")
+                                if self.notification_manager:
+                                    await self.notification_manager.send_generic_notification(
+                                        f"🛑 <b>Linked Stop Losses Cancelled</b>\n\n"
+                                        f"Token: {token}\n"
+                                        f"Cancelled: {cancelled_sl_count} stop loss(es)\n"
+                                        f"Reason: Parent order {exchange_oid[:8]}... disappeared or was not filled\n"
+                                        f"Parent Status: {parent_order_current_state.get('status', 'N/A').replace('_', ' ').title()}\n"
+                                        f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}"
+                                    )
+                        else:
+                            logger.info(f"ℹ️ Stop losses for order {exchange_oid} (status: {parent_order_current_state.get('status') if parent_order_current_state else 'N/A'}) preserved as parent order is considered filled or partially filled.")
+                            continue
+                else:
+                    logger.warning(f"Order {exchange_oid} disappeared from exchange but was not found in our DB. This might be an order placed externally.")
+
+            if len(external_cancellations) > 1:
+                tokens_affected = list(set(item['token'] for item in external_cancellations))
+                if self.notification_manager:
+                    await self.notification_manager.send_generic_notification(
+                        f"⚠️ <b>Multiple External Cancellations Detected</b>\n\n"
+                        f"Orders Cancelled: {len(external_cancellations)}\n"
+                        f"Tokens Affected: {', '.join(tokens_affected)}\n"
+                        f"Source: Direct cancellation on Hyperliquid\n"
+                        f"Linked Stop Losses Cancelled: {total_linked_cancelled}\n"
+                        f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}\n\n"
+                        f"💡 Check individual orders for details"
+                    )
+        except Exception as e:
+            logger.error(f"❌ Error processing disappeared orders: {e}", exc_info=True)
+
+    async def _check_for_recent_fills_for_order(self, exchange_oid, order_in_db):
+        """Check for very recent fills that might match this order."""
+        # This method checks for fills that might have occurred *just before* an order disappeared,
+        # but *before* the main _check_external_trades might have run for the current cycle.
+        # It uses its own tracking of last_processed_trade_time or a default if not available.
+        try:
+            recent_fills = self.trading_engine.get_recent_fills()
+            if not recent_fills:
+                return False
+
+            # This last_processed_trade_time is for the context of this specific helper,
+            # to avoid re-checking fills that the broader external trade monitor might have already seen.
+            # It attempts to use the global one if available.
+            # The key 'last_processed_trade_time' might be distinct from 'market_monitor_last_processed_trade_time'.
+            # For safety, let's ensure this is consistently named if it's meant to be the same.
+            # Given it's a helper within OrderFillProcessor, and external trades are separate,
+            # we will keep its independent loading logic for now.
+            # If MarketMonitor centralizes this timestamp, this should be updated.
+            
+            # Attempt to load the specific last_processed_trade_time if not already set on this instance
+            # This implies self.last_processed_trade_time is an attribute of OrderFillProcessor
+            if not hasattr(self, 'last_processed_trade_time_helper') or self.last_processed_trade_time_helper is None:
+                try:
+                    # Using a distinct metadata key for this helper to avoid conflict,
+                    # or assuming it should use the global one. For now, let's assume it tries to use the global.
+                    last_time_str = self.trading_engine.stats._get_metadata('market_monitor_last_processed_trade_time')
+                    if last_time_str:
+                        self.last_processed_trade_time_helper = datetime.fromisoformat(last_time_str).replace(tzinfo=timezone.utc)
+                    else:
+                        self.last_processed_trade_time_helper = datetime.now(timezone.utc) - timedelta(hours=1)
+                except Exception: 
+                     self.last_processed_trade_time_helper = datetime.now(timezone.utc) - timedelta(hours=1)
+
+
+            for fill in recent_fills:
+                try:
+                    trade_id = fill.get('id')
+                    timestamp_ms = fill.get('timestamp')
+                    symbol_from_fill = fill.get('symbol')
+                    side_from_fill = fill.get('side')
+                    amount_from_fill = float(fill.get('amount', 0))
+                    price_from_fill = float(fill.get('price', 0))
+                    
+                    timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) if timestamp_ms else datetime.now(timezone.utc)
+                    
+                    if timestamp_dt <= self.last_processed_trade_time_helper:
+                        continue
+                    
+                    if symbol_from_fill and side_from_fill and amount_from_fill > 0 and price_from_fill > 0:
+                        exchange_order_id_from_fill = fill.get('info', {}).get('oid')
+                        
+                        if exchange_order_id_from_fill == exchange_oid:
+                            if order_in_db.get('symbol') == symbol_from_fill and \
+                               order_in_db.get('side') == side_from_fill and \
+                               abs(float(order_in_db.get('amount_requested', 0)) - amount_from_fill) < 0.01 * amount_from_fill :
+                                logger.info(f"✅ Found recent matching fill {trade_id} for order {exchange_oid}. Not cancelling stop losses.")
+                                # This fill should be processed by the main external trade checker.
+                                # For the purpose of this helper, just confirming a fill exists is enough.
+                                return True
+                
+                except Exception as e:
+                    logger.error(f"Error processing fill {fill.get('id','N/A')} in _check_for_recent_fills_for_order: {e}")
+                    continue
+            
+            return False
+
+        except Exception as e:
+            logger.error(f"❌ Error in _check_for_recent_fills_for_order for OID {exchange_oid}: {e}", exc_info=True)
+            return False 
+
+    async def _activate_pending_stop_losses_from_trades(self):
+        """🆕 PHASE 4: Check trades table for pending stop loss activation first (highest priority)"""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                return
+            
+            formatter = get_formatter() 
+            trades_needing_sl = stats.get_pending_stop_loss_activations()
+            
+            if not trades_needing_sl:
+                return
+            
+            logger.debug(f"🆕 Found {len(trades_needing_sl)} open positions needing stop loss activation")
+            
+            for position_trade in trades_needing_sl:
+                try:
+                    symbol = position_trade['symbol']
+                    token = symbol.split('/')[0] if symbol and '/' in symbol else (symbol if symbol else "TOKEN")
+                    stop_loss_price = position_trade['stop_loss_price']
+                    position_side = position_trade['position_side'] 
+                    current_amount = position_trade.get('current_position_size', 0)
+                    lifecycle_id = position_trade['trade_lifecycle_id']
+
+                    if not all([symbol, stop_loss_price, position_side, abs(current_amount) > 1e-9, lifecycle_id]):
+                        logger.warning(f"Skipping SL activation for lifecycle {lifecycle_id} due to incomplete data: sym={symbol}, sl_price={stop_loss_price}, side={position_side}, amt={current_amount}")
+                        continue
+                    
+                    logger.info(f"Attempting to place LIMIT stop loss for lifecycle {lifecycle_id} ({position_side} {token} @ SL {formatter.format_price(stop_loss_price, symbol)})")
+                    sl_result = await self.trading_engine.place_limit_stop_for_lifecycle(
+                        lifecycle_id=lifecycle_id,
+                        symbol=symbol,
+                        sl_price=stop_loss_price,
+                        position_side=position_side,
+                        amount_to_cover=abs(current_amount) 
+                    )
+                        
+                    if sl_result.get('success'):
+                        placed_sl_details = sl_result.get('order_placed_details', {})
+                        sl_exchange_order_id = placed_sl_details.get('exchange_order_id')
+                        sl_db_order_id = placed_sl_details.get('order_db_id')
+                        stop_loss_price_str_log = formatter.format_price_with_symbol(stop_loss_price, token)
+
+                        logger.info(f"✅ Successfully processed SL request for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price {stop_loss_price_str_log}, Exchange SL Order ID: {sl_exchange_order_id or 'N/A'}, DB ID: {sl_db_order_id or 'N/A'}")
+                        
+                        if self.notification_manager and sl_exchange_order_id:
+                            current_price_for_notification = None
+                            try:
+                                market_data_notify = self.trading_engine.get_market_data(symbol)
+                                if market_data_notify and market_data_notify.get('ticker'):
+                                    current_price_for_notification = float(market_data_notify['ticker'].get('last', 0))
+                            except:
+                                pass 
+
+                            current_price_str_notify = formatter.format_price_with_symbol(current_price_for_notification, token) if current_price_for_notification else 'Unknown'
+                            stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token)
+                            
+                            await self.notification_manager.send_generic_notification(
+                                f"🛡️ <b>Stop Loss LIMIT Order Placed</b>\n\n"
+                                f"Token: {token}\n"
+                                f"Lifecycle ID: {lifecycle_id[:8]}...\n"
+                                f"Position Type: {position_side.upper()}\n"
+                                f"Stop Loss Price: {stop_loss_price_str_notify}\n"
+                                f"Amount: {formatter.format_amount(abs(current_amount), token)}\n"
+                                f"Current Price: {current_price_str_notify}\n"
+                                f"Exchange SL Order ID: {sl_exchange_order_id}\n"
+                                f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}"
+                            )
+                        elif not sl_exchange_order_id:
+                             logger.warning(f"SL Limit order for {token} (Lifecycle: {lifecycle_id[:8]}) placed in DB (ID: {sl_db_order_id}) but no exchange ID returned immediately.")
+
+                    else:
+                        logger.error(f"❌ Failed to place SL limit order for {token} (Lifecycle: {lifecycle_id[:8]}): {sl_result.get('error')}")
+
+                except Exception as trade_error:
+                    logger.error(f"❌ Error processing position trade for SL activation (Lifecycle: {position_trade.get('trade_lifecycle_id','N/A')}): {trade_error}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error activating pending stop losses from trades table: {e}", exc_info=True) 

+ 487 - 0
src/monitoring/position_synchronizer.py

@@ -0,0 +1,487 @@
+#!/usr/bin/env python3
+"""
+Handles synchronization of bot's position state with the exchange.
+"""
+
+import logging
+import asyncio
+from datetime import datetime, timezone
+from typing import Optional, Dict, Any, List
+
+from src.utils.token_display_formatter import get_formatter
+
+logger = logging.getLogger(__name__)
+
+class PositionSynchronizer:
+    def __init__(self, trading_engine, notification_manager, market_monitor_cache):
+        self.trading_engine = trading_engine
+        self.notification_manager = notification_manager
+        self.market_monitor_cache = market_monitor_cache # To access cached orders/positions
+        # Add necessary initializations
+
+    # Methods like _auto_sync_orphaned_positions, _immediate_startup_auto_sync, _estimate_entry_price_for_orphaned_position will go here
+    pass 
+
+    async def _auto_sync_orphaned_positions(self):
+        """Automatically detect and sync orphaned positions (positions on exchange without trade lifecycle records)."""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                return
+
+            formatter = get_formatter()
+
+            exchange_positions = self.market_monitor_cache.cached_positions or [] # Use fresh cache from market_monitor_cache
+            synced_count = 0
+
+            for exchange_pos in exchange_positions:
+                symbol = exchange_pos.get('symbol')
+                contracts_abs = abs(float(exchange_pos.get('contracts', 0))) 
+                
+                if not (symbol and contracts_abs > 1e-9): # Ensure position is substantial
+                    continue
+
+                existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                
+                if not existing_trade:
+                    entry_price_from_exchange = float(exchange_pos.get('entryPrice', 0)) or float(exchange_pos.get('entryPx', 0))
+                    
+                    position_side, order_side = '', ''
+                    ccxt_side = exchange_pos.get('side', '').lower()
+                    if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
+                    elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
+                    
+                    if not position_side: 
+                        raw_info = exchange_pos.get('info', {}).get('position', {})
+                        if isinstance(raw_info, dict):
+                            szi_str = raw_info.get('szi')
+                            if szi_str is not None:
+                                try: szi_val = float(szi_str)
+                                except ValueError: szi_val = 0
+                                if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
+                                elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
+                    
+                    if not position_side: 
+                        contracts_val = float(exchange_pos.get('contracts',0))
+                        if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
+                        elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell' 
+                        else:
+                            logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
+                            continue
+                    
+                    if not position_side:
+                        logger.error(f"AUTO-SYNC: CRITICAL - Could not determine position side for {symbol}. Data: {exchange_pos}. Skipping.")
+                        continue
+
+                    token = symbol.split('/')[0] if '/' in symbol else symbol
+                    actual_contracts_size = contracts_abs
+
+                    final_entry_price = entry_price_from_exchange
+                    price_source_log = "(exchange data)"
+                    if not final_entry_price or final_entry_price <= 0:
+                        estimated_entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, actual_contracts_size, position_side)
+                        if estimated_entry_price > 0:
+                            final_entry_price = estimated_entry_price
+                            price_source_log = "(estimated)"
+                        else:
+                            logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
+                            continue
+                    
+                    logger.info(f"🔄 AUTO-SYNC: Orphaned position detected - {symbol} {position_side.upper()} {actual_contracts_size} @ ${final_entry_price:.4f} {price_source_log}")
+                    
+                    lifecycle_id = stats.create_trade_lifecycle(
+                        symbol=symbol, side=order_side, 
+                        entry_order_id=f"external_sync_{int(datetime.now(timezone.utc).timestamp())}",
+                        trade_type='external_sync'
+                    )
+                    
+                    if lifecycle_id:
+                        success = stats.update_trade_position_opened(
+                            lifecycle_id, final_entry_price, actual_contracts_size,
+                            f"external_fill_sync_{int(datetime.now(timezone.utc).timestamp())}"
+                        )
+                        
+                        if success:
+                            synced_count += 1
+                            logger.info(f"✅ AUTO-SYNC: Successfully synced orphaned position for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+                            
+                            if self.notification_manager:
+                                unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
+                                pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
+                                notification_text = (
+                                    f"🔄 <b>Position Auto-Synced</b>\n\n"
+                                    f"Token: {token}\n"
+                                    f"Lifecycle ID: {lifecycle_id[:8]}...\n"
+                                    f"Direction: {position_side.upper()}\n"
+                                    f"Size: {actual_contracts_size:.6f} {token}\n"
+                                    f"Entry Price: ${final_entry_price:,.4f} {price_source_log}\n"
+                                    f"{pnl_emoji} P&L (Unrealized): ${unrealized_pnl:,.2f}\n"
+                                    f"Reason: Position found on exchange without bot record.\n"
+                                    f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}\n\n"
+                                    f"✅ Position now tracked. Use /sl or /tp if needed."
+                                )
+                                await self.notification_manager.send_generic_notification(notification_text)
+                        else:
+                            logger.error(f"❌ AUTO-SYNC: Failed to update lifecycle to 'position_opened' for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+                    else:
+                        logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for orphaned position {symbol}.")
+
+            if synced_count > 0:
+                logger.info(f"🔄 AUTO-SYNC: Synced {synced_count} orphaned position(s) this cycle (Exchange had position, Bot did not).")
+
+            bot_open_lifecycles = stats.get_trades_by_status('position_opened')
+            if not bot_open_lifecycles:
+                return 
+
+            current_exchange_positions_map = {}
+            for ex_pos in (self.market_monitor_cache.cached_positions or []): # Use fresh cache from market_monitor_cache
+                if ex_pos.get('symbol') and abs(float(ex_pos.get('contracts', 0))) > 1e-9:
+                    current_exchange_positions_map[ex_pos.get('symbol')] = ex_pos
+            
+            closed_due_to_discrepancy = 0
+            for lc in bot_open_lifecycles:
+                symbol = lc.get('symbol')
+                lc_id = lc.get('trade_lifecycle_id')
+                token = symbol.split('/')[0] if '/' in symbol else symbol
+
+                if symbol not in current_exchange_positions_map:
+                    logger.warning(f"🔄 AUTO-SYNC (Discrepancy): Bot lifecycle {lc_id} for {symbol} is 'position_opened', but NO position found on exchange. Closing lifecycle.")
+                    
+                    entry_price = lc.get('entry_price', 0)
+                    position_side = lc.get('position_side')
+                    position_size_for_pnl = lc.get('current_position_size', 0)
+                    exit_price_for_calc = 0
+                    price_source_info = "unknown"
+
+                    try:
+                        all_recent_fills = self.trading_engine.get_recent_fills()
+                        if all_recent_fills:
+                            symbol_specific_fills = [f for f in all_recent_fills if f.get('symbol') == symbol]
+                            if symbol_specific_fills:
+                                closing_side = 'sell' if position_side == 'long' else 'buy'
+                                relevant_fills = sorted(
+                                    [f for f in symbol_specific_fills if f.get('side') == closing_side],
+                                    key=lambda f: f.get('timestamp'), reverse=True
+                                )
+                                if relevant_fills:
+                                    last_closing_fill = relevant_fills[0]
+                                    exit_price_for_calc = float(last_closing_fill.get('price', 0))
+                                    fill_timestamp = datetime.fromtimestamp(last_closing_fill.get('timestamp')/1000, tz=timezone.utc).isoformat() if last_closing_fill.get('timestamp') else "N/A"
+                                    price_source_info = f"(last exchange fill ({formatter.format_price(exit_price_for_calc, symbol)} @ {fill_timestamp}))"
+                                    logger.info(f"AUTO-SYNC: Using exit price {price_source_info} for {symbol} lifecycle {lc_id}.")
+                    except Exception as e:
+                        logger.warning(f"AUTO-SYNC: Error fetching recent fills for {symbol} to determine exit price: {e}")
+
+                    if not exit_price_for_calc or exit_price_for_calc <= 0:
+                        mark_price_from_lc = lc.get('mark_price')
+                        if mark_price_from_lc and float(mark_price_from_lc) > 0:
+                            exit_price_for_calc = float(mark_price_from_lc)
+                            price_source_info = "lifecycle mark_price"
+                            logger.info(f"AUTO-SYNC: No recent fill found. Using exit price from lifecycle mark_price: {formatter.format_price(exit_price_for_calc, symbol)} for {symbol} lifecycle {lc_id}.")
+                        else:
+                            exit_price_for_calc = entry_price
+                            price_source_info = "lifecycle entry_price (0 PNL)"
+                            logger.info(f"AUTO-SYNC: No recent fill or mark_price. Using entry_price: {formatter.format_price(exit_price_for_calc, symbol)} for {symbol} lifecycle {lc_id}.")
+                    
+                    realized_pnl = 0
+                    if position_side == 'long':
+                        realized_pnl = position_size_for_pnl * (exit_price_for_calc - entry_price)
+                    elif position_side == 'short':
+                        realized_pnl = position_size_for_pnl * (entry_price - exit_price_for_calc)
+                    
+                    success = stats.update_trade_position_closed(
+                        lifecycle_id=lc_id,
+                        exit_price=exit_price_for_calc, 
+                        realized_pnl=realized_pnl,
+                        exchange_fill_id=f"auto_sync_flat_{int(datetime.now(timezone.utc).timestamp())}"
+                    )
+                    
+                    if success:
+                        closed_due_to_discrepancy += 1
+                        logger.info(f"✅ AUTO-SYNC (Discrepancy): Successfully closed bot lifecycle {lc_id} for {symbol}.")
+                        stats._migrate_trade_to_aggregated_stats(lc_id)
+                        if self.notification_manager:
+                            pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                            notification_text = (
+                                f"🔄 <b>Position Auto-Closed (Discrepancy)</b>\n\n"
+                                f"Token: {token}\n"
+                                f"Lifecycle ID: {lc_id[:8]}...\n"
+                                f"Reason: Bot showed open position, but no corresponding position found on exchange.\n"
+                                f"{pnl_emoji} Realized P&L for this closure: {formatter.format_price_with_symbol(realized_pnl)}\n"
+                                f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}\n\n"
+                                f"ℹ️ Bot state synchronized with exchange."
+                            )
+                            await self.notification_manager.send_generic_notification(notification_text)
+                    else:
+                        logger.error(f"❌ AUTO-SYNC (Discrepancy): Failed to close bot lifecycle {lc_id} for {symbol}.")
+            
+            if closed_due_to_discrepancy > 0:
+                logger.info(f"🔄 AUTO-SYNC: Closed {closed_due_to_discrepancy} lifecycle(s) due to discrepancy (Bot had position, Exchange did not).")
+
+        except Exception as e:
+            logger.error(f"❌ Error in auto-sync orphaned positions: {e}", exc_info=True)
+
+    async def _estimate_entry_price_for_orphaned_position(self, symbol: str, contracts: float, side: str) -> float:
+        """Estimate entry price for an orphaned position by checking recent fills and market data."""
+        try:
+            entry_fill_side = 'buy' if side == 'long' else 'sell'
+            formatter = get_formatter()
+            token = symbol.split('/')[0] if '/' in symbol else symbol
+            all_recent_fills = self.trading_engine.get_recent_fills() 
+            recent_fills = [f for f in all_recent_fills if f.get('symbol') == symbol] 
+
+            if recent_fills:
+                symbol_side_fills = [
+                    fill for fill in recent_fills 
+                    if fill.get('symbol') == symbol and fill.get('side') == entry_fill_side and float(fill.get('amount',0)) > 0
+                ]
+                if symbol_side_fills:
+                    symbol_side_fills.sort(key=lambda f: (
+                        datetime.fromtimestamp(f.get('timestamp') / 1000, tz=timezone.utc) if f.get('timestamp') else datetime.min.replace(tzinfo=timezone.utc),
+                        abs(float(f.get('amount',0)) - contracts)
+                        ), reverse=True)
+                    
+                    best_fill = symbol_side_fills[0]
+                    fill_price = float(best_fill.get('price', 0))
+                    fill_amount = float(best_fill.get('amount', 0))
+                    if fill_price > 0:
+                        logger.info(f"💡 AUTO-SYNC: Estimated entry for {side} {symbol} via recent {entry_fill_side} fill: {formatter.format_price_with_symbol(fill_price, token)} (Amount: {formatter.format_amount(fill_amount, token)})")
+                        return fill_price
+            
+            market_data = self.trading_engine.get_market_data(symbol)
+            if market_data and market_data.get('ticker'):
+                current_price = float(market_data['ticker'].get('last', 0))
+                if current_price > 0:
+                    logger.warning(f"⚠️ AUTO-SYNC: Using current market price as entry estimate for {side} {symbol}: {formatter.format_price_with_symbol(current_price, token)}")
+                    return current_price
+            
+            if market_data and market_data.get('ticker'):
+                bid = float(market_data['ticker'].get('bid', 0))
+                ask = float(market_data['ticker'].get('ask', 0))
+                if bid > 0 and ask > 0: return (bid + ask) / 2
+
+            logger.warning(f"AUTO-SYNC: Could not estimate entry price for {side} {symbol} through any method.")
+            return 0.0
+        except Exception as e:
+            logger.error(f"❌ Error estimating entry price for orphaned position {symbol}: {e}", exc_info=True)
+            return 0.0
+
+    async def _immediate_startup_auto_sync(self):
+        """🆕 Immediately check for and sync orphaned positions on startup."""
+        try:
+            logger.info("🔍 STARTUP: Checking for orphaned positions...")
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                logger.warning("⚠️ STARTUP: TradingStats not available for auto-sync.")
+                return
+
+            formatter = get_formatter()
+            exchange_positions = self.trading_engine.get_positions() or []
+            if not exchange_positions:
+                logger.info("✅ STARTUP: No positions found on exchange.")
+                return
+                
+            synced_count = 0
+            for exchange_pos in exchange_positions:
+                symbol = exchange_pos.get('symbol')
+                contracts_abs = abs(float(exchange_pos.get('contracts', 0)))
+                token_for_log = symbol.split('/')[0] if symbol and '/' in symbol else symbol
+                
+                if not (symbol and contracts_abs > 1e-9): continue
+
+                existing_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                if not existing_trade_lc:
+                    position_side, order_side = '', ''
+                    ccxt_side = exchange_pos.get('side', '').lower()
+                    if ccxt_side == 'long': position_side, order_side = 'long', 'buy'
+                    elif ccxt_side == 'short': position_side, order_side = 'short', 'sell'
+                    
+                    if not position_side:
+                        raw_info = exchange_pos.get('info', {}).get('position', {})
+                        if isinstance(raw_info, dict):
+                            szi_str = raw_info.get('szi')
+                            if szi_str is not None:
+                                try: szi_val = float(szi_str)
+                                except ValueError: szi_val = 0
+                                if szi_val > 1e-9: position_side, order_side = 'long', 'buy'
+                                elif szi_val < -1e-9: position_side, order_side = 'short', 'sell'
+                    
+                    if not position_side:
+                        contracts_val = float(exchange_pos.get('contracts',0))
+                        if contracts_val > 1e-9: position_side, order_side = 'long', 'buy'
+                        elif contracts_val < -1e-9: position_side, order_side = 'short', 'sell'
+                        else:
+                            logger.warning(f"AUTO-SYNC: Position size is effectively 0 for {symbol} after side checks, skipping sync. Data: {exchange_pos}")
+                            continue
+                    
+                    if not position_side:
+                        logger.error(f"AUTO-SYNC: CRITICAL - Could not determine position side for {symbol}. Data: {exchange_pos}. Skipping.")
+                        continue
+
+                    entry_price = float(exchange_pos.get('entryPrice', 0)) or float(exchange_pos.get('entryPx', 0))
+                    price_source_log = "(exchange data)"
+                    if not entry_price or entry_price <= 0:
+                        estimated_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts_abs, position_side)
+                        if estimated_price > 0: 
+                            entry_price = estimated_price
+                            price_source_log = "(estimated)"
+                        else:
+                            logger.error(f"AUTO-SYNC: Could not determine/estimate entry price for {symbol}. Skipping sync.")
+                            continue
+                    
+                    logger.info(f"🔄 STARTUP: Auto-syncing orphaned position: {symbol} {position_side.upper()} {formatter.format_amount(contracts_abs, token_for_log)} @ {formatter.format_price_with_symbol(entry_price, token_for_log)} {price_source_log}")
+                    
+                    lifecycle_id = stats.create_trade_lifecycle(
+                        symbol=symbol, side=order_side,
+                        entry_order_id=f"startup_sync_{int(datetime.now(timezone.utc).timestamp())}",
+                        trade_type='external_startup_sync'
+                    )
+                    
+                    if lifecycle_id:
+                        success = stats.update_trade_position_opened(
+                            lifecycle_id, entry_price, contracts_abs,
+                            f"startup_fill_sync_{int(datetime.now(timezone.utc).timestamp())}"
+                        )
+                        if success:
+                            synced_count += 1
+                            logger.info(f"✅ STARTUP: Successfully synced orphaned position for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+                            await self._send_startup_auto_sync_notification(exchange_pos, symbol, position_side, contracts_abs, entry_price, lifecycle_id, price_source_log)
+                        else: 
+                            logger.error(f"❌ STARTUP: Failed to update lifecycle for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+                    else: 
+                        logger.error(f"❌ STARTUP: Failed to create lifecycle for {symbol}.")
+            
+            if synced_count == 0 and exchange_positions:
+                 logger.info("✅ STARTUP: All existing exchange positions are already tracked.")
+            elif synced_count > 0:
+                 logger.info(f"🎉 STARTUP: Auto-synced {synced_count} orphaned position(s) (Exchange had pos, Bot did not).")
+
+            logger.info("🔍 STARTUP: Checking for discrepancies (Bot has pos, Exchange does not)...")
+            bot_open_lifecycles = stats.get_trades_by_status('position_opened')
+            
+            current_exchange_positions_map = {}
+            for ex_pos in (exchange_positions or []): 
+                if ex_pos.get('symbol') and abs(float(ex_pos.get('contracts', 0))) > 1e-9:
+                    current_exchange_positions_map[ex_pos.get('symbol')] = ex_pos
+
+            closed_due_to_discrepancy_startup = 0
+            if bot_open_lifecycles:
+                for lc in bot_open_lifecycles:
+                    symbol = lc.get('symbol')
+                    lc_id = lc.get('trade_lifecycle_id')
+                    token_for_log_discrepancy = symbol.split('/')[0] if symbol and '/' in symbol else symbol
+
+                    if symbol not in current_exchange_positions_map:
+                        logger.warning(f"🔄 STARTUP (Discrepancy): Bot lifecycle {lc_id} for {symbol} is 'position_opened', but NO position found on exchange. Closing lifecycle.")
+                        
+                        entry_price_lc = lc.get('entry_price', 0) 
+                        position_side_lc = lc.get('position_side') 
+                        position_size_for_pnl = lc.get('current_position_size', 0)
+                        exit_price_for_calc = 0
+                        price_source_info = "unknown"
+
+                        try:
+                            all_recent_fills_for_startup_sync = self.trading_engine.get_recent_fills()
+                            if all_recent_fills_for_startup_sync:
+                                symbol_specific_fills_startup = [f for f in all_recent_fills_for_startup_sync if f.get('symbol') == symbol]
+                                if symbol_specific_fills_startup:
+                                    closing_side = 'sell' if position_side_lc == 'long' else 'buy' 
+                                    relevant_fills = sorted(
+                                        [f for f in symbol_specific_fills_startup if f.get('side') == closing_side],
+                                        key=lambda f: f.get('timestamp'), reverse=True
+                                    )
+                                    if relevant_fills:
+                                        last_closing_fill = relevant_fills[0]
+                                        exit_price_for_calc = float(last_closing_fill.get('price', 0))
+                                        fill_ts_val = last_closing_fill.get('timestamp')
+                                        fill_timestamp_str = datetime.fromtimestamp(fill_ts_val/1000, tz=timezone.utc).isoformat() if fill_ts_val else "N/A"
+                                        price_source_info = f"(last exchange fill ({formatter.format_price(exit_price_for_calc, symbol)} @ {fill_timestamp_str}))"
+                                        logger.info(f"STARTUP SYNC: Using exit price {price_source_info} for {symbol} lifecycle {lc_id}.")
+                        except Exception as e:
+                            logger.warning(f"STARTUP SYNC: Error fetching recent fills for {symbol} to determine exit price: {e}")
+
+                        if not exit_price_for_calc or exit_price_for_calc <= 0:
+                            mark_price_from_lc = lc.get('mark_price')
+                            if mark_price_from_lc and float(mark_price_from_lc) > 0:
+                                exit_price_for_calc = float(mark_price_from_lc)
+                                price_source_info = "lifecycle mark_price"
+                            else:
+                                exit_price_for_calc = entry_price_lc 
+                                price_source_info = "lifecycle entry_price (0 PNL)"
+                        
+                        realized_pnl = 0
+                        if position_side_lc == 'long': 
+                            realized_pnl = position_size_for_pnl * (exit_price_for_calc - entry_price_lc) 
+                        elif position_side_lc == 'short': 
+                            realized_pnl = position_size_for_pnl * (entry_price_lc - exit_price_for_calc) 
+                        
+                        success_close = stats.update_trade_position_closed(
+                            lifecycle_id=lc_id,
+                            exit_price=exit_price_for_calc, 
+                            realized_pnl=realized_pnl,
+                            exchange_fill_id=f"startup_sync_flat_{int(datetime.now(timezone.utc).timestamp())}"
+                        )
+                        
+                        if success_close:
+                            closed_due_to_discrepancy_startup += 1
+                            logger.info(f"✅ STARTUP (Discrepancy): Successfully closed bot lifecycle {lc_id} for {symbol}.")
+                            stats._migrate_trade_to_aggregated_stats(lc_id)
+                            if self.notification_manager:
+                                pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                                notification_text = (
+                                    f"🔄 <b>Position Auto-Closed (Startup Sync)</b>\n\n"
+                                    f"Token: {token_for_log_discrepancy}\n"
+                                    f"Lifecycle ID: {lc_id[:8]}...\n"
+                                    f"Reason: Bot startup - found open lifecycle, but no corresponding position on exchange.\n"
+                                    f"Assumed Exit Price: {formatter.format_price(exit_price_for_calc, symbol)} (Source: {price_source_info})\n"
+                                    f"{pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(realized_pnl)}\n"
+                                    f"Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}"
+                                )
+                                await self.notification_manager.send_generic_notification(notification_text)
+                        else:
+                            logger.error(f"❌ STARTUP (Discrepancy): Failed to close bot lifecycle {lc_id} for {symbol}.")
+            
+            if closed_due_to_discrepancy_startup > 0:
+                logger.info(f"🎉 STARTUP: Auto-closed {closed_due_to_discrepancy_startup} lifecycle(s) due to discrepancy (Bot had pos, Exchange did not).")
+            else:
+                logger.info("✅ STARTUP: No discrepancies found where bot had position and exchange did not.")
+                
+        except Exception as e:
+            logger.error(f"❌ Error in startup auto-sync: {e}", exc_info=True)
+
+    async def _send_startup_auto_sync_notification(self, exchange_pos, symbol, position_side, contracts, entry_price, lifecycle_id, price_source_log):
+        """Send notification for positions auto-synced on startup."""
+        try:
+            if not self.notification_manager: return
+
+            formatter = get_formatter()
+            token = symbol.split('/')[0] if '/' in symbol else symbol
+            unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
+            pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
+            
+            size_str = formatter.format_amount(contracts, token)
+            entry_price_str = formatter.format_price_with_symbol(entry_price, token)
+            pnl_str = formatter.format_price_with_symbol(unrealized_pnl)
+
+            notification_text_parts = [
+                f"🚨 <b>Bot Startup: Position Auto-Synced</b>\n",
+                f"Token: {token}",
+                f"Lifecycle ID: {lifecycle_id[:8]}...",
+                f"Direction: {position_side.upper()}",
+                f"Size: {size_str} {token}",
+                f"Entry Price: {entry_price_str} {price_source_log}",
+                f"{pnl_emoji} P&L (Unrealized): {pnl_str}",
+                f"Reason: Position found on exchange without bot record.",
+                "\n✅ Position now tracked. Use /sl or /tp if needed."
+            ]
+            
+            liq_price = float(exchange_pos.get('liquidationPrice', 0))
+            if liq_price > 0: 
+                liq_price_str = formatter.format_price_with_symbol(liq_price, token)
+                notification_text_parts.append(f"⚠️ Liquidation: {liq_price_str}")
+            
+            await self.notification_manager.send_generic_notification("\n".join(notification_text_parts))
+            logger.info(f"📤 STARTUP: Sent auto-sync notification for {symbol} (Lifecycle: {lifecycle_id[:8]}).")
+            
+        except Exception as e:
+            logger.error(f"❌ STARTUP: Failed to send auto-sync notification for {symbol}: {e}") 

+ 501 - 0
src/monitoring/risk_cleanup_manager.py

@@ -0,0 +1,501 @@
+#!/usr/bin/env python3
+"""
+Handles risk management checks and cleanup routines for orphaned orders.
+"""
+
+import logging
+import asyncio
+from datetime import datetime, timezone
+from typing import Optional, Dict, Any, List
+
+from src.config.config import Config # For STOP_LOSS_PERCENTAGE
+from src.utils.token_display_formatter import get_formatter
+
+logger = logging.getLogger(__name__)
+
+class RiskCleanupManager:
+    def __init__(self, trading_engine, notification_manager, market_monitor_cache):
+        self.trading_engine = trading_engine
+        self.notification_manager = notification_manager
+        self.market_monitor_cache = market_monitor_cache # To access cached orders/positions
+        self.external_stop_losses: Dict[str, Dict[str, Any]] = {} # exchange_order_id -> details
+
+    # Methods like _check_automatic_risk_management, _cleanup_orphaned_stop_losses,
+    # _check_external_stop_loss_orders, _cleanup_external_stop_loss_tracking,
+    # _check_pending_triggers, and the new _cleanup_orphaned_pending_sl_activations will go here
+    pass 
+
+    async def _check_pending_triggers(self):
+        """Check and process pending conditional triggers (e.g., SL/TP)."""
+        stats = self.trading_engine.get_stats()
+        if not stats:
+            logger.warning("⚠️ TradingStats not available in _check_pending_triggers.")
+            return
+
+        try:
+            pending_sl_triggers = stats.get_orders_by_status(status='pending_trigger', order_type_filter='stop_limit_trigger')
+            
+            if not pending_sl_triggers:
+                return
+
+            logger.debug(f"Found {len(pending_sl_triggers)} pending SL triggers to check.")
+
+            for trigger_order in pending_sl_triggers:
+                symbol = trigger_order['symbol']
+                trigger_price = trigger_order['price']
+                trigger_side = trigger_order['side']
+                order_db_id = trigger_order['id']
+                parent_ref_id = trigger_order.get('parent_bot_order_ref_id')
+
+                if not symbol or trigger_price is None:
+                    logger.warning(f"Invalid trigger order data for DB ID {order_db_id}, skipping: {trigger_order}")
+                    continue
+
+                market_data = self.trading_engine.get_market_data(symbol)
+                if not market_data or not market_data.get('ticker'):
+                    logger.warning(f"Could not fetch market data for {symbol} to check SL trigger {order_db_id}.")
+                    continue
+                
+                current_price = float(market_data['ticker'].get('last', 0))
+                if current_price <= 0:
+                    logger.warning(f"Invalid current price ({current_price}) for {symbol} checking SL trigger {order_db_id}.")
+                    continue
+
+                trigger_hit = False
+                if trigger_side.lower() == 'sell' and current_price <= trigger_price:
+                    trigger_hit = True
+                    logger.info(f"🔴 SL TRIGGER HIT (Sell): Order DB ID {order_db_id}, Symbol {symbol}, Trigger@ ${trigger_price:.4f}, Market@ ${current_price:.4f}")
+                elif trigger_side.lower() == 'buy' and current_price >= trigger_price:
+                    trigger_hit = True
+                    logger.info(f"🟢 SL TRIGGER HIT (Buy): Order DB ID {order_db_id}, Symbol {symbol}, Trigger@ ${trigger_price:.4f}, Market@ ${current_price:.4f}")
+                
+                if trigger_hit:
+                    logger.info(f"Attempting to execute actual stop order for triggered DB ID: {order_db_id} (Parent Bot Ref: {trigger_order.get('parent_bot_order_ref_id')})")
+                    execution_result = await self.trading_engine.execute_triggered_stop_order(original_trigger_order_db_id=order_db_id)
+                    notification_message_detail = ""
+
+                    if execution_result.get("success"):
+                        new_trigger_status = 'triggered_order_placed'
+                        placed_sl_details = execution_result.get("placed_sl_order_details", {})
+                        logger.info(f"Successfully placed actual SL order from trigger {order_db_id}. New SL Order DB ID: {placed_sl_details.get('order_db_id')}, Exchange ID: {placed_sl_details.get('exchange_order_id')}")
+                        notification_message_detail = f"Actual SL order placed (New DB ID: {placed_sl_details.get('order_db_id', 'N/A')})."
+                    else:
+                        new_trigger_status = 'trigger_execution_failed'
+                        error_msg = execution_result.get("error", "Unknown error during SL execution.")
+                        logger.error(f"Failed to execute actual SL order from trigger {order_db_id}: {error_msg}")
+                        notification_message_detail = f"Failed to place actual SL order: {error_msg}"
+
+                    stats.update_order_status(order_db_id=order_db_id, new_status=new_trigger_status)
+                    
+                    if self.notification_manager:
+                        await self.notification_manager.send_generic_notification(
+                            f"🔔 Stop-Loss Update!\\nSymbol: {symbol}\\nSide: {trigger_side.upper()}\\nTrigger Price: ${trigger_price:.4f}\\nMarket Price: ${current_price:.4f}\\n(Original Trigger DB ID: {order_db_id}, Parent: {parent_ref_id or 'N/A'})\\nStatus: {new_trigger_status.replace('_', ' ').title()}\\nDetails: {notification_message_detail}"
+                        )
+        except Exception as e:
+            logger.error(f"❌ Error checking pending SL triggers: {e}", exc_info=True)
+
+    async def _check_automatic_risk_management(self):
+        """Check for automatic stop loss triggers based on Config.STOP_LOSS_PERCENTAGE as safety net."""
+        try:
+            if not getattr(Config, 'RISK_MANAGEMENT_ENABLED', True) or Config.STOP_LOSS_PERCENTAGE <= 0:
+                return
+
+            positions = self.market_monitor_cache.cached_positions or []
+            if not positions:
+                await self._cleanup_orphaned_stop_losses() # Call within class
+                return
+
+            for position in positions:
+                try:
+                    symbol = position.get('symbol', '')
+                    contracts = float(position.get('contracts', 0))
+                    entry_price = float(position.get('entryPx', 0))
+                    mark_price = float(position.get('markPx', 0))
+                    unrealized_pnl = float(position.get('unrealizedPnl', 0))
+                    
+                    if contracts == 0 or entry_price <= 0 or mark_price <= 0:
+                        continue
+
+                    entry_value = abs(contracts) * entry_price
+                    if entry_value <= 0:
+                        continue
+                        
+                    pnl_percentage = (unrealized_pnl / entry_value) * 100
+
+                    if pnl_percentage <= -Config.STOP_LOSS_PERCENTAGE:
+                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                        position_side = "LONG" if contracts > 0 else "SHORT"
+                        stats = self.trading_engine.get_stats()
+                        lifecycle_id_str = "N/A"
+                        if stats:
+                            active_trade_lc = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                            if active_trade_lc:
+                                lifecycle_id_str = active_trade_lc.get('trade_lifecycle_id', "N/A")[:8] + "..."
+
+                        logger.warning(f"🚨 AUTOMATIC STOP LOSS TRIGGERED: {token} {position_side} position (Lifecycle: {lifecycle_id_str}) has {pnl_percentage:.2f}% loss (threshold: -{Config.STOP_LOSS_PERCENTAGE}%)")
+                        
+                        if self.notification_manager:
+                            await self.notification_manager.send_generic_notification(
+                                f"""🚨 AUTOMATIC STOP LOSS TRIGGERED!\\n
+Token: {token}\\n
+Lifecycle ID: {lifecycle_id_str}\\n
+Position: {position_side} {abs(contracts):.6f}\\n
+Entry Price: ${entry_price:.4f}\\n
+Current Price: ${mark_price:.4f}\\n
+Unrealized P&L: ${unrealized_pnl:.2f} ({pnl_percentage:.2f}%)\\n
+Safety Threshold: -{Config.STOP_LOSS_PERCENTAGE}%\\n
+Action: Executing emergency exit order..."""
+                            )
+
+                        exit_result = await self.trading_engine.execute_exit_order(token)
+                        
+                        if exit_result.get('success'):
+                            placed_order_details = exit_result.get('order_placed_details', {})
+                            logger.info(f"✅ Emergency exit order placed for {token} (Lifecycle: {lifecycle_id_str}). Order details: {placed_order_details}")
+                            if stats:
+                                cancelled_sl_count = stats.cancel_pending_stop_losses_by_symbol(
+                                    symbol=symbol,
+                                    new_status='cancelled_auto_exit'
+                                )
+                                if cancelled_sl_count > 0:
+                                    logger.info(f"🛑 Cancelled {cancelled_sl_count} pending stop losses for {symbol} (Lifecycle: {lifecycle_id_str}) after automatic exit")
+                            
+                            if self.notification_manager:
+                                await self.notification_manager.send_generic_notification(
+                                    f"""✅ <b>Emergency Exit Initiated</b>\\n\\n
+📊 <b>Position:</b> {token} {position_side}\\n
+🆔 <b>Lifecycle ID:</b> {lifecycle_id_str}\\n
+📉 <b>Loss at Trigger:</b> {pnl_percentage:.2f}% (${unrealized_pnl:.2f})\\n
+⚠️ <b>Threshold:</b> -{Config.STOP_LOSS_PERCENTAGE}%\\n
+✅ <b>Action:</b> Market exit order placed successfully\\n
+🆔 <b>Exit Order ID:</b> {placed_order_details.get('exchange_order_id', 'N/A')}\\n
+{f'🛑 <b>Cleanup:</b> Cancelled {cancelled_sl_count} other pending stop losses' if cancelled_sl_count > 0 else ''}
+\\n\\n
+🛡️ The system will confirm closure and P&L once the exit order fill is processed."""
+                                )
+                        else:
+                            error_msg = exit_result.get('error', 'Unknown error')
+                            logger.error(f"❌ Failed to execute emergency exit order for {token} (Lifecycle: {lifecycle_id_str}): {error_msg}")
+                            if self.notification_manager:
+                                await self.notification_manager.send_generic_notification(
+                                    f"""❌ <b>CRITICAL: Emergency Exit Failed!</b>\\n\\n
+📊 <b>Position:</b> {token} {position_side}\\n
+🆔 <b>Lifecycle ID:</b> {lifecycle_id_str}\\n
+📉 <b>Loss:</b> {pnl_percentage:.2f}%\\n
+❌ <b>Error Placing Order:</b> {error_msg}\\n\\n
+⚠️ <b>MANUAL INTERVENTION REQUIRED</b>\\n
+Please close this position manually via /exit {token}"""
+                                )
+                except Exception as pos_error:
+                    logger.error(f"Error processing position for automatic stop loss: {pos_error}")
+                    continue
+        except Exception as e:
+            logger.error(f"❌ Error in automatic risk management check: {e}", exc_info=True)
+
+    async def _cleanup_orphaned_stop_losses(self):
+        """Clean up pending stop losses that no longer have corresponding positions OR whose parent orders have been cancelled/failed."""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                return
+
+            pending_stop_losses = stats.get_orders_by_status('pending_trigger', 'stop_limit_trigger')
+            if not pending_stop_losses:
+                return
+
+            logger.debug(f"Checking {len(pending_stop_losses)} pending stop losses for orphaned orders")
+            current_positions = self.market_monitor_cache.cached_positions or [] 
+            position_symbols = set()
+            if current_positions:
+                for pos in current_positions:
+                    symbol = pos.get('symbol')
+                    contracts = float(pos.get('contracts', 0))
+                    if symbol and contracts != 0:
+                        position_symbols.add(symbol)
+
+            orphaned_count = 0
+            for sl_order in pending_stop_losses:
+                symbol = sl_order.get('symbol')
+                order_db_id = sl_order.get('id')
+                parent_bot_ref_id = sl_order.get('parent_bot_order_ref_id')
+                should_cancel = False
+                cancel_reason = ""
+                
+                if parent_bot_ref_id:
+                    parent_order = stats.get_order_by_bot_ref_id(parent_bot_ref_id)
+                    if parent_order:
+                        parent_status = parent_order.get('status', '').lower()
+                        if parent_order.get('exchange_order_id'):
+                            entry_oid = parent_order['exchange_order_id']
+                            lc_pending = stats.get_lifecycle_by_entry_order_id(entry_oid, status='pending')
+                            lc_cancelled = stats.get_lifecycle_by_entry_order_id(entry_oid, status='cancelled')
+                            lc_opened = stats.get_lifecycle_by_entry_order_id(entry_oid, status='position_opened')
+
+                            if parent_status == 'cancelled_externally':
+                                if lc_cancelled:
+                                    should_cancel = True
+                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and lifecycle explicitly cancelled"
+                                elif not lc_pending and not lc_opened:
+                                    should_cancel = True
+                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no active/pending/cancelled lifecycle found"
+                                else:
+                                    current_lc_status = "N/A"
+                                    if lc_pending: current_lc_status = lc_pending.get('status')
+                                    elif lc_opened: current_lc_status = lc_opened.get('status')
+                                    logger.info(f"SL {order_db_id} for parent {parent_bot_ref_id} (status {parent_status}) - lifecycle is '{current_lc_status}'. SL not cancelled by this rule.")
+                                    should_cancel = False
+                            elif parent_status in ['failed_submission', 'failed_submission_no_data', 'cancelled_manually', 'disappeared_from_exchange']:
+                                if not lc_opened: 
+                                    should_cancel = True
+                                    cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no 'position_opened' lifecycle"
+                                else:
+                                    logger.info(f"SL {order_db_id} for parent {parent_bot_ref_id} (status {parent_status}) - lifecycle is 'position_opened'. SL not cancelled by this rule.")
+                                    should_cancel = False
+                            elif parent_status == 'filled':
+                                if symbol not in position_symbols: 
+                                    should_cancel = True
+                                    cancel_reason = "parent filled but actual position no longer exists"
+                        else: 
+                            if parent_status in ['open', 'pending_submission', 'submitted']:
+                                logger.info(f"SL Cleanup: Parent order {parent_order.get('id')} (status '{parent_status}') is missing exchange_id in DB record. SL {sl_order.get('id')} will NOT be cancelled by this rule, assuming parent is still active or pending.")
+                                should_cancel = False
+                                if parent_status == 'open':
+                                    logger.warning(f"SL Cleanup: DATA ANOMALY - Parent order {parent_order.get('id')} status is 'open' but fetched DB record shows no exchange_id. Investigate DB state for order {parent_order.get('id')}.)")
+                            else:
+                                should_cancel = True
+                                cancel_reason = f"parent order {parent_status} (no exch_id) and status indicates it's not live/pending."
+                    else: 
+                        should_cancel = True
+                        cancel_reason = "parent order not found in database"
+                else:
+                    if symbol not in position_symbols:
+                        should_cancel = True
+                        cancel_reason = "no position exists and no parent reference"
+                
+                if should_cancel:
+                    success = stats.update_order_status(
+                        order_db_id=order_db_id,
+                        new_status='cancelled_orphaned'
+                    )
+                    if success:
+                        orphaned_count += 1
+                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                        logger.info(f"🧹 Cancelled orphaned stop loss for {token} (Order DB ID: {order_db_id}) - Reason: {cancel_reason}")
+
+            if orphaned_count > 0:
+                logger.info(f"🧹 Cleanup completed: Cancelled {orphaned_count} orphaned stop loss order(s)")
+                if self.notification_manager:
+                    await self.notification_manager.send_generic_notification(
+                        f"""🧹 <b>Cleanup Completed</b>\\n\\n
+Cancelled {orphaned_count} orphaned stop loss order(s)\\n
+Reason: Parent orders invalid or positions closed externally\\n
+Time: {datetime.now(timezone.utc).strftime('%H:%M:%S')}\\n\\n
+💡 This ensures stop losses sync with actual orders/positions."""
+                    )
+        except Exception as e:
+            logger.error(f"❌ Error cleaning up orphaned stop losses: {e}", exc_info=True)
+
+    async def _check_external_stop_loss_orders(self):
+        """Check for externally placed stop loss orders and track them."""
+        try:
+            open_orders = self.market_monitor_cache.cached_orders or []
+            if not open_orders:
+                return
+            positions = self.market_monitor_cache.cached_positions or []
+            if not positions:
+                return
+            
+            position_map = {}
+            for position in positions:
+                symbol = position.get('symbol')
+                contracts = float(position.get('contracts', 0))
+                if symbol and contracts != 0:
+                    token_map_key = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                    position_map[token_map_key] = {
+                        'symbol': symbol,
+                        'contracts': contracts,
+                        'side': 'long' if contracts > 0 else 'short',
+                        'entry_price': float(position.get('entryPx', 0))
+                    }
+            
+            newly_detected = 0
+            formatter = get_formatter()
+            for order in open_orders:
+                try:
+                    exchange_order_id = order.get('id')
+                    symbol = order.get('symbol')
+                    order_side = order.get('side', '').lower()
+                    order_type = order.get('type', '').lower()
+                    order_price = float(order.get('price', 0))
+                    stop_price = float(order.get('info', {}).get('triggerPrice', 0)) # Hyperliquid specific
+                    is_reduce_only = order.get('reduceOnly', False) or order.get('info',{}).get('reduceOnly',False)
+                    order_amount = float(order.get('amount',0))
+
+                    if not (symbol and exchange_order_id and order_side and order_type):
+                        continue
+                    
+                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+
+                    if exchange_order_id in self.external_stop_losses: # Already tracking
+                        continue
+
+                    if token in position_map:
+                        pos_details = position_map[token]
+                        is_potential_sl = False
+                        if pos_details['side'] == 'long' and order_side == 'sell':
+                            if stop_price > 0 and stop_price < pos_details['entry_price']:
+                                is_potential_sl = True
+                            elif order_type in ['stop', 'stopmarket', 'stop-market'] and order_price > 0 and order_price < pos_details['entry_price']:
+                                is_potential_sl = True # Using order_price as trigger if stop_price is missing for some reason
+                        elif pos_details['side'] == 'short' and order_side == 'buy':
+                            if stop_price > 0 and stop_price > pos_details['entry_price']:
+                                is_potential_sl = True
+                            elif order_type in ['stop', 'stopmarket', 'stop-market'] and order_price > 0 and order_price > pos_details['entry_price']:
+                                is_potential_sl = True
+                        
+                        if is_potential_sl and is_reduce_only and abs(order_amount - abs(pos_details['contracts'])) < 0.01 * abs(pos_details['contracts']):
+                            self.external_stop_losses[exchange_order_id] = {
+                                'symbol': symbol, 'side': order_side, 'amount': order_amount,
+                                'trigger_price': stop_price or order_price, 'type': order_type,
+                                'position_entry_price': pos_details['entry_price'],
+                                'position_side': pos_details['side'],
+                                'detected_at': datetime.now(timezone.utc).isoformat()
+                            }
+                            newly_detected += 1
+                            logger.info(f"🛡️ Detected external Stop Loss for {token} ({pos_details['side']}): OID {exchange_order_id}, Trigger@ {formatter.format_price(stop_price or order_price, symbol)}, Amount {formatter.format_amount(order_amount, token)}. Now tracking.")
+                except Exception as order_err:
+                    logger.error(f"Error processing order {order.get('id')} for external SL detection: {order_err}")
+                    continue
+            
+            if newly_detected > 0:
+                logger.info(f"🛡️ Detected and now tracking {newly_detected} new external stop loss order(s).")
+                # Persist self.external_stop_losses if desired, or rely on periodic checks
+
+        except Exception as e:
+            logger.error(f"❌ Error checking external stop loss orders: {e}", exc_info=True)
+
+    async def _cleanup_external_stop_loss_tracking(self):
+        """Clean up tracked external stop losses if position is gone or SL order is gone."""
+        try:
+            if not self.external_stop_losses:
+                return
+
+            current_positions = self.market_monitor_cache.cached_positions or []
+            position_symbols = {pos.get('symbol') for pos in current_positions if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9}
+            
+            current_open_orders_on_exchange = self.market_monitor_cache.cached_orders or []
+            open_exchange_order_ids = {order.get('id') for order in current_open_orders_on_exchange if order.get('id')}
+
+            removed_count = 0
+            for exch_oid, sl_info in list(self.external_stop_losses.items()): # Iterate over a copy
+                symbol = sl_info['symbol']
+                should_remove = False
+                reason = ""
+
+                if symbol not in position_symbols:
+                    should_remove = True
+                    reason = f"Position for {symbol} no longer exists."
+                elif exch_oid not in open_exchange_order_ids:
+                    should_remove = True
+                    reason = f"SL order {exch_oid} for {symbol} no longer on exchange."
+                
+                if should_remove:
+                    del self.external_stop_losses[exch_oid]
+                    removed_count += 1
+                    logger.info(f"🧹 External SL Cleanup: Removed tracking for {symbol} SL OID {exch_oid}. Reason: {reason}")
+            
+            if removed_count > 0:
+                logger.info(f"🧹 External SL Cleanup: Removed {removed_count} tracked external stop losses.")
+
+        except Exception as e:
+            logger.error(f"❌ Error cleaning up external stop loss tracking: {e}", exc_info=True)
+
+    async def _cleanup_orphaned_pending_sl_activations(self):
+        """Clean up 'pending_sl_activation' orders if parent entry order is gone and no position exists for the symbol."""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                logger.warning("⚠️ TradingStats not available in _cleanup_orphaned_pending_sl_activations.")
+                return
+
+            pending_sl_activation_orders = stats.get_orders_by_status(
+                status='pending_activation',
+                order_type_filter='pending_sl_activation'
+            )
+
+            if not pending_sl_activation_orders:
+                return
+
+            logger.debug(f"Found {len(pending_sl_activation_orders)} 'pending_sl_activation' orders to check for cleanup.")
+            
+            # Get current open orders on the exchange from the cache
+            open_exchange_orders_map = {
+                order.get('id'): order for order in (self.market_monitor_cache.cached_orders or []) if order.get('id')
+            }
+            # Get current open positions on the exchange from the cache
+            active_position_symbols = {
+                pos.get('symbol') for pos in (self.market_monitor_cache.cached_positions or [])
+                if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
+            }
+
+            cleaned_count = 0
+            for sl_order in pending_sl_activation_orders:
+                pending_sl_db_id = sl_order.get('id')
+                symbol = sl_order.get('symbol')
+                parent_bot_ref_id = sl_order.get('parent_bot_order_ref_id')
+
+                if not symbol or not parent_bot_ref_id:
+                    logger.warning(f"Skipping pending SL (DB ID: {pending_sl_db_id}) due to missing symbol or parent_bot_ref_id: {sl_order}")
+                    continue
+                
+                # Condition 1: No active position for the symbol
+                if symbol in active_position_symbols:
+                    logger.debug(f"Pending SL (DB ID: {pending_sl_db_id}, Symbol: {symbol}) skipped: Active position exists.")
+                    continue
+
+                # Condition 2: Parent entry order is no longer active on the exchange
+                parent_order_active_on_exchange = False
+                parent_order_in_db = stats.get_order_by_bot_ref_id(parent_bot_ref_id)
+
+                if parent_order_in_db:
+                    parent_exchange_order_id = parent_order_in_db.get('exchange_order_id')
+                    if parent_exchange_order_id:
+                        if parent_exchange_order_id in open_exchange_orders_map:
+                            parent_order_active_on_exchange = True
+                            logger.debug(f"Pending SL (DB ID: {pending_sl_db_id}, Symbol: {symbol}) - Parent order {parent_bot_ref_id} (Exch ID: {parent_exchange_order_id}) is still active on exchange.")
+                        else:
+                            logger.info(f"Pending SL (DB ID: {pending_sl_db_id}, Symbol: {symbol}) - Parent order {parent_bot_ref_id} (Exch ID: {parent_exchange_order_id}) NOT active on exchange.")
+                    else:
+                        # Parent order in DB but has no exchange ID (e.g., failed submission, or never placed)
+                        logger.info(f"Pending SL (DB ID: {pending_sl_db_id}, Symbol: {symbol}) - Parent order {parent_bot_ref_id} found in DB but has no exchange_order_id. Assuming not active on exchange.")
+                else:
+                    # Parent order not even found in DB
+                    logger.info(f"Pending SL (DB ID: {pending_sl_db_id}, Symbol: {symbol}) - Parent order {parent_bot_ref_id} NOT found in DB. Assuming not active.")
+
+                if not parent_order_active_on_exchange:
+                    # Both conditions met: No active position AND parent order is not active
+                    logger.info(f"🧹 Cleaning up orphaned 'pending_sl_activation' (DB ID: {pending_sl_db_id}) for symbol {symbol}. Parent order {parent_bot_ref_id} gone and no active position.")
+                    success = stats.update_order_status(
+                        order_db_id=pending_sl_db_id,
+                        new_status='cancelled_orphaned_entry_gone'
+                    )
+                    if success:
+                        cleaned_count += 1
+                        logger.info(f"✅ Successfully cancelled orphaned 'pending_sl_activation' (DB ID: {pending_sl_db_id}).")
+                        if self.notification_manager:
+                            await self.notification_manager.send_generic_notification(
+                                f"🧹 <b>Pending SL Activation Cleaned Up</b>\\n\\n"
+                                f"Symbol: {symbol}\\n"
+                                f"Reason: Original entry order is no longer active and no position exists for this symbol.\\n"
+                                f"Pending SL Bot Ref ID: {sl_order.get('bot_order_ref_id', 'N/A')}\\n"
+                                f"Parent Entry Bot Ref ID: {parent_bot_ref_id}\\n"
+                                f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}"
+                            )
+                    else:
+                        logger.error(f"❌ Failed to cancel orphaned 'pending_sl_activation' (DB ID: {pending_sl_db_id}).")
+            
+            if cleaned_count > 0:
+                logger.info(f"🧹 Pending SL Activation Cleanup: Cancelled {cleaned_count} orphaned 'pending_sl_activation' orders.")
+
+        except Exception as e:
+            logger.error(f"❌ Error in _cleanup_orphaned_pending_sl_activations: {e}", exc_info=True)

+ 1 - 1
trading_bot.py

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