Parcourir la source

Update trading bot architecture and version

- Incremented bot version to 2.6.282.
- Replaced MarketMonitor with MonitoringCoordinator to streamline monitoring processes.
- Updated ManagementCommands and TradingCommands to utilize the new MonitoringCoordinator for improved monitoring status checks and pending stop loss management.
- Removed deprecated market monitor settings and methods, simplifying the architecture.
- Enhanced configuration documentation for stop loss percentage settings in env.example.
Carles Sentis il y a 21 heures
Parent
commit
e5e81d303f

+ 2 - 0
config/env.example

@@ -28,6 +28,8 @@ DEFAULT_TRADING_TOKEN=BTC
 RISK_MANAGEMENT_ENABLED=true
 # Stop loss threshold based on ROE (Return on Equity) percentage - matches Hyperliquid UI
 # This is the percentage loss of your actual cash investment, not margin
+# For hard exits: 10.0 = -10% ROE triggers automatic position closure
+# Set to 100.0 to disable (would require -100% ROE = total loss)
 STOP_LOSS_PERCENTAGE=10.0
 
 # ========================================

+ 13 - 11
src/bot/core.py

@@ -13,7 +13,7 @@ import signal
 
 from src.config.config import Config
 from src.trading.trading_engine import TradingEngine
-from src.monitoring.market_monitor import MarketMonitor
+from src.monitoring.monitoring_coordinator import MonitoringCoordinator
 from src.notifications.notification_manager import NotificationManager
 from src.commands.trading_commands import TradingCommands
 from src.commands.management_commands import ManagementCommands
@@ -46,13 +46,14 @@ class TelegramTradingBot:
         # Initialize subsystems
         self.trading_engine = TradingEngine()
         self.notification_manager = NotificationManager()
-        self.market_monitor = MarketMonitor(self.trading_engine, self.notification_manager)
-        
-        # 🆕 WIRE UP: Connect market monitor to trading engine for cached data sharing
-        self.trading_engine.set_market_monitor(self.market_monitor)
+        self.monitoring_coordinator = MonitoringCoordinator(
+            self.trading_engine.hl_client, 
+            self.notification_manager,
+            Config
+        )
         
-        # Initialize command handlers
-        self.management_commands = ManagementCommands(self.trading_engine, self.market_monitor)
+        # Initialize command handlers  
+        self.management_commands = ManagementCommands(self.trading_engine, self.monitoring_coordinator)
         
         # Instantiate new info command classes
         self.balance_cmds = BalanceCommands(self.trading_engine, self.notification_manager)
@@ -93,6 +94,7 @@ class TelegramTradingBot:
         self.trading_commands = TradingCommands(
             self.trading_engine, 
             self.notification_manager,
+            monitoring_coordinator=self.monitoring_coordinator,
             management_commands_handler=self.management_commands,
             info_commands_handler=self.info_commands
         )
@@ -412,7 +414,7 @@ For support or issues, check the logs or contact the administrator.
             "Use /start for quick actions or /help for all commands."
         )
         
-        await self.market_monitor.start()
+        await self.monitoring_coordinator.start()
         
         logger.info("▶️ Starting PTB application's internal tasks (update processing, job queue).")
         await self.application.start()
@@ -436,9 +438,9 @@ For support or issues, check the logs or contact the administrator.
         finally:
             logger.info("🔌 Starting graceful shutdown sequence in TelegramTradingBot.run (v20.x style)...")
             try:
-                logger.info("Stopping market monitor...")
-                await self.market_monitor.stop()
-                logger.info("Market monitor stopped.")
+                logger.info("Stopping monitoring coordinator...")
+                await self.monitoring_coordinator.stop()
+                logger.info("Monitoring coordinator stopped.")
 
                 if self.application:
                     # Stop the updater first

+ 21 - 11
src/commands/management_commands.py

@@ -34,10 +34,10 @@ def _normalize_token_case(token: str) -> str:
 class ManagementCommands:
     """Handles all management-related Telegram commands."""
     
-    def __init__(self, trading_engine, market_monitor):
-        """Initialize with trading engine and market monitor."""
+    def __init__(self, trading_engine, monitoring_coordinator):
+        """Initialize with trading engine and monitoring coordinator."""
         self.trading_engine = trading_engine
-        self.market_monitor = market_monitor
+        self.monitoring_coordinator = monitoring_coordinator
         self.alarm_manager = AlarmManager()
     
     def _is_authorized(self, chat_id: str) -> bool:
@@ -61,16 +61,19 @@ class ManagementCommands:
         }
         formatter = get_formatter()
         
-        # Safety checks for monitoring attributes
-        monitoring_active = self.market_monitor._monitoring_active
+        # Get monitoring status from coordinator
+        monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
+        monitoring_active = monitoring_status.get('is_running', False)
         
         status_text = f"""
 🔄 <b>System Monitoring Status</b>
 
-📊 <b>Order Monitoring:</b>
-• Active: {'✅ Yes' if monitoring_active else '❌ No'}
+📊 <b>Monitoring System:</b>
+• Status: {'✅ Active' if monitoring_active else '❌ Inactive'}
 • Check Interval: {Config.BOT_HEARTBEAT_SECONDS} seconds
-• Market Monitor: {'✅ Running' if monitoring_active else '❌ Stopped'}
+• Position Tracker: {'✅' if monitoring_status.get('components', {}).get('position_tracker', False) else '❌'}
+• Risk Manager: {'✅' if monitoring_status.get('components', {}).get('risk_manager', False) else '❌'}
+• Pending Orders: {'✅' if monitoring_status.get('components', {}).get('pending_orders_manager', False) else '❌'}
 
 💰 <b>Balance Tracking:</b>
 • Total Adjustments: {adjustments_summary['adjustment_count']}
@@ -358,6 +361,10 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
             return
         
         try:
+            # Get monitoring status
+            monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
+            monitoring_active = monitoring_status.get('is_running', False)
+            
             # Get system information
             debug_info = f"""
 🐛 <b>Debug Information</b>
@@ -371,8 +378,8 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
 • Stats Available: {'✅ Yes' if self.trading_engine.get_stats() else '❌ No'}
 • Client Connected: {'✅ Yes' if self.trading_engine.client else '❌ No'}
 
-🔄 <b>Market Monitor:</b>
-• Running: {'✅ Yes' if self.market_monitor._monitoring_active else '❌ No'}
+🔄 <b>Monitoring System:</b>
+• Running: {'✅ Yes' if monitoring_active else '❌ No'}
 
 📁 <b>State Files:</b>
 • Price Alarms: {'✅ Exists' if os.path.exists('data/price_alarms.json') else '❌ Missing'}
@@ -427,6 +434,9 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
             return
         
         try:
+            # Get monitoring status
+            monitoring_status = await self.monitoring_coordinator.get_monitoring_status()
+            monitoring_active = monitoring_status.get('is_running', False)
             # Get uptime info
             uptime_info = "Unknown"
             try:
@@ -469,7 +479,7 @@ Will trigger when {token} price moves {alarm['direction']} {target_price_str}
 • Start Date: {basic_stats['start_date']}
 
 🔄 <b>Monitoring Status:</b>
-• Market Monitor: {'✅ Active' if self.market_monitor._monitoring_active else '❌ Inactive'}
+• Monitoring System: {'✅ Active' if monitoring_active else '❌ Inactive'}
 • External Trades: ✅ Active
 • Price Alarms: ✅ Active ({self.alarm_manager.get_statistics()['total_active']} active)
 

+ 18 - 3
src/commands/trading_commands.py

@@ -19,10 +19,11 @@ logger = logging.getLogger(__name__)
 class TradingCommands:
     """Handles all trading-related Telegram commands."""
     
-    def __init__(self, trading_engine: TradingEngine, notification_manager: NotificationManager, management_commands_handler=None, info_commands_handler=None):
+    def __init__(self, trading_engine: TradingEngine, notification_manager: NotificationManager, monitoring_coordinator=None, management_commands_handler=None, info_commands_handler=None):
         """Initialize trading commands with required dependencies."""
         self.trading_engine = trading_engine
         self.notification_manager = notification_manager
+        self.monitoring_coordinator = monitoring_coordinator
         self.management_commands_handler = management_commands_handler
         self.info_commands_handler = info_commands_handler
     
@@ -731,7 +732,8 @@ This action cannot be undone.
         
         await query.edit_message_text("⏳ Executing long order...")
         
-        result = await self.trading_engine.execute_long_order(token, usdc_amount, price, stop_loss_price)
+        # Execute the order without stop loss (will be handled by pending orders manager)
+        result = await self.trading_engine.execute_long_order(token, usdc_amount, price, None)
         
         if result["success"]:
             # Extract data from new response format
@@ -740,6 +742,12 @@ This action cannot be undone.
             price_used = order_details.get("price_requested") or price
             trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             
+            # Add pending stop loss if specified
+            if stop_loss_price and self.monitoring_coordinator:
+                await self.monitoring_coordinator.add_pending_stop_loss(
+                    token, stop_loss_price, token_amount, 'long'
+                )
+            
             # Create a mock order object for backward compatibility with notification method
             mock_order = {
                 "id": order_details.get("exchange_order_id", "N/A"),
@@ -766,7 +774,8 @@ This action cannot be undone.
         
         await query.edit_message_text("⏳ Executing short order...")
         
-        result = await self.trading_engine.execute_short_order(token, usdc_amount, price, stop_loss_price)
+        # Execute the order without stop loss (will be handled by pending orders manager)
+        result = await self.trading_engine.execute_short_order(token, usdc_amount, price, None)
         
         if result["success"]:
             # Extract data from new response format
@@ -775,6 +784,12 @@ This action cannot be undone.
             price_used = order_details.get("price_requested") or price
             trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             
+            # Add pending stop loss if specified
+            if stop_loss_price and self.monitoring_coordinator:
+                await self.monitoring_coordinator.add_pending_stop_loss(
+                    token, stop_loss_price, token_amount, 'short'
+                )
+            
             # Create a mock order object for backward compatibility with notification method
             mock_order = {
                 "id": order_details.get("exchange_order_id", "N/A"),

+ 1 - 2
src/config/config.py

@@ -45,8 +45,7 @@ class Config:
     # Bot settings
     BOT_HEARTBEAT_SECONDS: int = int(os.getenv('BOT_HEARTBEAT_SECONDS', '5'))
     
-    # Market Monitor settings
-    MARKET_MONITOR_CLEANUP_INTERVAL_HEARTBEATS: int = 120 # Approx every 10 minutes if heartbeat is 5s
+    # Removed market monitor settings - simplified architecture
     
     # Order settings
     DEFAULT_SLIPPAGE: float = 0.005  # 0.5%

+ 20 - 1
src/monitoring/__init__.py

@@ -1 +1,20 @@
-# Monitoring module for Telegram bot 
+# Monitoring module for Telegram bot 
+
+# Simplified Monitoring System
+from .monitoring_coordinator import MonitoringCoordinator
+from .position_tracker import PositionTracker
+from .pending_orders_manager import PendingOrdersManager
+from .risk_manager import RiskManager
+from .alarm_manager import AlarmManager
+from .drawdown_monitor import DrawdownMonitor
+from .rsi_monitor import RSIMonitor
+
+__all__ = [
+    'MonitoringCoordinator',
+    'PositionTracker', 
+    'PendingOrdersManager',
+    'RiskManager',
+    'AlarmManager',
+    'DrawdownMonitor',
+    'RSIMonitor'
+] 

+ 0 - 829
src/monitoring/external_event_monitor.py

@@ -1,829 +0,0 @@
-#!/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
-
-    def _safe_get_positions(self) -> Optional[List[Dict[str, Any]]]:
-        """Safely get positions from trading engine, returning None on API failures instead of empty list."""
-        try:
-            return self.trading_engine.get_positions()
-        except Exception as e:
-            logger.warning(f"⚠️ Failed to fetch positions in external event monitor: {e}")
-            return None
-
-    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 = await 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 _determine_position_action_type(self, full_symbol: str, side_from_fill: str, 
-                                            amount_from_fill: float, existing_lc: Optional[Dict] = None) -> str:
-        """
-        Determine the type of position action based on current state and fill details.
-        Returns one of: 'position_opened', 'position_closed', 'position_increased', 'position_decreased'
-        """
-        try:
-            # Get current position from exchange
-            current_positions = self._safe_get_positions()
-            if current_positions is None:
-                logger.warning(f"⚠️ Failed to fetch positions for {full_symbol} analysis - returning external_unmatched")
-                return 'external_unmatched'
-            
-            current_exchange_position = None
-            for pos in current_positions:
-                if pos.get('symbol') == full_symbol:
-                    current_exchange_position = pos
-                    break
-            
-            current_size = 0.0
-            if current_exchange_position:
-                current_size = abs(float(current_exchange_position.get('contracts', 0)))
-            
-            # If no existing lifecycle, this is a position opening
-            if not existing_lc:
-                logger.debug(f"🔍 Position analysis: {full_symbol} no existing lifecycle, current size: {current_size}")
-                if current_size > 1e-9:  # Position exists on exchange
-                    return 'position_opened'
-                else:
-                    return 'external_unmatched'
-            
-            # Get previous position size from lifecycle
-            previous_size = existing_lc.get('current_position_size', 0)
-            lc_position_side = existing_lc.get('position_side')
-            
-            logger.debug(f"🔍 Position analysis: {full_symbol} {side_from_fill} {amount_from_fill}")
-            logger.debug(f"  Lifecycle side: {lc_position_side}, previous size: {previous_size}, current size: {current_size}")
-            
-            # Check if this is a closing trade (opposite side)
-            is_closing_trade = False
-            if lc_position_side == 'long' and side_from_fill.lower() == 'sell':
-                is_closing_trade = True
-            elif lc_position_side == 'short' and side_from_fill.lower() == 'buy':
-                is_closing_trade = True
-            
-            logger.debug(f"  Is closing trade: {is_closing_trade}")
-            
-            if is_closing_trade:
-                if current_size < 1e-9:  # Position is now closed
-                    logger.debug(f"  → Position closed (current_size < 1e-9)")
-                    return 'position_closed'
-                elif current_size < previous_size - 1e-9:  # Position reduced but not closed
-                    logger.debug(f"  → Position decreased (current_size {current_size} < previous_size - 1e-9 {previous_size - 1e-9})")
-                    return 'position_decreased'
-            else:
-                # Same side trade - position increase
-                logger.debug(f"  Same side trade check: current_size {current_size} > previous_size + 1e-9 {previous_size + 1e-9}?")
-                if current_size > previous_size + 1e-9:
-                    logger.debug(f"  → Position increased")
-                    return 'position_increased'
-                else:
-                    logger.debug(f"  → Size check failed, not enough increase")
-            
-            # Default fallback
-            logger.debug(f"  → Fallback to external_unmatched")
-            return 'external_unmatched'
-            
-        except Exception as e:
-            logger.error(f"Error determining position action type: {e}")
-            return 'external_unmatched'
-
-    async def _update_lifecycle_position_size(self, lifecycle_id: str, new_size: float) -> bool:
-        """Update the current position size in the lifecycle."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                return False
-            
-            # Update the current position size
-            success = stats.trade_manager.update_trade_market_data(
-                lifecycle_id, current_position_size=new_size
-            )
-            return success
-        except Exception as e:
-            logger.error(f"Error updating lifecycle position size: {e}")
-            return False
-
-    async def _send_position_change_notification(self, full_symbol: str, side_from_fill: str, 
-                                               amount_from_fill: float, price_from_fill: float, 
-                                               action_type: str, timestamp_dt: datetime, 
-                                               existing_lc: Optional[Dict] = None, 
-                                               realized_pnl: Optional[float] = None):
-        """Send position change notification."""
-        try:
-            if not self.notification_manager:
-                return
-                
-            token = full_symbol.split('/')[0] if '/' in full_symbol else full_symbol.split(':')[0]
-            time_str = timestamp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
-            formatter = get_formatter()
-            
-            if action_type == 'position_closed' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                entry_price = existing_lc.get('entry_price', 0)
-                pnl_emoji = "🟢" if realized_pnl and realized_pnl >= 0 else "🔴"
-                pnl_text = f"{await formatter.format_price_with_symbol(realized_pnl)}" if realized_pnl is not None else "N/A"
-                
-                # Get ROE directly from exchange data
-                info_data = existing_lc.get('info', {})
-                position_info = info_data.get('position', {})
-                roe_raw = position_info.get('returnOnEquity')  # Changed from 'percentage' to 'returnOnEquity'
-                
-                if roe_raw is not None:
-                    try:
-                        # The exchange provides ROE as a decimal (e.g., -0.326 for -32.6%)
-                        # We need to multiply by 100 and keep the sign
-                        roe = float(roe_raw) * 100
-                        roe_text = f" ({roe:+.2f}%)"
-                    except (ValueError, TypeError):
-                        logger.warning(f"Could not parse ROE value: {roe_raw} for {full_symbol}")
-                        roe_text = ""
-                else:
-                    logger.warning(f"No ROE data available from exchange for {full_symbol}")
-                    roe_text = ""
-                
-                message = f"""
-🎯 <b>Position Closed (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Closed: {await formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
-• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-{pnl_emoji} <b>P&L:</b> {pnl_text}{roe_text}
-✅ <b>Status:</b> {position_side} position closed externally
-⏰ <b>Time:</b> {time_str}
-
-📊 Use /stats to view updated performance
-                """
-                
-            elif action_type == 'position_opened':
-                position_side = 'LONG' if side_from_fill.lower() == 'buy' else 'SHORT'
-                message = f"""
-🚀 <b>Position Opened (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size: {await formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Position Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-✅ <b>Status:</b> New {position_side} position opened externally
-⏰ <b>Time:</b> {time_str}
-
-📱 Use /positions to view all positions
-                """
-                
-            elif action_type == 'position_increased' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                previous_size = existing_lc.get('current_position_size', 0)
-                # Get current size from exchange
-                current_positions = self._safe_get_positions()
-                if current_positions is None:
-                    # Skip notification if we can't get position data
-                    logger.warning(f"⚠️ Failed to fetch positions for notification - skipping {action_type} notification")
-                    return
-                current_size = 0
-                for pos in current_positions:
-                    if pos.get('symbol') == full_symbol:
-                        current_size = abs(float(pos.get('contracts', 0)))
-                        break
-                
-                message = f"""
-📈 <b>Position Increased (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Added: {await formatter.format_amount(amount_from_fill, token)}
-• Add Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {await formatter.format_amount(previous_size, token)}
-• New Size: {await formatter.format_amount(current_size, token)}
-• Add Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-📈 <b>Status:</b> {position_side} position size increased externally
-⏰ <b>Time:</b> {time_str}
-
-📈 Use /positions to view current position
-                """
-                
-            elif action_type == 'position_decreased' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                previous_size = existing_lc.get('current_position_size', 0)
-                entry_price = existing_lc.get('entry_price', 0)
-                
-                # Get current size from exchange
-                current_positions = self._safe_get_positions()
-                if current_positions is None:
-                    # Skip notification if we can't get position data
-                    logger.warning(f"⚠️ Failed to fetch positions for notification - skipping {action_type} notification")
-                    return
-                current_size = 0
-                for pos in current_positions:
-                    if pos.get('symbol') == full_symbol:
-                        current_size = abs(float(pos.get('contracts', 0)))
-                        break
-                
-                # Calculate partial PnL for the reduced amount
-                partial_pnl = 0
-                if entry_price > 0:
-                    if position_side == 'LONG':
-                        partial_pnl = amount_from_fill * (price_from_fill - entry_price)
-                    else:  # SHORT
-                        partial_pnl = amount_from_fill * (entry_price - price_from_fill)
-                
-                pnl_emoji = "🟢" if partial_pnl >= 0 else "🔴"
-                
-                # Calculate ROE for the partial close
-                roe_text = ""
-                if entry_price > 0 and amount_from_fill > 0:
-                    cost_basis = amount_from_fill * entry_price
-                    roe = (partial_pnl / cost_basis) * 100
-                    roe_text = f" ({roe:+.2f}%)"
-                
-                message = f"""
-📉 <b>Position Decreased (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Reduced: {await formatter.format_amount(amount_from_fill, token)}
-• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {await formatter.format_amount(previous_size, token)}
-• Remaining Size: {await formatter.format_amount(current_size, token)}
-• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-{pnl_emoji} <b>Partial P&L:</b> {await formatter.format_price_with_symbol(partial_pnl)}{roe_text}
-📉 <b>Status:</b> {position_side} position size decreased externally  
-⏰ <b>Time:</b> {time_str}
-
-📊 Position remains open. Use /positions to view details
-                """
-            else:
-                # No fallback notification sent - only position-based notifications per user preference
-                logger.debug(f"No notification sent for action_type: {action_type}")
-                return
-            
-            await self.notification_manager.send_generic_notification(message.strip())
-            
-        except Exception as e:
-            logger.error(f"Error sending position change notification: {e}")
-    
-    async def _auto_sync_single_position(self, symbol: str, exchange_position: Dict[str, Any], stats) -> bool:
-        """Auto-sync a single orphaned position to create a lifecycle record."""
-        try:
-            import uuid
-            from src.utils.token_display_formatter import get_formatter
-            
-            formatter = get_formatter()
-            contracts_abs = abs(float(exchange_position.get('contracts', 0)))
-            
-            if contracts_abs <= 1e-9:
-                return False
-            
-            entry_price_from_exchange = float(exchange_position.get('entryPrice', 0)) or float(exchange_position.get('entryPx', 0))
-            
-            # Determine position side
-            position_side, order_side = '', ''
-            ccxt_side = exchange_position.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:
-                contracts_val = float(exchange_position.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:
-                    return False
-            
-            if not position_side:
-                logger.error(f"AUTO-SYNC: Could not determine position side for {symbol}.")
-                return False
-            
-            final_entry_price = entry_price_from_exchange
-            if not final_entry_price or final_entry_price <= 0:
-                # Fallback to a reasonable estimate (current mark price)
-                mark_price = float(exchange_position.get('markPrice', 0)) or float(exchange_position.get('markPx', 0))
-                if mark_price > 0:
-                    final_entry_price = mark_price
-                else:
-                    logger.error(f"AUTO-SYNC: Could not determine entry price for {symbol}.")
-                    return False
-            
-            logger.info(f"🔄 AUTO-SYNC: Creating lifecycle for {symbol} {position_side.upper()} {contracts_abs} @ {await formatter.format_price_with_symbol(final_entry_price, symbol)}")
-            
-            unique_sync_id = str(uuid.uuid4())[:8]
-            lifecycle_id = stats.create_trade_lifecycle(
-                symbol=symbol, 
-                side=order_side, 
-                entry_order_id=f"external_sync_{unique_sync_id}",
-                trade_type='external_sync'
-            )
-            
-            if lifecycle_id:
-                success = await stats.update_trade_position_opened(
-                    lifecycle_id, 
-                    final_entry_price, 
-                    contracts_abs,
-                    f"external_fill_sync_{unique_sync_id}"
-                )
-                
-                if success:
-                    logger.info(f"✅ AUTO-SYNC: Successfully synced position for {symbol} (Lifecycle: {lifecycle_id[:8]})")
-                    
-                    # Send position opened notification for auto-synced position
-                    try:
-                        await self._send_position_change_notification(
-                            symbol, order_side, contracts_abs, final_entry_price,
-                            'position_opened', datetime.now(timezone.utc)
-                        )
-                        logger.info(f"📨 AUTO-SYNC: Sent position opened notification for {symbol}")
-                    except Exception as e:
-                        logger.error(f"❌ AUTO-SYNC: Failed to send notification for {symbol}: {e}")
-                    
-                    return True
-                else:
-                    logger.error(f"❌ AUTO-SYNC: Failed to update lifecycle to 'position_opened' for {symbol}")
-            else:
-                logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for {symbol}")
-            
-            return False
-            
-        except Exception as e:
-            logger.error(f"❌ AUTO-SYNC: Error syncing position for {symbol}: {e}")
-            return False
-    
-    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
-                    
-                    # Check if this fill has already been processed to prevent duplicates
-                    if trade_id and stats.has_exchange_fill_been_processed(str(trade_id)):
-                        logger.debug(f"Skipping already processed fill: {trade_id}")
-                        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')
-
-                    # First check if this is a pending entry order 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 = await 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)
-                                
-                                # Send position opened notification (this is a bot-initiated position)
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    'position_opened', timestamp_dt
-                                )
-                            fill_processed_this_iteration = True
-
-                    # Check if this is a known bot order (SL/TP/exit)
-                    if not fill_processed_this_iteration and exchange_order_id_from_fill:
-                        active_lc = None
-                        closure_reason_action_type = None
-                        bot_order_db_id_to_update = None
-
-                        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']}.")
-
-                        # Process known bot order fills
-                        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: {await formatter.format_price_with_symbol(realized_pnl)}")
-                                symbols_with_fills.add(token)
-                                
-                                # Send position closed notification
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    'position_closed', timestamp_dt, active_lc, realized_pnl
-                                )
-                                
-                                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
-
-                    # Check for external stop losses
-                    if not fill_processed_this_iteration:
-                        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 @ {await 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: {await 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.")
-
-                    # NEW: Enhanced external trade processing with position state detection
-                    if not fill_processed_this_iteration:
-                        existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        
-                        # If no lifecycle exists but we have a position on exchange, try to auto-sync first
-                        if not existing_lc:
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for external trade detection - skipping this fill")
-                                continue
-                            exchange_position = None
-                            for pos in current_positions:
-                                if pos.get('symbol') == full_symbol:
-                                    exchange_position = pos
-                                    break
-                            
-                            if exchange_position and abs(float(exchange_position.get('contracts', 0))) > 1e-9:
-                                logger.info(f"🔄 AUTO-SYNC: Position exists on exchange for {full_symbol} but no lifecycle found. Auto-syncing before processing fill.")
-                                success = await self._auto_sync_single_position(full_symbol, exchange_position, stats)
-                                if success:
-                                    # Re-check for lifecycle after auto-sync
-                                    existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        
-                        action_type = await self._determine_position_action_type(
-                            full_symbol, side_from_fill, amount_from_fill, existing_lc
-                        )
-                        
-                        logger.info(f"🔍 External fill analysis: {full_symbol} {side_from_fill} {amount_from_fill} -> {action_type}")
-                        
-                        # Additional debug logging for position changes
-                        if existing_lc:
-                            previous_size = existing_lc.get('current_position_size', 0)
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for debug logging - skipping debug info")
-                                # Set defaults to avoid reference errors
-                                current_size = previous_size
-                            else:
-                                current_size = 0
-                                for pos in current_positions:
-                                    if pos.get('symbol') == full_symbol:
-                                        current_size = abs(float(pos.get('contracts', 0)))
-                                        break
-                                logger.info(f"📊 Position size change: {previous_size} -> {current_size} (diff: {current_size - previous_size})")
-                                logger.info(f"🎯 Expected change based on fill: {'+' if side_from_fill.lower() == 'buy' else '-'}{amount_from_fill}")
-                                
-                                # If lifecycle is already closed, we should not re-process as a new closure.
-                                if existing_lc.get('status') == 'position_closed':
-                                    logger.info(f"ℹ️ Fill {trade_id} received for already closed lifecycle {existing_lc['trade_lifecycle_id']}. Recording fill and skipping further action.")
-                                    stats.record_trade(
-                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                        exchange_fill_id=trade_id, trade_type="external_fill_for_closed_pos",
-                                        pnl=None, timestamp=timestamp_dt.isoformat(),
-                                        linked_order_table_id_to_link=None 
-                                    )
-                                    fill_processed_this_iteration = True
-                                    continue # Skip to next fill
-
-                                # Check if this might be a position decrease that was misclassified
-                                if (action_type == 'external_unmatched' and 
-                                    existing_lc.get('position_side') == 'long' and 
-                                    side_from_fill.lower() == 'sell' and 
-                                    current_size < previous_size):
-                                    logger.warning(f"⚠️ Potential misclassification: {full_symbol} {side_from_fill} looks like position decrease but classified as external_unmatched")
-                                    # Force re-check with proper parameters
-                                    action_type = 'position_decreased'
-                                    logger.info(f"🔄 Corrected action_type to: {action_type}")
-                                elif (action_type == 'external_unmatched' and 
-                                      existing_lc.get('position_side') == 'long' and 
-                                      side_from_fill.lower() == 'buy' and 
-                                      current_size > previous_size):
-                                    logger.warning(f"⚠️ Potential misclassification: {full_symbol} {side_from_fill} looks like position increase but classified as external_unmatched")
-                                    action_type = 'position_increased'
-                                    logger.info(f"🔄 Corrected action_type to: {action_type}")
-                        
-                        if action_type == 'position_opened':
-                            # Create new lifecycle for external position
-                            lifecycle_id = stats.create_trade_lifecycle(
-                                symbol=full_symbol,
-                                side=side_from_fill,
-                                entry_order_id=exchange_order_id_from_fill or f"external_{trade_id}",
-                                trade_type="external"
-                            )
-                            
-                            if lifecycle_id:
-                                success = stats.update_trade_position_opened(
-                                    lifecycle_id=lifecycle_id,
-                                    entry_price=price_from_fill,
-                                    entry_amount=amount_from_fill,
-                                    exchange_fill_id=trade_id
-                                )
-                                
-                                if success:
-                                    logger.info(f"📈 Created and opened new external lifecycle: {lifecycle_id[:8]} for {full_symbol}")
-                                    symbols_with_fills.add(token)
-                                    
-                                    # Send position opened notification  
-                                    await self._send_position_change_notification(
-                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                        action_type, timestamp_dt
-                                    )
-                                    fill_processed_this_iteration = True
-                        
-                        elif action_type == 'position_closed' and existing_lc:
-                            # Close existing lifecycle
-                            lc_id = existing_lc['trade_lifecycle_id']
-                            lc_entry_price = existing_lc.get('entry_price', 0)
-                            lc_position_side = existing_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 (External): {lc_id}. PNL: {await formatter.format_price_with_symbol(realized_pnl)}")
-                                symbols_with_fills.add(token)
-                                
-                                # Send position closed notification
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    action_type, timestamp_dt, existing_lc, realized_pnl
-                                )
-                                
-                                stats.migrate_trade_to_aggregated_stats(lc_id)
-                                fill_processed_this_iteration = True
-                        
-                        elif action_type in ['position_increased', 'position_decreased'] and existing_lc:
-                            # Update lifecycle position size and send notification
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for size update - skipping position change processing")
-                                continue
-                            new_size = 0
-                            for pos in current_positions:
-                                if pos.get('symbol') == full_symbol:
-                                    new_size = abs(float(pos.get('contracts', 0)))
-                                    break
-                            
-                            # Update lifecycle with new position size
-                            await self._update_lifecycle_position_size(existing_lc['trade_lifecycle_id'], new_size)
-                            
-                            # Send appropriate notification
-                            await self._send_position_change_notification(
-                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                action_type, timestamp_dt, existing_lc
-                            )
-                            
-                            symbols_with_fills.add(token)
-                            fill_processed_this_iteration = True
-                            logger.info(f"📊 Position {action_type}: {full_symbol} new size: {new_size}")
-
-                    # Fallback for unmatched external trades
-                    if not fill_processed_this_iteration:
-                        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.debug(f"Position {full_symbol} found in open positions but no active lifecycle - likely auto-sync failed or timing issue for fill {trade_id}")
-                        
-                        # Record as unmatched external trade
-                        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')
-                        
-                        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)")
-                        
-                        # No notification sent for unmatched external trades per user preference
-                        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) 

+ 0 - 450
src/monitoring/market_monitor.py

@@ -1,450 +0,0 @@
-#!/usr/bin/env python3
-"""
-Market Monitor - Main coordinator for monitoring market events, orders, and positions.
-"""
-
-import logging
-import asyncio
-from datetime import datetime, timedelta, timezone
-from typing import Optional, Dict, Any, List
-
-from src.config.config import Config
-from src.monitoring.alarm_manager import AlarmManager
-from src.utils.token_display_formatter import get_formatter
-
-# Core monitoring components
-from src.monitoring.order_fill_processor import OrderFillProcessor
-from src.monitoring.position_monitor import PositionMonitor
-from src.monitoring.risk_cleanup_manager import RiskCleanupManager
-from src.monitoring.drawdown_monitor import DrawdownMonitor
-from src.monitoring.rsi_monitor import RsiMonitor
-
-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:
-    """Coordinates monitoring activities by delegating to specialized processors and managers."""
-    
-    def __init__(self, trading_engine, notification_manager=None):
-        self.trading_engine = trading_engine
-        self.notification_manager = notification_manager
-        self._monitoring_active = False
-        self._monitor_task = None
-        
-        self.alarm_manager = AlarmManager() # AlarmManager is standalone
-        
-        # Initialize the DrawdownMonitor
-        stats = self.trading_engine.get_stats()
-        self.drawdown_monitor = DrawdownMonitor(stats) if stats else None
-        
-        # 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_monitor = PositionMonitor(
-            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
-        )
-        
-        # Initialize RSI monitor for crossover notifications
-        self.rsi_monitor = RsiMonitor(
-            hyperliquid_client=self.trading_engine.client,
-            notification_manager=self.notification_manager
-        )
-        
-        # Load minimal persistent state if any (most state is now in delegates or transient)
-        self._load_state()
-        
-    async def start(self):
-        if self._monitoring_active:
-            logger.warning("Market monitor is already active")
-            return
-        
-        self._monitoring_active = True
-        logger.info("🔄 Market monitor started")
-        
-        await self._initialize_tracking()
-        self._monitor_task = asyncio.create_task(self._monitor_loop())
-    
-    async def stop(self):
-        if not self._monitoring_active:
-            return
-        
-        self._monitoring_active = False
-        if self._monitor_task:
-            self._monitor_task.cancel()
-            try:
-                await self._monitor_task
-            except asyncio.CancelledError:
-                pass
-        
-        self._save_state() # Save minimal state if any
-        logger.info("🛑 Market monitor stopped")
-    
-    def _load_state(self):
-        """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()
-        if not stats:
-            logger.warning("⚠️ TradingStats not available, cannot load MarketMonitor helper states.")
-            return
-        try:
-            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:
-            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):
-        """Save minimal MarketMonitor-specific state if necessary."""
-        # self.alarm_manager.save_alarms()
-        stats = self.trading_engine.get_stats()
-        if not stats:
-            logger.warning("⚠️ TradingStats not available, cannot save MarketMonitor helper states.")
-            return
-        try:
-            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:
-            logger.error(f"Error saving OrderFillProcessor helper state: {e}")
-            
-        logger.info("MarketMonitor _save_state: Minimal state saved.")
-    
-    async def _initialize_tracking(self):
-        """Initialize basic tracking for cache."""
-        try:
-            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()
-            if positions is None:
-                logger.warning("⚠️ Failed to fetch positions during initialization - using empty state")
-                positions = []
-            # 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")
-            
-            # Initial position sync handled by simplified position tracker on first run
-            # The simplified tracker will auto-detect and sync any missing positions
-                    
-        except Exception as e:
-            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):
-        try:
-            loop_count = 0
-            while self._monitoring_active:
-                await self._update_cached_data() # Updates self.cache
-                
-                # 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() 
-                
-                # Consolidated position monitoring
-                await self.position_monitor.run_monitor_cycle()
-                
-                # Risk, cleanup, and sync tasks are now handled by the RiskCleanupManager
-                await self.risk_cleanup_manager.run_cleanup_tasks()
-                
-                # RSI monitoring - only calculates when there are new candles
-                await self._run_rsi_monitoring()
-                
-                loop_count += 1
-                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
-                    
-                    # Position sync now handled by simplified position tracker
-                    # await self.position_synchronizer._auto_sync_orphaned_positions()
-                    
-                    loop_count = 0
-                
-                await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
-        except asyncio.CancelledError:
-            logger.info("Market monitor loop cancelled")
-            raise
-        except Exception as e:
-            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):
-        """Continuously update cached exchange data for all components to use."""
-        try:
-            fresh_positions_list = self.trading_engine.get_positions()
-            fresh_orders_list = self.trading_engine.get_orders()
-            fresh_balance = self.trading_engine.get_balance()
-            
-            # Handle API failures gracefully
-            if fresh_positions_list is None:
-                logger.warning("⚠️ Failed to fetch positions - keeping previous cache")
-                fresh_positions_list = self.cache.cached_positions or []
-            if fresh_orders_list is None:
-                logger.warning("⚠️ Failed to fetch orders - keeping previous cache")
-                fresh_orders_list = self.cache.cached_orders or []
-            
-            # 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)
-            
-            # Update drawdown monitor with the latest balance
-            if self.drawdown_monitor and fresh_balance and fresh_balance.get('total') is not None:
-                # fresh_balance['total'] is a dict like {'USDC': 1234.56, 'BTC': 0.001}
-                usdc_balance = fresh_balance['total'].get('USDC', 0)
-                if usdc_balance:
-                    self.drawdown_monitor.update_balance(float(usdc_balance))
-
-            logger.debug(f"🔄 Cache updated: {len(fresh_positions_list)} positions, {len(fresh_orders_list)} orders")
-
-            current_exchange_position_map = {
-                pos.get('symbol'): pos for pos in fresh_positions_list
-                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')}
-
-            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.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 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.
-            
-            stats = self.trading_engine.get_stats()
-            if stats and fresh_positions_list:
-                for ex_pos in fresh_positions_list: # Changed pos_data to ex_pos to match original
-                    symbol = ex_pos.get('symbol')
-                    if not symbol: 
-                        continue
-
-                    # Attempt to get the lifecycle_id for this symbol
-                    db_trade = stats.get_trade_by_symbol_and_status(symbol, status='position_opened')
-                    if db_trade:
-                        lifecycle_id = db_trade.get('trade_lifecycle_id')
-                        if not lifecycle_id:
-                            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
-
-                        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
-
-                            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
-
-                            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
-
-                            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
-
-                            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
-
-                            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
-
-                            leverage_from_ex = ex_pos.get('leverage')
-                            leverage = float(leverage_from_ex) if leverage_from_ex is not None else None
-
-                            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 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
-
-                            # Extract ROE from info.position.returnOnEquity
-                            roe_raw = None
-                            if 'info' in ex_pos and 'position' in ex_pos['info']:
-                                roe_raw = ex_pos['info']['position'].get('returnOnEquity')
-                            roe_percentage = float(roe_raw) * 100 if roe_raw is not None else None
-
-                            # Update position size using existing manager
-                            success = stats.trade_manager.update_trade_market_data(
-                                lifecycle_id, 
-                                current_position_size=current_position_size,
-                                unrealized_pnl=unrealized_pnl,
-                                roe_percentage=roe_percentage,
-                                mark_price=mark_price,
-                                position_value=position_value,
-                                margin_used=margin_used,
-                                leverage=leverage,
-                                liquidation_price=liquidation_price
-                            )
-                        except (ValueError, TypeError) as e:
-                            logger.warning(f"Could not parse full market data for {symbol} (Lifecycle: {lifecycle_id}) from {ex_pos}: {e}")
-                    else:
-                        # 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.")
-            
-        except Exception as e:
-            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()
-    
-    def get_monitoring_status(self) -> Dict[str, Any]:
-        """Get status of all monitoring components."""
-        return {
-            'market_monitor_active': self._monitoring_active,
-            'cache_age_seconds': self.get_cache_age_seconds(),
-            'simplified_position_tracker': self.position_monitor.get_status(),
-            'drawdown_monitor_active': self.drawdown_monitor is not None,
-            'active_alarms': len(self.get_all_active_alarms()),
-            'components': {
-                'order_fill_processor': 'active',
-                'simplified_position_tracker': 'active', 
-                'price_alarms': 'active',
-                'risk_cleanup_manager': 'active',
-                'rsi_monitor': 'active' if self.rsi_monitor.enabled else 'disabled',
-                'drawdown_monitor': 'active' if self.drawdown_monitor else 'disabled'
-            },
-            'rsi_monitor_status': self.rsi_monitor.get_status() if hasattr(self, 'rsi_monitor') else None
-        }
-
-    async def _run_rsi_monitoring(self):
-        """
-        Run RSI monitoring for open positions and tracked symbols.
-        Only calculates RSI when there are new candles for efficiency.
-        """
-        try:
-            if not self.rsi_monitor.enabled:
-                return
-            
-            # Get symbols to monitor from open positions
-            symbols_to_monitor = set()
-            
-            # Add symbols from current positions
-            if self.cache.cached_positions:
-                for position in self.cache.cached_positions:
-                    symbol = position.get('symbol')
-                    if symbol and abs(float(position.get('contracts', 0))) > 1e-9:
-                        symbols_to_monitor.add(symbol)
-            
-            # Add default trading token if configured
-            default_symbol = f"{Config.DEFAULT_TRADING_TOKEN}/USDC:USDC"
-            symbols_to_monitor.add(default_symbol)
-            
-            # Convert to list and monitor
-            symbols_list = list(symbols_to_monitor)
-            
-            if symbols_list:
-                logger.debug(f"🔍 Running RSI monitoring for {len(symbols_list)} symbols: {symbols_list}")
-                await self.rsi_monitor.monitor_symbols_for_new_candles(symbols_list)
-            else:
-                logger.debug("📊 No symbols to monitor for RSI")
-                
-        except Exception as e:
-            logger.error(f"❌ Error in RSI monitoring: {e}")
-

+ 114 - 0
src/monitoring/monitoring_coordinator.py

@@ -0,0 +1,114 @@
+import asyncio
+import logging
+from typing import Optional
+
+from ..clients.hyperliquid_client import HyperliquidClient
+from ..notifications.notification_manager import NotificationManager
+from ..config.config import Config
+
+from .position_tracker import PositionTracker
+from .pending_orders_manager import PendingOrdersManager
+from .risk_manager import RiskManager
+from .alarm_manager import AlarmManager
+from .drawdown_monitor import DrawdownMonitor
+from .rsi_monitor import RSIMonitor
+
+logger = logging.getLogger(__name__)
+
+class MonitoringCoordinator:
+    """
+    Simplified monitoring coordinator that manages all monitoring components.
+    Replaces the complex unified monitor with a clean, focused approach.
+    """
+    
+    def __init__(self, hl_client: HyperliquidClient, notification_manager: NotificationManager, config: Config):
+        self.hl_client = hl_client
+        self.notification_manager = notification_manager
+        self.config = config
+        self.is_running = False
+        
+        # Initialize monitoring components
+        self.position_tracker = PositionTracker(hl_client, notification_manager)
+        self.pending_orders_manager = PendingOrdersManager(hl_client, notification_manager)
+        self.risk_manager = RiskManager(hl_client, notification_manager, config)
+        self.alarm_manager = AlarmManager(hl_client, notification_manager, config)
+        self.drawdown_monitor = DrawdownMonitor(hl_client, notification_manager, config)
+        self.rsi_monitor = RSIMonitor(hl_client, notification_manager, config)
+        
+    async def start(self):
+        """Start all monitoring components"""
+        if self.is_running:
+            return
+            
+        self.is_running = True
+        logger.info("Starting simplified monitoring system")
+        
+        try:
+            # Start all monitors
+            await self.position_tracker.start()
+            await self.pending_orders_manager.start()
+            await self.risk_manager.start()
+            await self.alarm_manager.start()
+            await self.drawdown_monitor.start()
+            await self.rsi_monitor.start()
+            
+            logger.info("All monitoring components started successfully")
+            
+        except Exception as e:
+            logger.error(f"Error starting monitoring system: {e}")
+            await self.stop()
+            raise
+            
+    async def stop(self):
+        """Stop all monitoring components"""
+        if not self.is_running:
+            return
+            
+        self.is_running = False
+        logger.info("Stopping monitoring system")
+        
+        # Stop all monitors
+        await self.position_tracker.stop()
+        await self.pending_orders_manager.stop()
+        await self.risk_manager.stop()
+        await self.alarm_manager.stop()
+        await self.drawdown_monitor.stop()
+        await self.rsi_monitor.stop()
+        
+        logger.info("Monitoring system stopped")
+        
+    async def add_pending_stop_loss(self, symbol: str, stop_price: float, size: float, side: str, expires_hours: int = 24):
+        """Add a pending stop loss order"""
+        await self.pending_orders_manager.add_pending_stop_loss(symbol, stop_price, size, side, expires_hours)
+        
+    async def cancel_pending_order(self, symbol: str) -> bool:
+        """Cancel pending order for symbol"""
+        return await self.pending_orders_manager.cancel_pending_order(symbol)
+        
+    async def get_pending_orders(self) -> list:
+        """Get all pending orders"""
+        return await self.pending_orders_manager.get_pending_orders()
+        
+    async def get_risk_status(self) -> dict:
+        """Get current risk status"""
+        return await self.risk_manager.get_risk_status()
+        
+    async def get_monitoring_status(self) -> dict:
+        """Get overall monitoring status"""
+        try:
+            return {
+                'is_running': self.is_running,
+                'components': {
+                    'position_tracker': self.position_tracker.is_running,
+                    'pending_orders_manager': self.pending_orders_manager.is_running,
+                    'risk_manager': self.risk_manager.is_running,
+                    'alarm_manager': self.alarm_manager.is_running if hasattr(self.alarm_manager, 'is_running') else True,
+                    'drawdown_monitor': self.drawdown_monitor.is_running if hasattr(self.drawdown_monitor, 'is_running') else True,
+                    'rsi_monitor': self.rsi_monitor.is_running if hasattr(self.rsi_monitor, 'is_running') else True
+                },
+                'pending_orders_count': len(await self.get_pending_orders()),
+                'risk_status': await self.get_risk_status()
+            }
+        except Exception as e:
+            logger.error(f"Error getting monitoring status: {e}")
+            return {'error': str(e)} 

+ 0 - 325
src/monitoring/order_fill_processor.py

@@ -1,325 +0,0 @@
-#!/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.")
-                            # Update order status to filled to prevent incorrect cancellation notifications
-                            stats.update_order_status(exchange_order_id=exchange_oid, new_status='filled')
-                            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:
-                    # Check if this order was actually filled by looking for recent fills 
-                    # This is normal behavior for external orders that get filled
-                    recent_fills = self.trading_engine.get_recent_fills()
-                    order_was_filled = False
-                    if recent_fills:
-                        for fill in recent_fills[-10:]:  # Check last 10 fills
-                            if fill.get('info', {}).get('oid') == exchange_oid:
-                                order_was_filled = True
-                                break
-                    
-                    if order_was_filled:
-                        logger.info(f"Order {exchange_oid} disappeared but was filled externally. This is normal for external orders.")
-                    else:
-                        logger.warning(f"Order {exchange_oid} disappeared from exchange but was not found in our DB. This might be an external order or a timing issue.")
-
-            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."""
-        try:
-            recent_fills = self.trading_engine.get_recent_fills()
-            if not recent_fills:
-                return False
-
-            # Check last 50 fills for the order ID to be thorough
-            for fill in recent_fills[-50:]:
-                try:
-                    exchange_order_id_from_fill = fill.get('info', {}).get('oid')
-                    
-                    # Direct order ID match is the most reliable indicator
-                    if exchange_order_id_from_fill == exchange_oid:
-                        symbol_from_fill = fill.get('symbol')
-                        side_from_fill = fill.get('side')
-                        amount_from_fill = float(fill.get('amount', 0))
-                        
-                        # Verify this fill matches our order details
-                        if (symbol_from_fill == order_in_db.get('symbol') and 
-                            side_from_fill == order_in_db.get('side') and 
-                            amount_from_fill > 0):
-                            
-                            logger.info(f"✅ Found matching fill {fill.get('id')} for order {exchange_oid}. Order was filled, not cancelled.")
-                            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'] 
-                    lifecycle_id = position_trade['trade_lifecycle_id']
-
-                    # Get the most up-to-date position size directly from the exchange
-                    # to ensure the SL covers the entire position, not just a partial fill.
-                    position = await self.trading_engine.find_position(token)
-                    if not position:
-                        logger.warning(f"Could not find an active position for {token} to place SL for lifecycle {lifecycle_id}. Skipping.")
-                        continue
-                    
-                    current_amount = float(position.get('contracts', 0))
-
-                    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
-                    
-                    # Check if a SL order for this lifecycle is already pending or open
-                    # This is a safeguard against race conditions where this check runs multiple times
-                    # before the DB has been updated with the linked stop_loss_order_id.
-                    if self.trading_engine.stats.get_lifecycle_by_sl_order_id(lifecycle_id):
-                         logger.info(f"SL activation for lifecycle {lifecycle_id} skipped: An SL order is already linked.")
-                         continue
-                    
-                    # Additional check: query orders from DB to see if an SL is pending submission for this lifecycle
-                    pending_sl_orders = self.trading_engine.stats.get_orders_by_status('pending_submission', order_type_filter='limit', parent_bot_order_ref_id=lifecycle_id)
-                    if any(pso for pso in pending_sl_orders if pso.get('parent_bot_order_ref_id') == lifecycle_id):
-                        logger.info(f"SL activation for lifecycle {lifecycle_id} skipped: An SL order is already pending submission in the DB.")
-                        continue
-
-                    logger.info(f"Attempting to place LIMIT stop loss for lifecycle {lifecycle_id} ({position_side} {token} @ SL {await 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 = await 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 = await 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 = await formatter.format_price_with_symbol(current_price_for_notification, token) if current_price_for_notification else 'Unknown'
-                            stop_loss_price_str_notify = await 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: {await 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) 

+ 278 - 0
src/monitoring/pending_orders_manager.py

@@ -0,0 +1,278 @@
+import asyncio
+import logging
+from typing import Dict, List, Optional, Any
+from datetime import datetime, timezone, timedelta
+import sqlite3
+
+from ..clients.hyperliquid_client import HyperliquidClient
+from ..notifications.notification_manager import NotificationManager
+
+logger = logging.getLogger(__name__)
+
+class PendingOrdersManager:
+    """
+    Manages pending stop loss orders from /long commands with sl: parameter.
+    Places stop loss orders when positions are opened.
+    """
+    
+    def __init__(self, hl_client: HyperliquidClient, notification_manager: NotificationManager):
+        self.hl_client = hl_client
+        self.notification_manager = notification_manager
+        self.db_path = "data/pending_orders.db"
+        self.is_running = False
+        
+        # Initialize database
+        self._init_database()
+        
+    def _init_database(self):
+        """Initialize pending orders database"""
+        try:
+            with sqlite3.connect(self.db_path) as conn:
+                conn.execute("""
+                    CREATE TABLE IF NOT EXISTS pending_stop_loss (
+                        id INTEGER PRIMARY KEY AUTOINCREMENT,
+                        symbol TEXT NOT NULL,
+                        stop_price REAL NOT NULL,
+                        size REAL NOT NULL,
+                        side TEXT NOT NULL,
+                        created_at TEXT NOT NULL,
+                        expires_at TEXT,
+                        status TEXT DEFAULT 'pending',
+                        order_id TEXT,
+                        placed_at TEXT
+                    )
+                """)
+                conn.commit()
+                logger.info("Pending orders database initialized")
+        except Exception as e:
+            logger.error(f"Error initializing pending orders database: {e}")
+            
+    async def start(self):
+        """Start pending orders manager"""
+        if self.is_running:
+            return
+            
+        self.is_running = True
+        logger.info("Starting pending orders manager")
+        
+        # Start monitoring loop
+        asyncio.create_task(self._monitoring_loop())
+        
+    async def stop(self):
+        """Stop pending orders manager"""
+        self.is_running = False
+        logger.info("Stopping pending orders manager")
+        
+    async def add_pending_stop_loss(self, symbol: str, stop_price: float, size: float, side: str, expires_hours: int = 24):
+        """Add a pending stop loss order"""
+        try:
+            created_at = datetime.now(timezone.utc)
+            expires_at = created_at + timedelta(hours=expires_hours)
+            
+            with sqlite3.connect(self.db_path) as conn:
+                conn.execute("""
+                    INSERT INTO pending_stop_loss 
+                    (symbol, stop_price, size, side, created_at, expires_at)
+                    VALUES (?, ?, ?, ?, ?, ?)
+                """, (symbol, stop_price, size, side, created_at.isoformat(), expires_at.isoformat()))
+                conn.commit()
+                
+            logger.info(f"Added pending stop loss: {symbol} {side} {size} @ ${stop_price}")
+            
+            message = (
+                f"⏳ Pending Stop Loss Added\n"
+                f"Token: {symbol}\n"
+                f"Side: {side}\n"
+                f"Size: {size}\n"
+                f"Stop Price: ${stop_price:.4f}\n"
+                f"Expires: {expires_hours}h"
+            )
+            await self.notification_manager.send_notification(message)
+            
+        except Exception as e:
+            logger.error(f"Error adding pending stop loss: {e}")
+            
+    async def _monitoring_loop(self):
+        """Main monitoring loop"""
+        while self.is_running:
+            try:
+                await self._check_pending_orders()
+                await self._cleanup_expired_orders()
+                await asyncio.sleep(5)  # Check every 5 seconds
+            except Exception as e:
+                logger.error(f"Error in pending orders monitoring loop: {e}")
+                await asyncio.sleep(10)
+                
+    async def _check_pending_orders(self):
+        """Check if any pending orders should be placed"""
+        try:
+            # Get current positions
+            user_state = await self.hl_client.get_user_state()
+            if not user_state or 'assetPositions' not in user_state:
+                return
+                
+            current_positions = {}
+            for position in user_state['assetPositions']:
+                if position.get('position', {}).get('szi') != '0':
+                    symbol = position.get('position', {}).get('coin', '')
+                    if symbol:
+                        size = float(position.get('position', {}).get('szi', 0))
+                        current_positions[symbol] = {
+                            'size': size,
+                            'side': 'long' if size > 0 else 'short'
+                        }
+            
+            # Check pending orders against current positions
+            with sqlite3.connect(self.db_path) as conn:
+                cursor = conn.execute("""
+                    SELECT id, symbol, stop_price, size, side 
+                    FROM pending_stop_loss 
+                    WHERE status = 'pending'
+                """)
+                pending_orders = cursor.fetchall()
+                
+            for order_id, symbol, stop_price, size, side in pending_orders:
+                if symbol in current_positions:
+                    current_pos = current_positions[symbol]
+                    
+                    # Check if position matches pending order
+                    if (current_pos['side'] == side.lower() and 
+                        abs(current_pos['size']) >= abs(size) * 0.95):  # Allow 5% tolerance
+                        
+                        await self._place_stop_loss_order(order_id, symbol, stop_price, 
+                                                        current_pos['size'], side)
+                        
+        except Exception as e:
+            logger.error(f"Error checking pending orders: {e}")
+            
+    async def _place_stop_loss_order(self, pending_id: int, symbol: str, stop_price: float, 
+                                   position_size: float, side: str):
+        """Place stop loss order on exchange"""
+        try:
+            # Determine stop loss side (opposite of position)
+            sl_side = 'sell' if side.lower() == 'long' else 'buy'
+            sl_size = abs(position_size)
+            
+            # Place stop loss order
+            order_result = await self.hl_client.place_order(
+                symbol=symbol,
+                side=sl_side,
+                size=sl_size,
+                order_type='stop',
+                stop_px=stop_price,
+                reduce_only=True
+            )
+            
+            if order_result and 'response' in order_result:
+                response = order_result['response']
+                if response.get('type') == 'order' and response.get('data', {}).get('statuses', [{}])[0].get('filled'):
+                    # Order placed successfully
+                    order_id = str(response.get('data', {}).get('statuses', [{}])[0].get('resting', {}).get('oid', ''))
+                    
+                    # Update database
+                    with sqlite3.connect(self.db_path) as conn:
+                        conn.execute("""
+                            UPDATE pending_stop_loss 
+                            SET status = 'placed', order_id = ?, placed_at = ?
+                            WHERE id = ?
+                        """, (order_id, datetime.now(timezone.utc).isoformat(), pending_id))
+                        conn.commit()
+                        
+                    message = (
+                        f"✅ Stop Loss Placed\n"
+                        f"Token: {symbol}\n"
+                        f"Size: {sl_size:.4f}\n"
+                        f"Stop Price: ${stop_price:.4f}\n"
+                        f"Order ID: {order_id}"
+                    )
+                    await self.notification_manager.send_notification(message)
+                    logger.info(f"Placed stop loss: {symbol} {sl_size} @ ${stop_price}")
+                    
+                else:
+                    logger.error(f"Failed to place stop loss for {symbol}: {response}")
+                    
+        except Exception as e:
+            logger.error(f"Error placing stop loss order for {symbol}: {e}")
+            
+    async def _cleanup_expired_orders(self):
+        """Remove expired pending orders"""
+        try:
+            current_time = datetime.now(timezone.utc)
+            
+            with sqlite3.connect(self.db_path) as conn:
+                # Get expired orders
+                cursor = conn.execute("""
+                    SELECT id, symbol, stop_price FROM pending_stop_loss 
+                    WHERE status = 'pending' AND expires_at < ?
+                """, (current_time.isoformat(),))
+                expired_orders = cursor.fetchall()
+                
+                if expired_orders:
+                    # Mark as expired
+                    conn.execute("""
+                        UPDATE pending_stop_loss 
+                        SET status = 'expired' 
+                        WHERE status = 'pending' AND expires_at < ?
+                    """, (current_time.isoformat(),))
+                    conn.commit()
+                    
+                    for order_id, symbol, stop_price in expired_orders:
+                        logger.info(f"Expired pending stop loss: {symbol} @ ${stop_price}")
+                        
+        except Exception as e:
+            logger.error(f"Error cleaning up expired orders: {e}")
+            
+    async def get_pending_orders(self) -> List[Dict]:
+        """Get all pending orders"""
+        try:
+            with sqlite3.connect(self.db_path) as conn:
+                cursor = conn.execute("""
+                    SELECT symbol, stop_price, size, side, created_at, expires_at, status
+                    FROM pending_stop_loss 
+                    ORDER BY created_at DESC
+                """)
+                orders = cursor.fetchall()
+                
+            return [
+                {
+                    'symbol': row[0],
+                    'stop_price': row[1],
+                    'size': row[2],
+                    'side': row[3],
+                    'created_at': row[4],
+                    'expires_at': row[5],
+                    'status': row[6]
+                }
+                for row in orders
+            ]
+            
+        except Exception as e:
+            logger.error(f"Error getting pending orders: {e}")
+            return []
+            
+    async def cancel_pending_order(self, symbol: str) -> bool:
+        """Cancel pending order for symbol"""
+        try:
+            with sqlite3.connect(self.db_path) as conn:
+                cursor = conn.execute("""
+                    SELECT id FROM pending_stop_loss 
+                    WHERE symbol = ? AND status = 'pending'
+                """, (symbol,))
+                order = cursor.fetchone()
+                
+                if order:
+                    conn.execute("""
+                        UPDATE pending_stop_loss 
+                        SET status = 'cancelled' 
+                        WHERE id = ?
+                    """, (order[0],))
+                    conn.commit()
+                    
+                    logger.info(f"Cancelled pending stop loss for {symbol}")
+                    return True
+                    
+            return False
+            
+        except Exception as e:
+            logger.error(f"Error cancelling pending order for {symbol}: {e}")
+            return False 

+ 0 - 1341
src/monitoring/position_monitor.py

@@ -1,1341 +0,0 @@
-#!/usr/bin/env python3
-"""
-Monitors positions, 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 PositionMonitor:
-    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 run_monitor_cycle(self):
-        """Runs a full monitoring cycle."""
-        await self._check_external_trades()
-        await self._check_price_alarms()
-        await self._reconcile_positions()
-
-    def _safe_get_positions(self) -> Optional[List[Dict[str, Any]]]:
-        """Safely get positions from trading engine, returning None on API failures instead of empty list."""
-        try:
-            return self.trading_engine.get_positions()
-        except Exception as e:
-            logger.warning(f"⚠️ Failed to fetch positions in external event monitor: {e}")
-            return None
-
-    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 = await 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 _determine_position_action_type(self, full_symbol: str, side_from_fill: str, 
-                                            amount_from_fill: float, existing_lc: Optional[Dict] = None) -> str:
-        """
-        Determine the type of position action based on current state and fill details.
-        Returns one of: 'position_opened', 'position_closed', 'position_increased', 'position_decreased'
-        """
-        try:
-            # Get current position from exchange
-            current_positions = self._safe_get_positions()
-            if current_positions is None:
-                logger.warning(f"⚠️ Failed to fetch positions for {full_symbol} analysis - returning external_unmatched")
-                return 'external_unmatched'
-            
-            current_exchange_position = None
-            for pos in current_positions:
-                if pos.get('symbol') == full_symbol:
-                    current_exchange_position = pos
-                    break
-            
-            current_size = 0.0
-            if current_exchange_position:
-                current_size = abs(float(current_exchange_position.get('contracts', 0)))
-            
-            # If no existing lifecycle, this is a position opening
-            if not existing_lc:
-                logger.debug(f"🔍 Position analysis: {full_symbol} no existing lifecycle, current size: {current_size}")
-                if current_size > 1e-9:  # Position exists on exchange
-                    return 'position_opened'
-                else:
-                    return 'external_unmatched'
-            
-            # Get previous position size from lifecycle
-            previous_size = existing_lc.get('current_position_size', 0)
-            lc_position_side = existing_lc.get('position_side')
-            
-            logger.debug(f"🔍 Position analysis: {full_symbol} {side_from_fill} {amount_from_fill}")
-            logger.debug(f"  Lifecycle side: {lc_position_side}, previous size: {previous_size}, current size: {current_size}")
-            
-            # Check if this is a closing trade (opposite side)
-            is_closing_trade = False
-            if lc_position_side == 'long' and side_from_fill.lower() == 'sell':
-                is_closing_trade = True
-            elif lc_position_side == 'short' and side_from_fill.lower() == 'buy':
-                is_closing_trade = True
-            
-            logger.debug(f"  Is closing trade: {is_closing_trade}")
-            
-            if is_closing_trade:
-                if current_size < 1e-9:  # Position is now closed
-                    logger.debug(f"  → Position closed (current_size < 1e-9)")
-                    return 'position_closed'
-                elif current_size < previous_size - 1e-9:  # Position reduced but not closed
-                    logger.debug(f"  → Position decreased (current_size {current_size} < previous_size - 1e-9 {previous_size - 1e-9})")
-                    return 'position_decreased'
-            else:
-                # Same side trade - position increase
-                logger.debug(f"  Same side trade check: current_size {current_size} > previous_size + 1e-9 {previous_size + 1e-9}?")
-                if current_size > previous_size + 1e-9:
-                    logger.debug(f"  → Position increased")
-                    return 'position_increased'
-                else:
-                    logger.debug(f"  → Size check failed, not enough increase")
-            
-            # Default fallback
-            logger.debug(f"  → Fallback to external_unmatched")
-            return 'external_unmatched'
-            
-        except Exception as e:
-            logger.error(f"Error determining position action type: {e}")
-            return 'external_unmatched'
-
-    async def _update_lifecycle_position_size(self, lifecycle_id: str, new_size: float) -> bool:
-        """Update the current position size in the lifecycle."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                return False
-            
-            # Update the current position size
-            success = stats.trade_manager.update_trade_market_data(
-                lifecycle_id, current_position_size=new_size
-            )
-            return success
-        except Exception as e:
-            logger.error(f"Error updating lifecycle position size: {e}")
-            return False
-
-    async def _send_position_change_notification(self, full_symbol: str, side_from_fill: str, 
-                                               amount_from_fill: float, price_from_fill: float, 
-                                               action_type: str, timestamp_dt: datetime, 
-                                               existing_lc: Optional[Dict] = None, 
-                                               realized_pnl: Optional[float] = None):
-        """Send position change notification."""
-        try:
-            if not self.notification_manager:
-                return
-                
-            token = full_symbol.split('/')[0] if '/' in full_symbol else full_symbol.split(':')[0]
-            time_str = timestamp_dt.strftime('%Y-%m-%d %H:%M:%S UTC')
-            formatter = get_formatter()
-            
-            if action_type == 'position_closed' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                entry_price = existing_lc.get('entry_price', 0)
-                pnl_emoji = "🟢" if realized_pnl and realized_pnl >= 0 else "🔴"
-                pnl_text = f"{await formatter.format_price_with_symbol(realized_pnl)}" if realized_pnl is not None else "N/A"
-                
-                # Use the last known ROE from heartbeat data (stored in lifecycle)
-                stored_roe = existing_lc.get('roe_percentage')
-                if stored_roe is not None:
-                    try:
-                        roe = float(stored_roe)
-                        roe_text = f" ({roe:+.2f}%)"
-                        logger.debug(f"Using stored ROE from heartbeat for {full_symbol}: {roe:+.2f}%")
-                    except (ValueError, TypeError):
-                        logger.warning(f"Could not parse stored ROE value: {stored_roe} for {full_symbol}")
-                        roe_text = ""
-                else:
-                    logger.debug(f"No stored ROE available for {full_symbol}")
-                    roe_text = ""
-                
-                message = f"""
-🎯 <b>Position Closed</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Closed: {await formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {await formatter.format_price_with_symbol(entry_price, token)}
-• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-{pnl_emoji} <b>P&L:</b> {pnl_text}{roe_text}
-✅ <b>Status:</b> {position_side} position closed externally
-⏰ <b>Time:</b> {time_str}
-
-📊 Use /stats to view updated performance
-                """
-                
-            elif action_type == 'position_opened':
-                position_side = 'LONG' if side_from_fill.lower() == 'buy' else 'SHORT'
-                message = f"""
-🚀 <b>Position Opened (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size: {await formatter.format_amount(amount_from_fill, token)}
-• Entry Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Position Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-✅ <b>Status:</b> New {position_side} position opened externally
-⏰ <b>Time:</b> {time_str}
-
-📱 Use /positions to view all positions
-                """
-                
-            elif action_type == 'position_increased' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                previous_size = existing_lc.get('current_position_size', 0)
-                # Get current size from exchange
-                current_positions = self._safe_get_positions()
-                if current_positions is None:
-                    # Skip notification if we can't get position data
-                    logger.warning(f"⚠️ Failed to fetch positions for notification - skipping {action_type} notification")
-                    return
-                current_size = 0
-                for pos in current_positions:
-                    if pos.get('symbol') == full_symbol:
-                        current_size = abs(float(pos.get('contracts', 0)))
-                        break
-                
-                message = f"""
-📈 <b>Position Increased (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Added: {await formatter.format_amount(amount_from_fill, token)}
-• Add Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {await formatter.format_amount(previous_size, token)}
-• New Size: {await formatter.format_amount(current_size, token)}
-• Add Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-📈 <b>Status:</b> {position_side} position size increased externally
-⏰ <b>Time:</b> {time_str}
-
-📈 Use /positions to view current position
-                """
-                
-            elif action_type == 'position_decreased' and existing_lc:
-                position_side = existing_lc.get('position_side', 'unknown').upper()
-                previous_size = existing_lc.get('current_position_size', 0)
-                entry_price = existing_lc.get('entry_price', 0)
-                
-                # Get current size from exchange
-                current_positions = self._safe_get_positions()
-                if current_positions is None:
-                    # Skip notification if we can't get position data
-                    logger.warning(f"⚠️ Failed to fetch positions for notification - skipping {action_type} notification")
-                    return
-                current_size = 0
-                for pos in current_positions:
-                    if pos.get('symbol') == full_symbol:
-                        current_size = abs(float(pos.get('contracts', 0)))
-                        break
-                
-                # Calculate partial PnL for the reduced amount
-                partial_pnl = 0
-                if entry_price > 0:
-                    if position_side == 'LONG':
-                        partial_pnl = amount_from_fill * (price_from_fill - entry_price)
-                    else:  # SHORT
-                        partial_pnl = amount_from_fill * (entry_price - price_from_fill)
-                
-                pnl_emoji = "🟢" if partial_pnl >= 0 else "🔴"
-                
-                # Calculate ROE for the partial close
-                roe_text = ""
-                if entry_price > 0 and amount_from_fill > 0:
-                    cost_basis = amount_from_fill * entry_price
-                    roe = (partial_pnl / cost_basis) * 100
-                    roe_text = f" ({roe:+.2f}%)"
-                
-                message = f"""
-📉 <b>Position Decreased (External)</b>
-
-📊 <b>Trade Details:</b>
-• Token: {token}
-• Direction: {position_side}
-• Size Reduced: {await formatter.format_amount(amount_from_fill, token)}
-• Exit Price: {await formatter.format_price_with_symbol(price_from_fill, token)}
-• Previous Size: {await formatter.format_amount(previous_size, token)}
-• Remaining Size: {await formatter.format_amount(current_size, token)}
-• Exit Value: {await formatter.format_price_with_symbol(amount_from_fill * price_from_fill)}
-
-{pnl_emoji} <b>Partial P&L:</b> {await formatter.format_price_with_symbol(partial_pnl)}{roe_text}
-📉 <b>Status:</b> {position_side} position size decreased externally  
-⏰ <b>Time:</b> {time_str}
-
-📊 Position remains open. Use /positions to view details
-                """
-            else:
-                # No fallback notification sent - only position-based notifications per user preference
-                logger.debug(f"No notification sent for action_type: {action_type}")
-                return
-            
-            await self.notification_manager.send_generic_notification(message.strip())
-            
-        except Exception as e:
-            logger.error(f"Error sending position change notification: {e}")
-    
-    async def _auto_sync_single_position(self, symbol: str, exchange_position: Dict[str, Any], stats) -> bool:
-        """Auto-sync a single orphaned position to create a lifecycle record."""
-        try:
-            import uuid
-            from src.utils.token_display_formatter import get_formatter
-            
-            formatter = get_formatter()
-            contracts_abs = abs(float(exchange_position.get('contracts', 0)))
-            
-            if contracts_abs <= 1e-9:
-                return False
-            
-            entry_price_from_exchange = float(exchange_position.get('entryPrice', 0)) or float(exchange_position.get('entryPx', 0))
-            
-            # Determine position side
-            position_side, order_side = '', ''
-            ccxt_side = exchange_position.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:
-                contracts_val = float(exchange_position.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:
-                    return False
-            
-            if not position_side:
-                logger.error(f"AUTO-SYNC: Could not determine position side for {symbol}.")
-                return False
-            
-            final_entry_price = entry_price_from_exchange
-            if not final_entry_price or final_entry_price <= 0:
-                # Fallback to a reasonable estimate (current mark price)
-                mark_price = float(exchange_position.get('markPrice', 0)) or float(exchange_position.get('markPx', 0))
-                if mark_price > 0:
-                    final_entry_price = mark_price
-                else:
-                    logger.error(f"AUTO-SYNC: Could not determine entry price for {symbol}.")
-                    return False
-            
-            logger.info(f"🔄 AUTO-SYNC: Creating lifecycle for {symbol} {position_side.upper()} {contracts_abs} @ {await formatter.format_price_with_symbol(final_entry_price, symbol)}")
-            
-            unique_sync_id = str(uuid.uuid4())[:8]
-            lifecycle_id = stats.create_trade_lifecycle(
-                symbol=symbol, 
-                side=order_side, 
-                entry_order_id=f"external_sync_{unique_sync_id}",
-                trade_type='external_sync'
-            )
-            
-            if lifecycle_id:
-                success = await stats.update_trade_position_opened(
-                    lifecycle_id, 
-                    final_entry_price, 
-                    contracts_abs,
-                    f"external_fill_sync_{unique_sync_id}"
-                )
-                
-                if success:
-                    logger.info(f"✅ AUTO-SYNC: Successfully synced position for {symbol} (Lifecycle: {lifecycle_id[:8]})")
-                    
-                    # Send position opened notification for auto-synced position
-                    try:
-                        await self._send_position_change_notification(
-                            symbol, order_side, contracts_abs, final_entry_price,
-                            'position_opened', datetime.now(timezone.utc)
-                        )
-                        logger.info(f"📨 AUTO-SYNC: Sent position opened notification for {symbol}")
-                    except Exception as e:
-                        logger.error(f"❌ AUTO-SYNC: Failed to send notification for {symbol}: {e}")
-                    
-                    return True
-                else:
-                    logger.error(f"❌ AUTO-SYNC: Failed to update lifecycle to 'position_opened' for {symbol}")
-            else:
-                logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for {symbol}")
-            
-            return False
-            
-        except Exception as e:
-            logger.error(f"❌ AUTO-SYNC: Error syncing position for {symbol}: {e}")
-            return False
-    
-    async def _check_external_trades(self):
-        """Check for external trades and update internal tracking."""
-        stats = self.trading_engine.stats
-        if not stats:
-            logger.warning("No stats manager available for external trade checking")
-            return
-
-        try:
-            external_trades_processed = 0
-            symbols_with_fills = set()
-            processed_fills_this_cycle = set()  # Track fills processed in this cycle
-
-            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
-                    
-                    # Check if this fill has already been processed to prevent duplicates
-                    if trade_id and stats.has_exchange_fill_been_processed(str(trade_id)):
-                        logger.debug(f"Skipping already processed fill: {trade_id}")
-                        continue
-                    
-                    # Check if this fill was already processed in this cycle
-                    if trade_id and trade_id in processed_fills_this_cycle:
-                        logger.debug(f"Skipping fill already processed in this cycle: {trade_id}")
-                        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')
-
-                    # First check if this is a pending entry order 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 = await 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)
-                                
-                                # Send position opened notification (this is a bot-initiated position)
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    'position_opened', timestamp_dt
-                                )
-                                
-                                # Mark fill as processed in this cycle
-                                if trade_id:
-                                    processed_fills_this_cycle.add(trade_id)
-                            fill_processed_this_iteration = True
-
-                    # Check if this is a bot order to increase an existing position
-                    if not fill_processed_this_iteration and 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')
-                            
-                            # Check if this is a limit order that should increase an existing position
-                            if order_type == 'limit':
-                                existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                                if existing_lc:
-                                    lc_pos_side = existing_lc.get('position_side')
-                                    # Check if this is a same-side order (position increase)
-                                    if ((lc_pos_side == 'long' and order_side == 'buy' and side_from_fill == 'buy') or 
-                                        (lc_pos_side == 'short' and order_side == 'sell' and side_from_fill == 'sell')):
-                                        
-                                        # Update existing position size
-                                        current_positions = self._safe_get_positions()
-                                        if current_positions is not None:
-                                            new_size = 0
-                                            for pos in current_positions:
-                                                if pos.get('symbol') == full_symbol:
-                                                    new_size = abs(float(pos.get('contracts', 0)))
-                                                    break
-                                            
-                                            # Update lifecycle with new position size
-                                            await self._update_lifecycle_position_size(existing_lc['trade_lifecycle_id'], new_size)
-                                            
-                                            # Mark order as filled
-                                            stats.update_order_status(order_db_id=bot_order_for_fill['id'], new_status='filled', amount_filled_increment=amount_from_fill)
-                                            
-                                            # Send position increased notification
-                                            await self._send_position_change_notification(
-                                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                                'position_increased', timestamp_dt, existing_lc
-                                            )
-                                            
-                                            logger.info(f"📈 Position INCREASED: {full_symbol} by {amount_from_fill} for lifecycle {existing_lc['trade_lifecycle_id'][:8]}...")
-                                            symbols_with_fills.add(token)
-                                            
-                                            # Mark fill as processed in this cycle
-                                            if trade_id:
-                                                processed_fills_this_cycle.add(trade_id)
-                                            fill_processed_this_iteration = True
-
-                    # Check if this is a known bot order (SL/TP/exit)
-                    if not fill_processed_this_iteration and exchange_order_id_from_fill:
-                        active_lc = None
-                        closure_reason_action_type = None
-                        bot_order_db_id_to_update = None
-
-                        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']}.")
-
-                        # Process known bot order fills
-                        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: {await formatter.format_price_with_symbol(realized_pnl)}")
-                                symbols_with_fills.add(token)
-                                
-                                # Send notification immediately with correct PnL from actual fill
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    'position_closed', timestamp_dt, active_lc, realized_pnl
-                                )
-                                
-                                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)
-                                
-                                # Mark fill as processed in this cycle
-                                if trade_id:
-                                    processed_fills_this_cycle.add(trade_id)
-                                fill_processed_this_iteration = True
-
-                    # Check for external stop losses
-                    if not fill_processed_this_iteration:
-                        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 @ {await 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: {await 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]
-                                    
-                                    # Mark fill as processed in this cycle
-                                    if trade_id:
-                                        processed_fills_this_cycle.add(trade_id)
-                                    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.")
-
-                    # NEW: Enhanced external trade processing with position state detection
-                    if not fill_processed_this_iteration:
-                        existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        
-                        # If no lifecycle exists but we have a position on exchange, try to auto-sync first
-                        if not existing_lc:
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for external trade detection - skipping this fill")
-                                continue
-                            exchange_position = None
-                            for pos in current_positions:
-                                if pos.get('symbol') == full_symbol:
-                                    exchange_position = pos
-                                    break
-                            
-                            if exchange_position and abs(float(exchange_position.get('contracts', 0))) > 1e-9:
-                                logger.info(f"🔄 AUTO-SYNC: Position exists on exchange for {full_symbol} but no lifecycle found. Auto-syncing before processing fill.")
-                                success = await self._auto_sync_single_position(full_symbol, exchange_position, stats)
-                                if success:
-                                    # Re-check for lifecycle after auto-sync
-                                    existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                        
-                        action_type = await self._determine_position_action_type(
-                            full_symbol, side_from_fill, amount_from_fill, existing_lc
-                        )
-                        
-                        logger.info(f"🔍 External fill analysis: {full_symbol} {side_from_fill} {amount_from_fill} -> {action_type}")
-                        
-                        # Additional debug logging for position changes
-                        if existing_lc:
-                            previous_size = existing_lc.get('current_position_size', 0)
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for debug logging - skipping debug info")
-                                # Set defaults to avoid reference errors
-                                current_size = previous_size
-                            else:
-                                current_size = 0
-                                for pos in current_positions:
-                                    if pos.get('symbol') == full_symbol:
-                                        current_size = abs(float(pos.get('contracts', 0)))
-                                        break
-                                logger.info(f"📊 Position size change: {previous_size} -> {current_size} (diff: {current_size - previous_size})")
-                                logger.info(f"🎯 Expected change based on fill: {'+' if side_from_fill.lower() == 'buy' else '-'}{amount_from_fill}")
-                                
-                                # If lifecycle is already closed, we should not re-process as a new closure.
-                                if existing_lc.get('status') == 'position_closed':
-                                    logger.info(f"ℹ️ Fill {trade_id} received for already closed lifecycle {existing_lc['trade_lifecycle_id']}. Recording fill and skipping further action.")
-                                    stats.record_trade(
-                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                        exchange_fill_id=trade_id, trade_type="external_fill_for_closed_pos",
-                                        pnl=None, timestamp=timestamp_dt.isoformat(),
-                                        linked_order_table_id_to_link=None 
-                                    )
-                                    fill_processed_this_iteration = True
-                                    continue # Skip to next fill
-
-                                # Check if this might be a position decrease that was misclassified
-                                if (action_type == 'external_unmatched' and 
-                                    existing_lc.get('position_side') == 'long' and 
-                                    side_from_fill.lower() == 'sell' and 
-                                    current_size < previous_size):
-                                    logger.warning(f"⚠️ Potential misclassification: {full_symbol} {side_from_fill} looks like position decrease but classified as external_unmatched")
-                                    # Force re-check with proper parameters
-                                    action_type = 'position_decreased'
-                                    logger.info(f"🔄 Corrected action_type to: {action_type}")
-                                elif (action_type == 'external_unmatched' and 
-                                      existing_lc.get('position_side') == 'long' and 
-                                      side_from_fill.lower() == 'buy' and 
-                                      current_size > previous_size):
-                                    logger.warning(f"⚠️ Potential misclassification: {full_symbol} {side_from_fill} looks like position increase but classified as external_unmatched")
-                                    action_type = 'position_increased'
-                                    logger.info(f"🔄 Corrected action_type to: {action_type}")
-                        
-                        if action_type == 'position_opened':
-                            # Create new lifecycle for external position
-                            lifecycle_id = stats.create_trade_lifecycle(
-                                symbol=full_symbol,
-                                side=side_from_fill,
-                                entry_order_id=exchange_order_id_from_fill or f"external_{trade_id}",
-                                trade_type="external"
-                            )
-                            
-                            if lifecycle_id:
-                                success = stats.update_trade_position_opened(
-                                    lifecycle_id=lifecycle_id,
-                                    entry_price=price_from_fill,
-                                    entry_amount=amount_from_fill,
-                                    exchange_fill_id=trade_id
-                                )
-                                
-                                if success:
-                                    logger.info(f"📈 Created and opened new external lifecycle: {lifecycle_id[:8]} for {full_symbol}")
-                                    symbols_with_fills.add(token)
-                                    
-                                    # Send position opened notification  
-                                    await self._send_position_change_notification(
-                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                        action_type, timestamp_dt
-                                    )
-                                    
-                                    # Mark fill as processed in this cycle
-                                    if trade_id:
-                                        processed_fills_this_cycle.add(trade_id)
-                                    fill_processed_this_iteration = True
-                        
-                        elif action_type == 'position_closed' and existing_lc:
-                            # Close existing lifecycle
-                            lc_id = existing_lc['trade_lifecycle_id']
-                            lc_entry_price = existing_lc.get('entry_price', 0)
-                            lc_position_side = existing_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 (External): {lc_id}. PNL: {await formatter.format_price_with_symbol(realized_pnl)}")
-                                symbols_with_fills.add(token)
-                                
-                                # Send position closed notification
-                                await self._send_position_change_notification(
-                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                    action_type, timestamp_dt, existing_lc, realized_pnl
-                                )
-                                
-                                stats.migrate_trade_to_aggregated_stats(lc_id)
-                                
-                                # Mark fill as processed in this cycle
-                                if trade_id:
-                                    processed_fills_this_cycle.add(trade_id)
-                                fill_processed_this_iteration = True
-                        
-                        elif action_type in ['position_increased', 'position_decreased'] and existing_lc:
-                            # Update lifecycle position size and send notification
-                            current_positions = self._safe_get_positions()
-                            if current_positions is None:
-                                logger.warning("⚠️ Failed to fetch positions for size update - skipping position change processing")
-                                continue
-                            new_size = 0
-                            for pos in current_positions:
-                                if pos.get('symbol') == full_symbol:
-                                    new_size = abs(float(pos.get('contracts', 0)))
-                                    break
-                            
-                            # Update lifecycle with new position size
-                            await self._update_lifecycle_position_size(existing_lc['trade_lifecycle_id'], new_size)
-                            
-                            # Send appropriate notification
-                            await self._send_position_change_notification(
-                                full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                action_type, timestamp_dt, existing_lc
-                            )
-                            
-                            symbols_with_fills.add(token)
-                            
-                            # Mark fill as processed in this cycle
-                            if trade_id:
-                                processed_fills_this_cycle.add(trade_id)
-                            fill_processed_this_iteration = True
-                            logger.info(f"📊 Position {action_type}: {full_symbol} new size: {new_size}")
-
-                    # Fallback for unmatched external trades
-                    if not fill_processed_this_iteration:
-                        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.debug(f"Position {full_symbol} found in open positions but no active lifecycle - likely auto-sync failed or timing issue for fill {trade_id}")
-                        
-                        # Record as unmatched external trade
-                        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')
-                        
-                        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)")
-                        
-                        # Mark fill as processed in this cycle
-                        if trade_id:
-                            processed_fills_this_cycle.add(trade_id)
-                        
-                        # No notification sent for unmatched external trades per user preference
-                        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) 
-
-    async def _reconcile_positions(self):
-        """Main method - check all positions for changes and send notifications."""
-        try:
-            stats = self.trading_engine.get_stats()
-            if not stats:
-                logger.warning("TradingStats not available")
-                return
-            
-            # Get current exchange positions
-            exchange_positions = self.trading_engine.get_positions()
-            
-            # Handle API failures gracefully - don't treat None as empty positions
-            if exchange_positions is None:
-                logger.warning("⚠️ Failed to fetch exchange positions - skipping position change detection to avoid false closures")
-                return
-            
-            # Get current DB positions (trades with status='position_opened')
-            db_positions = stats.get_open_positions()
-            
-            # Create lookup maps
-            exchange_map = {pos['symbol']: pos for pos in exchange_positions if abs(float(pos.get('contracts', 0))) > 1e-9}
-            db_map = {pos['symbol']: pos for pos in db_positions}
-            
-            all_symbols = set(exchange_map.keys()) | set(db_map.keys())
-            
-            for symbol in all_symbols:
-                await self._check_symbol_position_change(symbol, exchange_map.get(symbol), db_map.get(symbol), stats)
-            
-        except Exception as e:
-            logger.error(f"❌ Error checking position changes: {e}")
-    
-    async def _check_symbol_position_change(self, symbol: str, exchange_pos: Optional[Dict], 
-                                           db_pos: Optional[Dict], stats) -> None:
-        """Check position changes for a single symbol."""
-        try:
-            current_time = datetime.now(timezone.utc)
-            
-            # Case 1: New position (exchange has, DB doesn't)
-            if exchange_pos and not db_pos:
-                await self._handle_position_opened(symbol, exchange_pos, stats, current_time)
-            
-            # Case 2: Position closed (DB has, exchange doesn't) 
-            elif db_pos and not exchange_pos:
-                await self._handle_position_closed(symbol, db_pos, stats, current_time)
-            
-            # Case 3: Position size changed (both exist, different sizes)
-            elif exchange_pos and db_pos:
-                await self._handle_position_size_change(symbol, exchange_pos, db_pos, stats, current_time)
-            
-            # Case 4: Both None - no action needed
-            
-        except Exception as e:
-            logger.error(f"❌ Error checking position change for {symbol}: {e}")
-    
-    async def _handle_position_opened(self, symbol: str, exchange_pos: Dict, stats, timestamp: datetime):
-        """Handle new position detection."""
-        try:
-            contracts = float(exchange_pos.get('contracts', 0))
-            size = abs(contracts)
-            
-            # Use CCXT's side field first (more reliable), fallback to contract sign
-            ccxt_side = exchange_pos.get('side', '').lower()
-            if ccxt_side == 'long':
-                side, order_side = 'long', 'buy'
-            elif ccxt_side == 'short':
-                side, order_side = 'short', 'sell'
-            else:
-                # Fallback to contract sign (less reliable but better than nothing)
-                side = 'long' if contracts > 0 else 'short'
-                order_side = 'buy' if side == 'long' else 'sell'
-                logger.warning(f"⚠️ Using contract sign fallback for {symbol}: side={side}, ccxt_side='{ccxt_side}'")
-            
-            # Get entry price from exchange
-            entry_price = float(exchange_pos.get('entryPrice', 0)) or float(exchange_pos.get('entryPx', 0))
-            if not entry_price:
-                entry_price = float(exchange_pos.get('markPrice', 0)) or float(exchange_pos.get('markPx', 0))
-            
-            if not entry_price:
-                logger.error(f"❌ Cannot determine entry price for {symbol}")
-                return
-
-            # Extract ROE from info.position.returnOnEquity
-            roe_raw = None
-            if 'info' in exchange_pos and 'position' in exchange_pos['info']:
-                roe_raw = exchange_pos['info']['position'].get('returnOnEquity')
-            roe_percentage = float(roe_raw) * 100 if roe_raw is not None else 0.0
-
-            # Create trade lifecycle using existing manager
-            lifecycle_id = stats.create_trade_lifecycle(
-                symbol=symbol,
-                side=order_side,
-                entry_order_id=f"external_position_{timestamp.strftime('%Y%m%d_%H%M%S')}",
-                trade_type='external_detected'
-            )
-            
-            if lifecycle_id:
-                # Update to position_opened using existing manager
-                await stats.update_trade_position_opened(
-                    lifecycle_id=lifecycle_id,
-                    entry_price=entry_price,
-                    entry_amount=size,
-                    exchange_fill_id=f"position_detected_{timestamp.isoformat()}"
-                )
-
-                # Now, update the market data for the newly opened position
-                margin_used = None
-                if 'info' in exchange_pos and isinstance(exchange_pos['info'], dict):
-                    position_info = exchange_pos['info'].get('position', {})
-                    if position_info:
-                        margin_used = position_info.get('marginUsed')
-
-                stats.trade_manager.update_trade_market_data(
-                    lifecycle_id=lifecycle_id,
-                    current_position_size=size,
-                    unrealized_pnl=exchange_pos.get('unrealizedPnl'),
-                    roe_percentage=roe_percentage,
-                    mark_price=exchange_pos.get('markPrice'),
-                    position_value=exchange_pos.get('positionValue'),
-                    margin_used=margin_used,
-                    leverage=exchange_pos.get('leverage'),
-                    liquidation_price=exchange_pos.get('liquidationPrice')
-                )
-                
-                logger.info(f"🚀 NEW POSITION: {symbol} {side.upper()} {size} @ {entry_price}")
-                
-                # Send notification
-                await self._send_reconciliation_notification('opened', symbol, {
-                    'side': side,
-                    'size': size,
-                    'price': entry_price,
-                    'timestamp': timestamp
-                })
-        except Exception as e:
-            logger.error(f"❌ Error handling position opened for {symbol}: {e}")
-    
-    async def _handle_position_closed(self, symbol: str, db_pos: Dict, stats, timestamp: datetime):
-        """Handle position closed during reconciliation."""
-        try:
-            lifecycle_id = db_pos['trade_lifecycle_id']
-            entry_price = db_pos.get('entry_price', 0)
-            position_side = db_pos.get('position_side')
-            size = db_pos.get('current_position_size', 0)
-            
-            # Get the latest lifecycle status to check if it was already closed
-            latest_lifecycle = stats.get_trade_by_lifecycle_id(lifecycle_id)
-            if latest_lifecycle and latest_lifecycle.get('status') == 'position_closed':
-                logger.info(f"ℹ️ Position for {symbol} already marked as closed in lifecycle {lifecycle_id[:8]}. Skipping duplicate close processing.")
-                return
-            
-            # Check if this was already migrated to aggregated stats (bot exit already processed it)
-            try:
-                if not stats.get_trade_by_lifecycle_id(lifecycle_id):
-                    logger.info(f"ℹ️ Lifecycle {lifecycle_id[:8]} for {symbol} already migrated to aggregated stats. Bot exit already handled this closure.")
-                    return
-            except Exception:
-                # If we can't check, proceed with caution
-                pass
-            
-            # Estimate exit price from market data
-            exit_price = 0
-            try:
-                market_data = await self.trading_engine.get_market_data(symbol)
-                if market_data and market_data.get('ticker'):
-                    exit_price = float(market_data['ticker'].get('last', 0))
-                    if exit_price <= 0:
-                        logger.warning(f"⚠️ Invalid market price for {symbol} - using entry price")
-                        exit_price = entry_price
-                else:
-                    logger.warning(f"⚠️ Could not get market data for {symbol} - using entry price")
-                    exit_price = entry_price
-            except Exception as e:
-                logger.warning(f"⚠️ Error fetching market data for {symbol}: {e} - using entry price")
-                exit_price = entry_price
-
-            # Calculate realized PnL
-            realized_pnl = 0
-            if position_side == 'long':
-                realized_pnl = size * (exit_price - entry_price)
-            elif position_side == 'short':
-                realized_pnl = size * (entry_price - exit_price)
-            
-            # Update to position_closed using existing manager
-            success = await stats.update_trade_position_closed(
-                lifecycle_id=lifecycle_id,
-                exit_price=exit_price,
-                realized_pnl=realized_pnl,
-                exchange_fill_id=f"position_closed_detected_{timestamp.isoformat()}"
-            )
-            
-            if success:
-                logger.info(f"🎯 POSITION CLOSED: {symbol} {position_side.upper()} PnL: {realized_pnl:.2f}")
-                
-                # Get exchange position info for ROE
-                exchange_positions = self.trading_engine.get_positions()
-                exchange_pos = None
-                if exchange_positions:
-                    for pos in exchange_positions:
-                        if pos.get('symbol') == symbol:
-                            exchange_pos = pos
-                            break
-                
-                # Send notification
-                await self._send_reconciliation_notification('closed', symbol, {
-                    'side': position_side,
-                    'size': size,
-                    'entry_price': entry_price,
-                    'exit_price': exit_price,
-                    'realized_pnl': realized_pnl,
-                    'timestamp': timestamp,
-                    'lifecycle_id': lifecycle_id,  # Pass lifecycle_id to get stored ROE
-                    'info': exchange_pos.get('info', {}) if exchange_pos else {}
-                })
-                
-                # Clear any pending stop losses for this symbol
-                stats.order_manager.cancel_pending_stop_losses_by_symbol(symbol, 'cancelled_position_closed')
-                
-                # Migrate trade to aggregated stats and clean up
-                stats.migrate_trade_to_aggregated_stats(lifecycle_id)
-                
-        except Exception as e:
-            logger.error(f"❌ Error handling position closed for {symbol}: {e}")
-    
-    async def _handle_position_size_change(self, symbol: str, exchange_pos: Dict, 
-                                         db_pos: Dict, stats, timestamp: datetime):
-        """Handle position size changes and position flips."""
-        try:
-            exchange_size = abs(float(exchange_pos.get('contracts', 0)))
-            db_size = db_pos.get('current_position_size', 0)
-            db_position_side = db_pos.get('position_side')
-            
-            # Determine current exchange position side
-            ccxt_side = exchange_pos.get('side', '').lower()
-            if ccxt_side == 'long':
-                exchange_position_side = 'long'
-            elif ccxt_side == 'short':
-                exchange_position_side = 'short'
-            else:
-                # Fallback to contract sign
-                contracts = float(exchange_pos.get('contracts', 0))
-                exchange_position_side = 'long' if contracts > 1e-9 else 'short'
-                logger.warning(f"⚠️ Using contract sign fallback for side detection: {symbol}")
-            
-            # Check for POSITION FLIP (LONG ↔ SHORT)
-            if db_position_side != exchange_position_side:
-                logger.info(f"🔄 POSITION FLIP DETECTED: {symbol} {db_position_side.upper()} → {exchange_position_side.upper()}")
-                
-                # Handle as: close old position + open new position
-                await self._handle_position_closed(symbol, db_pos, stats, timestamp)
-                await self._handle_position_opened(symbol, exchange_pos, stats, timestamp)
-                return
-
-            # If we are here, the side is the same. Now we can update market data for the existing trade.
-            lifecycle_id = db_pos['trade_lifecycle_id']
-            
-            # Extract all relevant market data from the exchange position
-            unrealized_pnl = exchange_pos.get('unrealizedPnl')
-            leverage = exchange_pos.get('leverage')
-            liquidation_price = exchange_pos.get('liquidationPrice')
-            mark_price = exchange_pos.get('markPrice')
-            position_value = exchange_pos.get('contracts', 0) * exchange_pos.get('markPrice', 0) if mark_price else None
-
-            # Safely extract ROE and Margin from the 'info' dictionary
-            roe_percentage = None
-            margin_used = None
-            if 'info' in exchange_pos and isinstance(exchange_pos['info'], dict):
-                logger.debug(f"Exchange position info for {symbol}: {exchange_pos['info']}") # Temporary logging
-                position_info = exchange_pos['info'].get('position', {})
-                if position_info:
-                    roe_raw = position_info.get('returnOnEquity')
-                    roe_percentage = float(roe_raw) * 100 if roe_raw is not None else None
-                    margin_used = position_info.get('marginUsed')
-
-            # Call the trade manager to update the database
-            success = stats.trade_manager.update_trade_market_data(
-                lifecycle_id=lifecycle_id,
-                current_position_size=exchange_size,
-                unrealized_pnl=unrealized_pnl,
-                roe_percentage=roe_percentage,
-                mark_price=mark_price,
-                position_value=position_value,
-                margin_used=margin_used,
-                leverage=leverage,
-                liquidation_price=liquidation_price
-            )
-
-            if success:
-                logger.debug(f"🔄 Synced market data for {symbol} (Lifecycle: {lifecycle_id[:8]})")
-
-
-            # Check if size actually changed (with small tolerance)
-            if abs(exchange_size - db_size) < 1e-6:
-                return  # No meaningful change
-            
-            lifecycle_id = db_pos['trade_lifecycle_id']
-            entry_price = db_pos.get('entry_price', 0)
-            
-            # Extract ROE from info.position.returnOnEquity
-            roe_raw = None
-            if 'info' in exchange_pos and 'position' in exchange_pos['info']:
-                roe_raw = exchange_pos['info']['position'].get('returnOnEquity')
-            roe_percentage = float(roe_raw) * 100 if roe_raw is not None else 0.0
-
-            # Update position size using existing manager
-            success = stats.trade_manager.update_trade_market_data(
-                lifecycle_id, 
-                current_position_size=exchange_size,
-                unrealized_pnl=exchange_pos.get('unrealizedPnl'),
-                roe_percentage=roe_percentage,
-                mark_price=exchange_pos.get('markPrice'),
-                position_value=exchange_pos.get('positionValue'),
-                margin_used=exchange_pos.get('marginUsed'),
-                leverage=exchange_pos.get('leverage'),
-                liquidation_price=exchange_pos.get('liquidationPrice')
-            )
-            
-            if success:
-                change_type = 'increased' if exchange_size > db_size else 'decreased'
-                size_diff = abs(exchange_size - db_size)
-                logger.info(f"📊 Position size {change_type}: {symbol} by {size_diff} (ROE: {roe_percentage:+.2f}%)")
-                
-                # Send notification
-                await self._send_reconciliation_notification('size_changed', symbol, {
-                    'side': exchange_position_side,
-                    'old_size': db_size,
-                    'new_size': exchange_size,
-                    'change_type': change_type,
-                    'size_diff': size_diff,
-                    'roe': roe_percentage,
-                    'timestamp': timestamp
-                })
-                
-        except Exception as e:
-            logger.error(f"❌ Error handling position size change for {symbol}: {e}")
-
-    async def _send_reconciliation_notification(self, change_type: str, symbol: str, details: Dict[str, Any]):
-        """Send position change notification."""
-        try:
-            if not self.notification_manager:
-                return
-            
-            token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-            time_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
-            formatter = get_formatter()
-            
-            if change_type == 'opened':
-                side = details['side'].upper()
-                size = details['size']
-                entry_price = details['price']
-                
-                message = f"""🎯 <b>Position Opened (Reconciled)</b>
-
-📊 <b>Details:</b>
-• Token: {token}
-• Direction: {side}
-• Size: {await formatter.format_amount(size, token)}
-• Entry: {await formatter.format_price_with_symbol(entry_price, token)}
-
-⏰ <b>Time:</b> {time_str}
-📈 Use /positions to view current status"""
-                
-            elif change_type == 'closed':
-                side = details['side'].upper()
-                size = details['size']
-                entry_price = details['entry_price']
-                exit_price = details['exit_price']
-                pnl = details['realized_pnl']
-                pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-                
-                # Use the last known ROE from heartbeat data (stored in lifecycle)
-                stored_roe = None
-                lifecycle_id = details.get('lifecycle_id')
-                if lifecycle_id:
-                    stats = self.trading_engine.get_stats()
-                    if stats:
-                        lifecycle = stats.get_trade_by_lifecycle_id(lifecycle_id)
-                        if lifecycle:
-                            stored_roe = lifecycle.get('roe_percentage')
-                
-                if stored_roe is not None:
-                    try:
-                        roe = float(stored_roe)
-                        roe_text = f"({roe:+.2f}%)"
-                        logger.debug(f"Using stored ROE from heartbeat for reconciled {symbol}: {roe:+.2f}%")
-                    except (ValueError, TypeError):
-                        logger.warning(f"Could not parse stored ROE value: {stored_roe} for {symbol}")
-                        roe_text = ""
-                else:
-                    logger.debug(f"No stored ROE available for reconciled {symbol}")
-                    roe_text = ""
-                
-                message = f"""🎯 <b>Position Closed (Reconciled)</b>
-
-📊 <b>Details:</b>
-• Token: {token}
-• Direction: {side}
-• Size: {await formatter.format_amount(size, token)}
-• Entry: {await formatter.format_price_with_symbol(entry_price, token)}
-• Exit: {await formatter.format_price_with_symbol(exit_price, token)}
-
-{pnl_emoji} <b>P&L:</b> {await formatter.format_price_with_symbol(pnl)} {roe_text}
-
-⏰ <b>Time:</b> {time_str}
-📊 Use /stats to view performance"""
-                
-            elif change_type in ['increased', 'decreased']:
-                side = details['side'].upper()
-                old_size = details['old_size']
-                new_size = details['new_size']
-                size_diff = details['size_diff']
-                emoji = "📈" if change_type == 'increased' else "📉"
-                
-                message = f"""{emoji} <b>Position {change_type.title()} (Reconciled)</b>
-
-📊 <b>Details:</b>
-• Token: {token}
-• Direction: {side}
-• Previous Size: {await formatter.format_amount(old_size, token)}
-• New Size: {await formatter.format_amount(new_size, token)}
-• Change: {await formatter.format_amount(size_diff, token)}
-
-⏰ <b>Time:</b> {time_str}
-📈 Use /positions to view current status"""
-            else:
-                return
-                
-            await self.notification_manager.send_generic_notification(message.strip())
-            logger.debug(f"📨 Sent {change_type} notification for {symbol}")
-            
-        except Exception as e:
-            logger.error(f"❌ Error sending notification for {symbol}: {e}") 

+ 225 - 0
src/monitoring/position_tracker.py

@@ -0,0 +1,225 @@
+import asyncio
+import logging
+from typing import Dict, List, Optional, Any
+from datetime import datetime, timezone
+
+from ..clients.hyperliquid_client import HyperliquidClient
+from ..notifications.notification_manager import NotificationManager
+from ..stats.trading_stats import TradingStats
+
+logger = logging.getLogger(__name__)
+
+class PositionTracker:
+    """
+    Simplified position tracker that mirrors exchange state.
+    Monitors for position changes and saves stats when positions close.
+    """
+    
+    def __init__(self, hl_client: HyperliquidClient, notification_manager: NotificationManager):
+        self.hl_client = hl_client
+        self.notification_manager = notification_manager
+        self.trading_stats = TradingStats()
+        
+        # Track current positions
+        self.current_positions: Dict[str, Dict] = {}
+        self.is_running = False
+        
+    async def start(self):
+        """Start position tracking"""
+        if self.is_running:
+            return
+            
+        self.is_running = True
+        logger.info("Starting position tracker")
+        
+        # Initialize current positions
+        await self._update_current_positions()
+        
+        # Start monitoring loop
+        asyncio.create_task(self._monitoring_loop())
+        
+    async def stop(self):
+        """Stop position tracking"""
+        self.is_running = False
+        logger.info("Stopping position tracker")
+        
+    async def _monitoring_loop(self):
+        """Main monitoring loop"""
+        while self.is_running:
+            try:
+                await self._check_position_changes()
+                await asyncio.sleep(2)  # Check every 2 seconds
+            except Exception as e:
+                logger.error(f"Error in position tracking loop: {e}")
+                await asyncio.sleep(5)
+                
+    async def _check_position_changes(self):
+        """Check for any position changes"""
+        try:
+            previous_positions = self.current_positions.copy()
+            await self._update_current_positions()
+            
+            # Compare with previous positions
+            await self._process_position_changes(previous_positions, self.current_positions)
+            
+        except Exception as e:
+            logger.error(f"Error checking position changes: {e}")
+            
+    async def _update_current_positions(self):
+        """Update current positions from exchange"""
+        try:
+            user_state = await self.hl_client.get_user_state()
+            if not user_state or 'assetPositions' not in user_state:
+                return
+                
+            new_positions = {}
+            for position in user_state['assetPositions']:
+                if position.get('position', {}).get('szi') != '0':
+                    symbol = position.get('position', {}).get('coin', '')
+                    if symbol:
+                        new_positions[symbol] = {
+                            'size': float(position.get('position', {}).get('szi', 0)),
+                            'entry_px': float(position.get('position', {}).get('entryPx', 0)),
+                            'unrealized_pnl': float(position.get('position', {}).get('unrealizedPnl', 0)),
+                            'margin_used': float(position.get('position', {}).get('marginUsed', 0)),
+                            'max_leverage': float(position.get('position', {}).get('maxLeverage', 1)),
+                            'return_on_equity': float(position.get('position', {}).get('returnOnEquity', 0))
+                        }
+            
+            self.current_positions = new_positions
+            
+        except Exception as e:
+            logger.error(f"Error updating current positions: {e}")
+            
+    async def _process_position_changes(self, previous: Dict, current: Dict):
+        """Process changes between previous and current positions"""
+        
+        # Find new positions
+        for symbol in current:
+            if symbol not in previous:
+                await self._handle_position_opened(symbol, current[symbol])
+                
+        # Find closed positions  
+        for symbol in previous:
+            if symbol not in current:
+                await self._handle_position_closed(symbol, previous[symbol])
+                
+        # Find changed positions
+        for symbol in current:
+            if symbol in previous:
+                await self._handle_position_changed(symbol, previous[symbol], current[symbol])
+                
+    async def _handle_position_opened(self, symbol: str, position: Dict):
+        """Handle new position opened"""
+        try:
+            size = position['size']
+            side = "Long" if size > 0 else "Short"
+            
+            message = (
+                f"🟢 Position Opened\n"
+                f"Token: {symbol}\n"
+                f"Side: {side}\n"
+                f"Size: {abs(size):.4f}\n"
+                f"Entry: ${position['entry_px']:.4f}\n"
+                f"Leverage: {position['max_leverage']:.1f}x"
+            )
+            
+            await self.notification_manager.send_notification(message)
+            logger.info(f"Position opened: {symbol} {side} {abs(size)}")
+            
+        except Exception as e:
+            logger.error(f"Error handling position opened for {symbol}: {e}")
+            
+    async def _handle_position_closed(self, symbol: str, position: Dict):
+        """Handle position closed - save stats to database"""
+        try:
+            # Get current market price for PnL calculation
+            market_data = await self.hl_client.get_market_data(symbol)
+            if not market_data:
+                logger.error(f"Could not get market data for {symbol}")
+                return
+                
+            current_price = float(market_data.get('markPx', 0))
+            entry_price = position['entry_px']
+            size = abs(position['size'])
+            side = "Long" if position['size'] > 0 else "Short"
+            
+            # Calculate PnL
+            if side == "Long":
+                pnl = (current_price - entry_price) * size
+            else:
+                pnl = (entry_price - current_price) * size
+                
+            # Save to database
+            await self._save_position_stats(symbol, side, size, entry_price, current_price, pnl)
+            
+            # Send notification
+            pnl_emoji = "🟢" if pnl >= 0 else "🔴"
+            message = (
+                f"{pnl_emoji} Position Closed\n"
+                f"Token: {symbol}\n"
+                f"Side: {side}\n"
+                f"Size: {size:.4f}\n"
+                f"Entry: ${entry_price:.4f}\n"
+                f"Exit: ${current_price:.4f}\n"
+                f"PnL: ${pnl:.3f}"
+            )
+            
+            await self.notification_manager.send_notification(message)
+            logger.info(f"Position closed: {symbol} {side} PnL: ${pnl:.3f}")
+            
+        except Exception as e:
+            logger.error(f"Error handling position closed for {symbol}: {e}")
+            
+    async def _handle_position_changed(self, symbol: str, previous: Dict, current: Dict):
+        """Handle position size or direction changes"""
+        try:
+            prev_size = previous['size']
+            curr_size = current['size']
+            
+            # Check if position reversed (long to short or vice versa)
+            if (prev_size > 0 and curr_size < 0) or (prev_size < 0 and curr_size > 0):
+                # Position reversed - close old, open new
+                await self._handle_position_closed(symbol, previous)
+                await self._handle_position_opened(symbol, current)
+                return
+                
+            # Check if position size changed significantly
+            size_change = abs(curr_size) - abs(prev_size)
+            if abs(size_change) > 0.001:  # Threshold to avoid noise
+                
+                change_type = "Increased" if size_change > 0 else "Decreased"
+                side = "Long" if curr_size > 0 else "Short"
+                
+                message = (
+                    f"🔄 Position {change_type}\n"
+                    f"Token: {symbol}\n"
+                    f"Side: {side}\n"
+                    f"New Size: {abs(curr_size):.4f}\n"
+                    f"Change: {'+' if size_change > 0 else ''}{size_change:.4f}"
+                )
+                
+                await self.notification_manager.send_notification(message)
+                logger.info(f"Position changed: {symbol} {change_type} by {size_change:.4f}")
+                
+        except Exception as e:
+            logger.error(f"Error handling position change for {symbol}: {e}")
+            
+    async def _save_position_stats(self, symbol: str, side: str, size: float, 
+                                 entry_price: float, exit_price: float, pnl: float):
+        """Save position statistics to database using existing TradingStats interface"""
+        try:
+            # Use the existing process_trade_complete_cycle method
+            lifecycle_id = self.trading_stats.process_trade_complete_cycle(
+                symbol=symbol,
+                side=side.lower(),
+                entry_price=entry_price,
+                exit_price=exit_price,
+                amount=size,
+                timestamp=datetime.now(timezone.utc).isoformat()
+            )
+            
+            logger.info(f"Saved stats for {symbol}: PnL ${pnl:.3f}, lifecycle_id: {lifecycle_id}")
+            
+        except Exception as e:
+            logger.error(f"Error saving position stats for {symbol}: {e}") 

+ 0 - 661
src/monitoring/risk_cleanup_manager.py

@@ -1,661 +0,0 @@
-#!/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, shared_state):
-        self.trading_engine = trading_engine
-        self.notification_manager = notification_manager
-        self.market_monitor_cache = market_monitor_cache # To access cached orders/positions
-        self.shared_state = shared_state # Store shared_state
-        # Initialize external_stop_losses within shared_state if it's not already there
-        if 'external_stop_losses' not in self.shared_state:
-            self.shared_state['external_stop_losses'] = {}
-        # For direct access if needed, though primary access should be via shared_state
-        self.external_stop_losses: Dict[str, Dict[str, Any]] = self.shared_state['external_stop_losses']
-
-    # 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.info(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 = await 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:
-                    # Verify active position before executing SL
-                    active_position_exists = False
-                    cached_positions = self.market_monitor_cache.cached_positions or []
-                    for pos in cached_positions:
-                        if pos.get('symbol') == symbol and abs(float(pos.get('contracts') or 0)) > 1e-9: # Check for non-zero contracts
-                            active_position_exists = True
-                            break
-                    
-                    if not active_position_exists:
-                        logger.warning(f"🚫 SL TRIGGER IGNORED for {symbol} (DB ID: {order_db_id}): No active position found. Cancelling trigger.")
-                        stats.update_order_status(order_db_id=order_db_id, new_status='cancelled_no_position')
-                        if self.notification_manager:
-                            await self.notification_manager.send_generic_notification(
-                                f"⚠️ Stop-Loss Trigger Cancelled (No Position)\\n"
-                                f"Symbol: {symbol}\\n"
-                                f"Side: {trigger_side.upper()}\\n"
-                                f"Trigger Price: ${trigger_price:.4f}\\n"
-                                f"Reason: The stop-loss trigger price was hit, but no active position was found for this symbol.\\n"
-                                f"The pending SL (DB ID: {order_db_id}) has been cancelled to prevent incorrect order placement."
-                            )
-                        continue # Skip to the next trigger
-
-                    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:
-                logger.info(f"Risk management disabled or STOP_LOSS_PERCENTAGE <= 0 (value: {Config.STOP_LOSS_PERCENTAGE})")
-                return
-
-            # Use DB positions for risk checks to ensure validated, up-to-date data
-            stats = self.trading_engine.get_stats()
-            positions = stats.get_open_positions() if stats else []
-            if not positions:
-                logger.debug("No open positions found in DB for risk management check.")
-                await self._cleanup_orphaned_stop_losses() # Call within class
-                return
-
-            for position in positions:
-                try:
-                    symbol = position.get('symbol', '')
-                    
-                    # Safely convert position values, handling None values
-                    contracts = float(position.get('current_position_size') or 0)
-                    entry_price = float(position.get('entry_price') or 0)
-                    roe_percentage = float(position.get('roe_percentage') or 0)
-                    unrealized_pnl = float(position.get('unrealized_pnl') or 0)
-
-                    if contracts == 0 or entry_price <= 0:
-                        logger.info(f"Skipping position {symbol}: contracts={contracts}, entry_price={entry_price}")
-                        continue
-
-                    logger.debug(f"[RiskMgmt] {symbol}: ROE={roe_percentage:+.2f}%, Threshold=-{Config.STOP_LOSS_PERCENTAGE}% (Trigger: {roe_percentage <= -Config.STOP_LOSS_PERCENTAGE})")
-
-                    if roe_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 {roe_percentage:+.2f}% ROE loss (threshold: -{Config.STOP_LOSS_PERCENTAGE}%)")
-                        
-                        if self.notification_manager:
-                            await self.notification_manager.send_generic_notification(
-                                f"""🚨 AUTOMATIC STOP LOSS TRIGGERED!
-
-Token: {token}
-Lifecycle ID: {lifecycle_id_str}
-Position: {position_side} {abs(contracts):.6f}
-Entry Price: ${entry_price:.4f}
-Unrealized P&L: ${unrealized_pnl:+.2f} ({roe_percentage:+.2f}% ROE)
-Safety Threshold: -{Config.STOP_LOSS_PERCENTAGE}% ROE
-Action: Executing emergency exit order..."""
-                            )
-
-                        # Execute emergency exit order
-                        exit_result = await self.trading_engine.execute_exit_order(token)
-                        if not exit_result.get('success'):
-                            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>Emergency Exit Failed</b>\n\n"
-                                    f"Token: {token}\n"
-                                    f"Position: {position_side.upper()}\n"
-                                    f"ROE: {roe_percentage:.2f}%\n"
-                                    f"Error: {error_msg}\n\n"
-                                    f"Please check the position and close it manually if needed."
-                                )
-                            continue
-
-                        # Cancel any pending stop losses for this symbol
-                        if self.trading_engine.stats and hasattr(self.trading_engine.stats.order_manager, 'cancel_pending_stop_losses_by_symbol'):
-                            try:
-                                cancelled_sl_count = self.trading_engine.stats.order_manager.cancel_pending_stop_losses_by_symbol(
-                                    symbol=symbol,
-                                    new_status='cancelled_automatic_exit'
-                                )
-                                if cancelled_sl_count > 0:
-                                    logger.info(f"Cancelled {cancelled_sl_count} pending stop losses for {symbol} after automatic exit")
-                            except Exception as sl_error:
-                                logger.error(f"Error cancelling pending stop losses for {symbol}: {sl_error}")
-                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.info(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') or 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') or 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') or 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') or 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.info(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') or 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.info(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.info(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)
-
-    async def run_cleanup_tasks(self):
-        """Run all risk and cleanup tasks."""
-        stats = self.trading_engine.get_stats()
-        if not stats:
-            logger.warning("⚠️ TradingStats not available, skipping cleanup tasks.")
-            return
-
-        await self._check_automatic_risk_management()
-        await self._cleanup_orphaned_stop_losses()
-        await self._handle_pending_sl_activations(stats)
-        await self._cleanup_orphaned_pending_trades(stats)
-
-    async def _handle_pending_sl_activations(self, stats):
-        """Handle pending stop losses - place orders for positions that need them."""
-        try:
-            # Get positions with pending SLs using existing manager
-            pending_sl_trades = stats.get_pending_stop_loss_activations()
-            
-            for trade in pending_sl_trades:
-                symbol = trade['symbol']
-                stop_price = trade['stop_loss_price']
-                position_side = trade['position_side']
-                lifecycle_id = trade['trade_lifecycle_id']
-                
-                try:
-                    # Check if position still exists on exchange
-                    exchange_positions = self.trading_engine.get_positions()
-                    if exchange_positions is None:
-                        logger.warning("⚠️ Failed to fetch exchange positions - skipping stop loss check")
-                        continue
-                    position_exists = any(
-                        pos['symbol'] == symbol and abs(float(pos.get('contracts') or 0)) > 1e-9 
-                        for pos in exchange_positions
-                    )
-                    
-                    if position_exists:
-                        # Place stop loss order
-                        sl_side = 'sell' if position_side == 'long' else 'buy'
-                        
-                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-                        result = await self.trading_engine.execute_stop_loss_order(
-                            token=token,
-                            stop_price=stop_price
-                        )
-                        
-                        if result and result.get('success'):
-                            exchange_order_id = result.get('order_placed_details', {}).get('exchange_order_id')
-                            if exchange_order_id:
-                                # The execute_stop_loss_order already links the SL to the trade
-                                logger.info(f"✅ Placed pending SL: {symbol} @ {stop_price}")
-                            else:
-                                logger.warning(f"⚠️ SL placed for {symbol} but no exchange_order_id returned")
-                    else:
-                        # Position doesn't exist, clear pending SL
-                        logger.info(f"🗑️ Clearing pending SL for non-existent position: {symbol}")
-                        # This will be handled by position closed detection
-                        
-                except Exception as e:
-                    logger.error(f"❌ Error handling pending SL for {symbol}: {e}")
-                    
-        except Exception as e:
-            logger.error(f"❌ Error handling pending stop losses: {e}")
-
-    async def _cleanup_orphaned_pending_trades(self, stats):
-        """Handle trades stuck in 'pending' status due to cancelled orders."""
-        try:
-            # Get all pending trades
-            pending_trades = stats.get_trades_by_status('pending')
-            
-            if not pending_trades:
-                return
-                
-            logger.debug(f"🔍 Checking {len(pending_trades)} pending trades for orphaned status")
-            
-            # Get current exchange orders for comparison
-            exchange_orders = self.trading_engine.get_orders() or []
-            exchange_order_ids = {order.get('id') for order in exchange_orders if order.get('id')}
-            
-            for trade in pending_trades:
-                lifecycle_id = trade['trade_lifecycle_id']
-                entry_order_id = trade.get('entry_order_id')
-                symbol = trade['symbol']
-
-                # If the order is still open on the exchange, it's not orphaned.
-                if entry_order_id and entry_order_id in exchange_order_ids:
-                    logger.info(f"Trade {lifecycle_id} for {symbol} is pending but its order {entry_order_id} is still open. Skipping.")
-                    continue
-
-                # If no order is linked, or the order is not on the exchange,
-                # we assume it was cancelled or failed.
-                logger.warning(f"Orphaned pending trade detected for {symbol} (Lifecycle: {lifecycle_id}). Cancelling.")
-
-                # Mark the trade as cancelled (this is a sync function)
-                cancelled = stats.update_trade_cancelled(lifecycle_id, "Orphaned pending trade")
-                
-                if cancelled:
-                    # Migrate the cancelled trade to aggregated stats
-                    stats.migrate_trade_to_aggregated_stats(lifecycle_id)
-                    
-                    # Send a notification
-                    await self._send_trade_cancelled_notification(
-                        symbol, "Orphaned, presumed cancelled before fill.", trade
-                    )
-                else:
-                    logger.error(f"Failed to cancel orphaned trade lifecycle {lifecycle_id}")
-
-        except Exception as e:
-            logger.error(f"❌ Error handling orphaned pending trades: {e}", exc_info=True)
-
-    async def _send_trade_cancelled_notification(self, symbol: str, cancel_reason: str, trade: Dict[str, Any]):
-        """Send notification for a cancelled trade."""
-        try:
-            if not self.notification_manager:
-                return
-            
-            token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
-            lifecycle_id = trade['trade_lifecycle_id']
-            
-            # Create user-friendly reason
-            reason_map = {
-                'entry_order_cancelled_no_position': 'Entry order was cancelled before filling',
-                'no_entry_order_no_position': 'No entry order and no position opened',
-                'old_pending_trade_no_position': 'Trade was pending too long without execution'
-            }
-            user_reason = reason_map.get(cancel_reason, cancel_reason)
-            
-            message = f"""❌ <b>Trade Cancelled</b>
-
-📊 <b>Details:</b>
-• Token: {token}
-• Trade ID: {lifecycle_id[:8]}...
-• Reason: {user_reason}
-
-ℹ️ <b>Status:</b> Trade was automatically cancelled due to order issues
-📱 Use /positions to view current positions"""
-            
-            await self.notification_manager.send_generic_notification(message.strip())
-            logger.debug(f"📨 Sent cancellation notification for {symbol}")
-            
-        except Exception as e:
-            logger.error(f"❌ Error sending cancellation notification for {symbol}: {e}")

+ 192 - 0
src/monitoring/risk_manager.py

@@ -0,0 +1,192 @@
+import asyncio
+import logging
+from typing import Dict, List, Optional, Any
+
+from ..clients.hyperliquid_client import HyperliquidClient
+from ..notifications.notification_manager import NotificationManager
+from ..config.config import Config
+
+logger = logging.getLogger(__name__)
+
+class RiskManager:
+    """
+    Simplified risk manager that monitors for ROE hard exits.
+    Closes positions when they hit the configured ROE threshold.
+    """
+    
+    def __init__(self, hl_client: HyperliquidClient, notification_manager: NotificationManager, config: Config):
+        self.hl_client = hl_client
+        self.notification_manager = notification_manager
+        self.config = config
+        self.is_running = False
+        
+        # Risk thresholds from config (convert percentage to decimal)
+        self.hard_exit_roe = -(config.STOP_LOSS_PERCENTAGE / 100.0)  # 10.0% -> -0.10 ROE
+        
+    async def start(self):
+        """Start risk manager"""
+        if self.is_running:
+            return
+            
+        self.is_running = True
+        logger.info(f"Starting risk manager with hard exit ROE: {self.hard_exit_roe}")
+        
+        # Start monitoring loop
+        asyncio.create_task(self._monitoring_loop())
+        
+    async def stop(self):
+        """Stop risk manager"""
+        self.is_running = False
+        logger.info("Stopping risk manager")
+        
+    async def _monitoring_loop(self):
+        """Main monitoring loop"""
+        while self.is_running:
+            try:
+                await self._check_risk_thresholds()
+                await asyncio.sleep(3)  # Check every 3 seconds
+            except Exception as e:
+                logger.error(f"Error in risk manager monitoring loop: {e}")
+                await asyncio.sleep(5)
+                
+    async def _check_risk_thresholds(self):
+        """Check if any positions need risk management"""
+        try:
+            user_state = await self.hl_client.get_user_state()
+            if not user_state or 'assetPositions' not in user_state:
+                return
+                
+            positions_to_close = []
+            
+            for position in user_state['assetPositions']:
+                pos_data = position.get('position', {})
+                if pos_data.get('szi') != '0':
+                    symbol = pos_data.get('coin', '')
+                    roe = float(pos_data.get('returnOnEquity', 0))
+                    size = float(pos_data.get('szi', 0))
+                    
+                    # Check if ROE exceeds hard exit threshold
+                    if roe <= self.hard_exit_roe:
+                        positions_to_close.append({
+                            'symbol': symbol,
+                            'size': size,
+                            'roe': roe,
+                            'unrealized_pnl': float(pos_data.get('unrealizedPnl', 0))
+                        })
+                        
+            # Close positions that exceed risk threshold
+            for position in positions_to_close:
+                await self._execute_hard_exit(position)
+                
+        except Exception as e:
+            logger.error(f"Error checking risk thresholds: {e}")
+            
+    async def _execute_hard_exit(self, position: Dict):
+        """Execute hard exit for a position"""
+        try:
+            symbol = position['symbol']
+            size = position['size']
+            roe = position['roe']
+            unrealized_pnl = position['unrealized_pnl']
+            
+            # Determine order side (opposite of position)
+            side = 'sell' if size > 0 else 'buy'
+            order_size = abs(size)
+            
+            logger.warning(f"Hard exit triggered for {symbol}: ROE {roe:.2%} < {self.hard_exit_roe:.2%}")
+            
+            # Place market order to close position
+            order_result = await self.hl_client.place_order(
+                symbol=symbol,
+                side=side,
+                size=order_size,
+                order_type='market',
+                reduce_only=True
+            )
+            
+            if order_result and 'response' in order_result:
+                response = order_result['response']
+                if response.get('type') == 'order':
+                    
+                    # Send alert notification
+                    message = (
+                        f"🚨 HARD EXIT EXECUTED\n"
+                        f"Token: {symbol}\n"
+                        f"ROE: {roe:.2%}\n"
+                        f"Threshold: {self.hard_exit_roe:.2%}\n"
+                        f"Size: {order_size:.4f}\n"
+                        f"Est. PnL: ${unrealized_pnl:.3f}\n"
+                        f"Risk management action taken!"
+                    )
+                    
+                    await self.notification_manager.send_notification(message)
+                    logger.info(f"Hard exit executed for {symbol}: {order_size} @ ROE {roe:.2%}")
+                    
+                else:
+                    logger.error(f"Failed to execute hard exit for {symbol}: {response}")
+                    
+                    # Send failure notification
+                    error_message = (
+                        f"❌ HARD EXIT FAILED\n"
+                        f"Token: {symbol}\n"
+                        f"ROE: {roe:.2%}\n"
+                        f"Could not place market order!\n"
+                        f"Manual intervention required!"
+                    )
+                    await self.notification_manager.send_notification(error_message)
+                    
+        except Exception as e:
+            logger.error(f"Error executing hard exit for {position['symbol']}: {e}")
+            
+            # Send error notification
+            error_message = (
+                f"❌ HARD EXIT ERROR\n"
+                f"Token: {position['symbol']}\n"
+                f"ROE: {position['roe']:.2%}\n"
+                f"Error: {str(e)}\n"
+                f"Manual intervention required!"
+            )
+            await self.notification_manager.send_notification(error_message)
+            
+    async def get_risk_status(self) -> Dict:
+        """Get current risk status"""
+        try:
+            user_state = await self.hl_client.get_user_state()
+            if not user_state or 'assetPositions' not in user_state:
+                return {'positions': [], 'total_risk': 0}
+                
+            positions = []
+            total_unrealized_pnl = 0
+            
+            for position in user_state['assetPositions']:
+                pos_data = position.get('position', {})
+                if pos_data.get('szi') != '0':
+                    symbol = pos_data.get('coin', '')
+                    roe = float(pos_data.get('returnOnEquity', 0))
+                    unrealized_pnl = float(pos_data.get('unrealizedPnl', 0))
+                    size = float(pos_data.get('szi', 0))
+                    
+                    total_unrealized_pnl += unrealized_pnl
+                    
+                    # Calculate risk level
+                    risk_level = "HIGH" if roe <= self.hard_exit_roe * 0.8 else "MEDIUM" if roe <= self.hard_exit_roe * 0.5 else "LOW"
+                    
+                    positions.append({
+                        'symbol': symbol,
+                        'size': size,
+                        'roe': roe,
+                        'unrealized_pnl': unrealized_pnl,
+                        'risk_level': risk_level,
+                        'distance_to_hard_exit': roe - self.hard_exit_roe
+                    })
+                    
+            return {
+                'positions': positions,
+                'total_unrealized_pnl': total_unrealized_pnl,
+                'hard_exit_threshold': self.hard_exit_roe,
+                'high_risk_positions': len([p for p in positions if p['risk_level'] == 'HIGH'])
+            }
+            
+        except Exception as e:
+            logger.error(f"Error getting risk status: {e}")
+            return {'positions': [], 'total_risk': 0} 

+ 5 - 43
src/trading/trading_engine.py

@@ -25,7 +25,7 @@ class TradingEngine:
         """Initialize the trading engine."""
         self.client = HyperliquidClient()
         self.stats: Optional[TradingStats] = None
-        self.market_monitor = None  # Will be set by the main bot
+        # Removed market monitor dependency - simplified architecture
         
         # State persistence (Removed - state is now in DB)
         # self.state_file = "data/trading_engine_state.json"
@@ -69,56 +69,18 @@ class TradingEngine:
         except Exception as e:
             logger.error(f"Could not set initial balance during async init: {e}", exc_info=True)
     
-    def set_market_monitor(self, market_monitor):
-        """Set the market monitor reference for accessing cached data."""
-        self.market_monitor = market_monitor
+    # Removed set_market_monitor method - simplified architecture
         
     def get_balance(self) -> Optional[Dict[str, Any]]:
-        """Get account balance (uses cached data when available)."""
-        # Try cached data first (updated every heartbeat)
-        if self.market_monitor and hasattr(self.market_monitor, 'get_cached_balance'):
-            cached_balance = self.market_monitor.get_cached_balance()
-            cache_age = self.market_monitor.get_cache_age_seconds()
-            
-            # Use cached data if it's fresh (less than 30 seconds old)
-            if cached_balance and cache_age is not None and cache_age < 30:
-                logger.debug(f"Using cached balance (age: {cache_age:.1f}s)")
-                return cached_balance
-        
-        # Fallback to fresh API call
-        logger.debug("Using fresh balance API call")
+        """Get account balance."""
         return self.client.get_balance()
     
     def get_positions(self) -> Optional[List[Dict[str, Any]]]:
-        """Get all positions (uses cached data when available)."""
-        # Try cached data first (updated every heartbeat)
-        if self.market_monitor and hasattr(self.market_monitor, 'get_cached_positions'):
-            cached_positions = self.market_monitor.get_cached_positions()
-            cache_age = self.market_monitor.get_cache_age_seconds()
-            
-            # Use cached data if it's fresh (less than 30 seconds old)
-            if cached_positions is not None and cache_age is not None and cache_age < 30:
-                logger.debug(f"Using cached positions (age: {cache_age:.1f}s): {len(cached_positions)} positions")
-                return cached_positions
-        
-        # Fallback to fresh API call
-        logger.debug("Using fresh positions API call")
+        """Get all positions."""
         return self.client.get_positions()
     
     def get_orders(self) -> Optional[List[Dict[str, Any]]]:
-        """Get all open orders (uses cached data when available)."""
-        # Try cached data first (updated every heartbeat)
-        if self.market_monitor and hasattr(self.market_monitor, 'get_cached_orders'):
-            cached_orders = self.market_monitor.get_cached_orders()
-            cache_age = self.market_monitor.get_cache_age_seconds()
-            
-            # Use cached data if it's fresh (less than 30 seconds old)
-            if cached_orders is not None and cache_age is not None and cache_age < 30:
-                logger.debug(f"Using cached orders (age: {cache_age:.1f}s): {len(cached_orders)} orders")
-                return cached_orders
-        
-        # Fallback to fresh API call
-        logger.debug("Using fresh orders API call")
+        """Get all open orders."""
         return self.client.get_open_orders()
     
     def get_recent_fills(self) -> Optional[List[Dict[str, Any]]]:

+ 1 - 1
trading_bot.py

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