Просмотр исходного кода

Refactor trading bot initialization and enhance command handling - Updated the TelegramTradingBot to initialize MarketMonitor with a notification manager for improved alerting. Added a new /cycles command in InfoCommands to display trade cycle statistics, enhancing user insights into trading performance. Improved error handling and logging for better traceability in command execution.

Carles Sentis 4 дней назад
Родитель
Сommit
75edbe2ebf

+ 2 - 4
src/bot/core.py

@@ -31,11 +31,8 @@ class TelegramTradingBot:
         
         # Initialize subsystems
         self.trading_engine = TradingEngine()
-        self.market_monitor = MarketMonitor(self.trading_engine)
         self.notification_manager = NotificationManager()
-        
-        # Connect notification manager to market monitor
-        self.market_monitor.set_notification_manager(self.notification_manager)
+        self.market_monitor = MarketMonitor(self.trading_engine, self.notification_manager)
         
         # Initialize command handlers
         self.info_commands = InfoCommands(self.trading_engine)
@@ -86,6 +83,7 @@ class TelegramTradingBot:
         self.application.add_handler(CommandHandler("orders", self.info_commands.orders_command))
         self.application.add_handler(CommandHandler("stats", self.info_commands.stats_command))
         self.application.add_handler(CommandHandler("trades", self.info_commands.trades_command))
+        self.application.add_handler(CommandHandler("cycles", self.info_commands.cycles_command))
         self.application.add_handler(CommandHandler("market", self.info_commands.market_command))
         self.application.add_handler(CommandHandler("price", self.info_commands.price_command))
         self.application.add_handler(CommandHandler("performance", self.info_commands.performance_command))

+ 148 - 28
src/commands/info_commands.py

@@ -274,36 +274,156 @@ class InfoCommands:
             await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
     
     async def trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
-        """Handle the /trades command."""
-        chat_id = update.effective_chat.id
-        if not self._is_authorized(chat_id):
-            await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
+        """Handle the /trades command - Show recent trade history."""
+        if not self._is_authorized(update):
             return
-        
-        stats = self.trading_engine.get_stats()
-        if not stats:
-            await context.bot.send_message(chat_id=chat_id, text="❌ Could not load trading statistics")
-            return
-        
-        recent_trades = stats.get_recent_trades(10)
-        
-        if not recent_trades:
-            await context.bot.send_message(chat_id=chat_id, text="📝 No trades recorded yet.")
+            
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                await update.message.reply_text("❌ Trading statistics not available.", parse_mode='HTML')
+                return
+            
+            # Get recent trades (limit to last 20)
+            recent_trades = stats.get_recent_trades(limit=20)
+            
+            if not recent_trades:
+                await update.message.reply_text("📊 <b>No trades found.</b>", parse_mode='HTML')
+                return
+            
+            message = "📈 <b>Recent Trades (Last 20)</b>\n\n"
+            
+            for trade in recent_trades:
+                symbol = trade['symbol']
+                token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0] if ':' in symbol else symbol
+                side = trade['side'].upper()
+                amount = trade['amount']
+                price = trade['price']
+                timestamp = trade['timestamp']
+                pnl = trade.get('realized_pnl', 0)
+                
+                # Format timestamp
+                try:
+                    from datetime import datetime
+                    dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
+                    time_str = dt.strftime('%m/%d %H:%M')
+                except:
+                    time_str = "Unknown"
+                
+                # PnL emoji and formatting
+                if pnl > 0:
+                    pnl_emoji = "🟢"
+                    pnl_str = f"+${pnl:.2f}"
+                elif pnl < 0:
+                    pnl_emoji = "🔴"
+                    pnl_str = f"${pnl:.2f}"
+                else:
+                    pnl_emoji = "⚪"
+                    pnl_str = "$0.00"
+                
+                side_emoji = "🟢" if side == 'BUY' else "🔴"
+                
+                message += f"{side_emoji} <b>{side}</b> {amount} {token} @ ${price:,.2f}\n"
+                message += f"   {pnl_emoji} P&L: {pnl_str} | {time_str}\n\n"
+            
+            await update.message.reply_text(message, parse_mode='HTML')
+            
+        except Exception as e:
+            logger.error(f"Error in trades command: {e}")
+            await update.message.reply_text("❌ Error retrieving trade history.", parse_mode='HTML')
+    
+    async def cycles_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /cycles command - Show trade cycles and lifecycle statistics."""
+        if not self._is_authorized(update):
             return
-        
-        trades_text = "🔄 <b>Recent Trades</b>\n\n"
-        
-        formatter = get_formatter()
-        for trade in reversed(recent_trades[-5:]):  # Show last 5 trades
-            timestamp = datetime.fromisoformat(trade['timestamp']).strftime('%m/%d %H:%M')
-            side_emoji = "🟢" if trade['side'] == 'buy' else "🔴"
-            symbol = trade['symbol']
-            
-            trades_text += f"{side_emoji} <b>{trade['side'].upper()}</b> {trade['amount']} {symbol}\n"
-            trades_text += f"   💰 {formatter.format_price_with_symbol(trade['price'], symbol)} | 💵 {formatter.format_price_with_symbol(trade['value'])}\n"
-            trades_text += f"   📅 {timestamp}\n\n"
-        
-        await context.bot.send_message(chat_id=chat_id, text=trades_text, parse_mode='HTML')
+            
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                await update.message.reply_text("❌ Trading statistics not available.", parse_mode='HTML')
+                return
+            
+            # Get trade cycle performance stats
+            cycle_stats = stats.get_trade_cycle_performance_stats()
+            
+            if not cycle_stats or cycle_stats.get('total_closed_trades', 0) == 0:
+                await update.message.reply_text("📊 <b>No completed trade cycles found.</b>", parse_mode='HTML')
+                return
+            
+            # Get recent trade cycles
+            recent_cycles = stats.get_recent_trade_cycles(limit=10)
+            open_cycles = stats.get_open_trade_cycles()
+            
+            message = "🔄 <b>Trade Cycle Statistics</b>\n\n"
+            
+            # Performance summary
+            total_trades = cycle_stats.get('total_closed_trades', 0)
+            win_rate = cycle_stats.get('win_rate', 0)
+            total_pnl = cycle_stats.get('total_pnl', 0)
+            avg_duration = cycle_stats.get('avg_duration_minutes', 0)
+            profit_factor = cycle_stats.get('profit_factor', 0)
+            stop_loss_rate = cycle_stats.get('stop_loss_rate', 0)
+            
+            pnl_emoji = "🟢" if total_pnl >= 0 else "🔴"
+            
+            message += f"📊 <b>Performance Summary:</b>\n"
+            message += f"• Total Completed: {total_trades} trades\n"
+            message += f"• Win Rate: {win_rate:.1f}%\n"
+            message += f"• {pnl_emoji} Total P&L: ${total_pnl:,.2f}\n"
+            message += f"• Avg Duration: {avg_duration:.1f} min\n"
+            message += f"• Profit Factor: {profit_factor:.2f}\n"
+            message += f"• Stop Loss Rate: {stop_loss_rate:.1f}%\n\n"
+            
+            # Open cycles
+            if open_cycles:
+                message += f"🟢 <b>Open Cycles ({len(open_cycles)}):</b>\n"
+                for cycle in open_cycles[:5]:  # Show max 5
+                    symbol = cycle['symbol']
+                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                    side = cycle['side'].upper()
+                    entry_price = cycle.get('entry_price', 0)
+                    side_emoji = "📈" if side == 'BUY' else "📉"
+                    message += f"{side_emoji} {side} {token} @ ${entry_price:.2f}\n"
+                message += "\n"
+            
+            # Recent completed cycles
+            if recent_cycles:
+                completed_recent = [c for c in recent_cycles if c['status'] == 'closed'][:5]
+                if completed_recent:
+                    message += f"📋 <b>Recent Completed ({len(completed_recent)}):</b>\n"
+                    for cycle in completed_recent:
+                        symbol = cycle['symbol']
+                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                        side = cycle['side'].upper()
+                        entry_price = cycle.get('entry_price', 0)
+                        exit_price = cycle.get('exit_price', 0)
+                        pnl = cycle.get('realized_pnl', 0)
+                        exit_type = cycle.get('exit_type', 'unknown')
+                        duration = cycle.get('duration_seconds', 0)
+                        
+                        # Format duration
+                        if duration > 3600:
+                            duration_str = f"{duration//3600:.0f}h"
+                        elif duration > 60:
+                            duration_str = f"{duration//60:.0f}m"
+                        else:
+                            duration_str = f"{duration}s"
+                        
+                        pnl_emoji = "🟢" if pnl >= 0 else "🔴"
+                        side_emoji = "📈" if side == 'BUY' else "📉"
+                        exit_emoji = "🛑" if exit_type == 'stop_loss' else "🎯" if exit_type == 'take_profit' else "👋"
+                        
+                        message += f"{side_emoji} {side} {token}: ${entry_price:.2f} → ${exit_price:.2f}\n"
+                        message += f"   {pnl_emoji} ${pnl:+.2f} | {exit_emoji} {exit_type} | {duration_str}\n"
+                    message += "\n"
+            
+            message += "💡 Trade cycles track complete trades from open to close with full P&L analysis."
+            
+            await update.message.reply_text(message, parse_mode='HTML')
+            
+        except Exception as e:
+            logger.error(f"Error in cycles command: {e}")
+            await update.message.reply_text("❌ Error retrieving trade cycle data.", parse_mode='HTML')
     
     async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /market command."""

+ 270 - 59
src/monitoring/market_monitor.py

@@ -18,11 +18,16 @@ logger = logging.getLogger(__name__)
 class MarketMonitor:
     """Handles external trade monitoring and market events."""
     
-    def __init__(self, trading_engine):
+    def __init__(self, trading_engine, notification_manager=None):
         """Initialize the market monitor."""
         self.trading_engine = trading_engine
-        self.is_running = False
-        self._monitor_task = None
+        self.notification_manager = notification_manager
+        self.client = trading_engine.client
+        self._last_processed_trade_time = datetime.now(timezone.utc) - timedelta(seconds=120)
+        self._monitoring_active = False
+        
+        # 🆕 External stop loss tracking
+        self._external_stop_loss_orders = {}  # Format: {exchange_order_id: {'token': str, 'trigger_price': float, 'side': str, 'detected_at': datetime}}
         
         # External trade monitoring
         # self.state_file = "data/market_monitor_state.json" # Removed, state now in DB
@@ -35,21 +40,14 @@ class MarketMonitor:
         self.last_known_orders = set()
         self.last_known_positions = {}
         
-        # Notification manager (will be set by the core bot)
-        self.notification_manager = None
-        
         self._load_state()
         
-    def set_notification_manager(self, notification_manager):
-        """Set the notification manager for sending alerts."""
-        self.notification_manager = notification_manager
-        
     async def start(self):
         """Start the market monitor."""
-        if self.is_running:
+        if self._monitoring_active:
             return
         
-        self.is_running = True
+        self._monitoring_active = True
         logger.info("🔄 Market monitor started")
         
         # Initialize tracking
@@ -60,10 +58,10 @@ class MarketMonitor:
     
     async def stop(self):
         """Stop the market monitor."""
-        if not self.is_running:
+        if not self._monitoring_active:
             return
         
-        self.is_running = False
+        self._monitoring_active = False
         
         if self._monitor_task:
             self._monitor_task.cancel()
@@ -157,17 +155,19 @@ class MarketMonitor:
         """Main monitoring loop that runs every BOT_HEARTBEAT_SECONDS."""
         try:
             loop_count = 0
-            while self.is_running:
+            while self._monitoring_active:
                 await self._check_order_fills()
                 await self._check_price_alarms()
                 await self._check_external_trades()
                 await self._check_pending_triggers()
                 await self._check_automatic_risk_management()
+                await self._check_external_stop_loss_orders()
                 
                 # Run orphaned stop loss cleanup every 10 heartbeats (less frequent but regular)
                 loop_count += 1
                 if loop_count % 10 == 0:
                     await self._cleanup_orphaned_stop_losses()
+                    await self._cleanup_external_stop_loss_tracking()
                     loop_count = 0  # Reset counter to prevent overflow
                 
                 await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
@@ -177,7 +177,7 @@ class MarketMonitor:
         except Exception as e:
             logger.error(f"Error in market monitor loop: {e}")
             # Restart after error
-            if self.is_running:
+            if self._monitoring_active:
                 await asyncio.sleep(5)
                 await self._monitor_loop()
     
@@ -244,7 +244,7 @@ class MarketMonitor:
                 
                 if not old_data:
                     # New position opened
-                    logger.info(f"📈 New position detected (observed by MarketMonitor): {symbol} {new_data['contracts']} @ ${new_data['entry_price']}. TradingStats is the definitive source.")
+                    logger.info(f"📈 New position detected (observed by MarketMonitor): {symbol} {new_data['contracts']} @ ${new_data['entry_price']:.4f}. TradingStats is the definitive source.")
                 elif abs(new_data['contracts'] - old_data['contracts']) > 0.000001:
                     # Position size changed
                     change = new_data['contracts'] - old_data['contracts']
@@ -507,59 +507,142 @@ class MarketMonitor:
                             # Look for Hyperliquid order ID in the raw response
                             exchange_order_id_from_fill = fill['info'].get('oid')
                         
-                        logger.info(f"🔍 Processing external trade: {trade_id} - {side} {amount} {full_symbol} @ ${price:.2f}")
+                        # 🆕 Check if this fill corresponds to an external stop loss order
+                        is_external_stop_loss = False
+                        stop_loss_info = None
+                        if exchange_order_id_from_fill and exchange_order_id_from_fill in self._external_stop_loss_orders:
+                            is_external_stop_loss = True
+                            stop_loss_info = self._external_stop_loss_orders[exchange_order_id_from_fill]
+                            logger.info(f"🛑 EXTERNAL STOP LOSS EXECUTION: {token} - Order {exchange_order_id_from_fill} filled @ ${price:.2f}")
+                        
+                        logger.info(f"🔍 Processing {'external stop loss' if is_external_stop_loss else 'external trade'}: {trade_id} - {side} {amount} {full_symbol} @ ${price:.2f}")
                         
                         stats = self.trading_engine.stats
-                        if stats:
-                            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['id']
-                                    logger.info(f"🔗 Linking fill {trade_id} to order DB ID {linked_order_db_id} (Exchange Order ID: {exchange_order_id_from_fill})")
-                                    # Update the order status and amount filled
-                                    new_status_after_fill = order_in_db['status'] # Default to current
-                                    current_filled = order_in_db.get('amount_filled', 0.0)
-                                    requested_amount = order_in_db.get('amount_requested', 0.0)
-                                    
-                                    if abs((current_filled + amount) - requested_amount) < 1e-9: # Comparing floats
-                                        new_status_after_fill = 'filled'
-                                    elif (current_filled + amount) < requested_amount:
-                                        new_status_after_fill = 'partially_filled'
-                                    else: # Overfilled? Or issue with amounts. Log a warning.
-                                        logger.warning(f"Order {linked_order_db_id} might be overfilled. Current: {current_filled}, Fill: {amount}, Requested: {requested_amount}")
-                                        new_status_after_fill = 'filled' # Assume filled for now if it exceeds
-
-                                    stats.update_order_status(order_db_id=linked_order_db_id, 
-                                                            new_status=new_status_after_fill, 
-                                                            amount_filled_increment=amount)
-                                    
-                                    # Check if this order is now fully filled and has pending stop losses to activate
-                                    if new_status_after_fill == 'filled':
-                                        await self._activate_pending_stop_losses(order_in_db, stats)
-                            
-                            # Record the trade in stats with enhanced tracking
+                        if not stats:
+                            logger.warning("⚠️ TradingStats not available in _check_external_trades.")
+                            continue
+                        
+                        # If this is an external stop loss execution, handle it specially
+                        if is_external_stop_loss and stop_loss_info:
+                            # Record the trade with enhanced tracking but mark it as stop loss execution
                             action_type = stats.record_trade_with_enhanced_tracking(
                                 full_symbol, side, amount, price, 
-                                exchange_fill_id=trade_id, trade_type="external",
+                                exchange_fill_id=trade_id, trade_type="external_stop_loss",
                                 timestamp=timestamp_dt.isoformat(),
-                                linked_order_table_id_to_link=linked_order_db_id
+                                linked_order_table_id_to_link=None  # External stop losses don't link to bot orders
                             )
                             
-                            # Track symbol for potential stop loss activation
-                            symbols_with_fills.add(token)
+                            # 🆕 Handle trade cycle closure for external stop loss
+                            # Find open trade cycle for this symbol and close it
+                            open_trade_cycles = stats.get_open_trade_cycles(full_symbol)
+                            for trade_cycle in open_trade_cycles:
+                                if trade_cycle['status'] == 'open':
+                                    stats.update_trade_cycle_closed(
+                                        trade_cycle['id'], trade_id, price, amount,
+                                        timestamp_dt.isoformat(), 'stop_loss', None
+                                    )
+                                    logger.info(f"📊 Trade cycle {trade_cycle['id']} closed via external stop loss {trade_id}")
+                                    break  # Only close one trade cycle per stop loss execution
                             
-                            # Send notification for external trade
+                            # Send specialized stop loss execution notification
                             if self.notification_manager:
-                                await self.notification_manager.send_external_trade_notification(
-                                    full_symbol, side, amount, price, action_type, timestamp_dt.isoformat()
+                                await self.notification_manager.send_stop_loss_execution_notification(
+                                    stop_loss_info, full_symbol, side, amount, price, action_type, timestamp_dt.isoformat()
                                 )
                             
-                            logger.info(f"📋 Processed external trade: {side} {amount} {full_symbol} @ ${price:.2f} ({action_type}) using timestamp {timestamp_dt.isoformat()}")
-                            external_trades_processed += 1
+                            # Remove from tracking since it's now executed
+                            del self._external_stop_loss_orders[exchange_order_id_from_fill]
                             
-                            # Update last processed time
-                            self._last_processed_trade_time = timestamp_dt
+                            logger.info(f"🛑 Processed external stop loss execution: {side} {amount} {full_symbol} @ ${price:.2f} ({action_type})")
+                        
+                        else:
+                            # Handle as regular external trade
+                            # Check if this corresponds to a bot order by exchange_order_id
+                            linked_order_db_id = None
+                            if exchange_order_id_from_fill:
+                                order_in_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+                                if order_in_db:
+                                    linked_order_db_id = order_in_db.get('id')
+                                    logger.info(f"🔗 Linked external fill {trade_id} to bot order DB ID {linked_order_db_id} (Exchange OID: {exchange_order_id_from_fill})")
+                                    
+                                    # Update order status to filled if it was open
+                                    current_status = order_in_db.get('status', '')
+                                    if current_status in ['open', 'partially_filled', 'pending_submission']:
+                                        # Determine if this is a partial or full fill
+                                        order_amount_requested = float(order_in_db.get('amount_requested', 0))
+                                        if abs(amount - order_amount_requested) < 0.000001:  # Allow small floating point differences
+                                            new_status_after_fill = 'filled'
+                                        else:
+                                            new_status_after_fill = 'partially_filled'
+                                        
+                                        stats.update_order_status(
+                                            order_db_id=linked_order_db_id, 
+                                            new_status=new_status_after_fill
+                                        )
+                                        logger.info(f"📊 Updated bot order {linked_order_db_id} status: {current_status} → {new_status_after_fill}")
+                                        
+                                        # Check if this order is now fully filled and has pending stop losses to activate
+                                        if new_status_after_fill == 'filled':
+                                            await self._activate_pending_stop_losses(order_in_db, stats)
+                                
+                                # Record the trade in stats with enhanced tracking
+                                action_type = stats.record_trade_with_enhanced_tracking(
+                                    full_symbol, side, amount, price, 
+                                    exchange_fill_id=trade_id, trade_type="external",
+                                    timestamp=timestamp_dt.isoformat(),
+                                    linked_order_table_id_to_link=linked_order_db_id
+                                )
+                                
+                                # 🆕 Update trade cycle based on the action
+                                if linked_order_db_id:
+                                    # This fill is linked to a bot order - check if we need to update trade cycle
+                                    trade_cycle = stats.get_trade_cycle_by_entry_order(linked_order_db_id)
+                                    if trade_cycle:
+                                        if trade_cycle['status'] == 'pending_open' and action_type in ['long_opened', 'short_opened', 'position_opened']:
+                                            # Entry order filled - update trade cycle to opened
+                                            stats.update_trade_cycle_opened(
+                                                trade_cycle['id'], trade_id, price, amount, timestamp_dt.isoformat()
+                                            )
+                                            logger.info(f"📊 Trade cycle {trade_cycle['id']} opened via fill {trade_id}")
+                                            
+                                        elif trade_cycle['status'] == 'open' and action_type in ['long_closed', 'short_closed', 'position_closed']:
+                                            # Exit order filled - update trade cycle to closed
+                                            exit_type = 'manual'  # Default for manual exits
+                                            stats.update_trade_cycle_closed(
+                                                trade_cycle['id'], trade_id, price, amount, 
+                                                timestamp_dt.isoformat(), exit_type, linked_order_db_id
+                                            )
+                                            logger.info(f"📊 Trade cycle {trade_cycle['id']} closed via fill {trade_id}")
+                                elif action_type in ['long_opened', 'short_opened', 'position_opened']:
+                                    # External trade that opened a position - create external trade cycle
+                                    side_for_cycle = 'buy' if side.lower() == 'buy' else 'sell'
+                                    trade_cycle_id = stats.create_trade_cycle(
+                                        symbol=full_symbol,
+                                        side=side_for_cycle,
+                                        entry_order_id=None,  # External order
+                                        trade_type='external'
+                                    )
+                                    if trade_cycle_id:
+                                        stats.update_trade_cycle_opened(
+                                            trade_cycle_id, trade_id, price, amount, timestamp_dt.isoformat()
+                                        )
+                                        logger.info(f"📊 Created external trade cycle {trade_cycle_id} for {side.upper()} {full_symbol}")
+                                
+                                # Track symbol for potential stop loss activation
+                                symbols_with_fills.add(token)
+                                
+                                # Send notification for external trade
+                                if self.notification_manager:
+                                    await self.notification_manager.send_external_trade_notification(
+                                        full_symbol, side, amount, price, action_type, timestamp_dt.isoformat()
+                                    )
+                                
+                                logger.info(f"📋 Processed external trade: {side} {amount} {full_symbol} @ ${price:.2f} ({action_type}) using timestamp {timestamp_dt.isoformat()}")
+                        
+                        external_trades_processed += 1
+                        
+                        # Update last processed time
+                        self._last_processed_trade_time = timestamp_dt
                         
                 except Exception as e:
                     logger.error(f"Error processing fill {fill}: {e}")
@@ -1121,4 +1204,132 @@ class MarketMonitor:
 
         except Exception as e:
             logger.error(f"❌ Error checking for recent fills for order: {e}", exc_info=True)
-            return False 
+            return False 
+
+    async def _check_external_stop_loss_orders(self):
+        """Check for externally placed stop loss orders and track them."""
+        try:
+            # Get current open orders 
+            open_orders = self.trading_engine.get_orders()
+            if not open_orders:
+                return
+                
+            # Get current positions to understand what could be stop losses
+            positions = self.trading_engine.get_positions()
+            if not positions:
+                return
+                
+            # Create a map of current positions
+            position_map = {}
+            for position in positions:
+                symbol = position.get('symbol')
+                contracts = float(position.get('contracts', 0))
+                if symbol and contracts != 0:
+                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                    position_map[token] = {
+                        'symbol': symbol,
+                        'contracts': contracts,
+                        'side': 'long' if contracts > 0 else 'short',
+                        'entry_price': float(position.get('entryPx', 0))
+                    }
+            
+            # Check each order to see if it could be a stop loss
+            newly_detected = 0
+            for order in open_orders:
+                try:
+                    exchange_order_id = order.get('id')
+                    symbol = order.get('symbol')
+                    side = order.get('side')  # 'buy' or 'sell'
+                    amount = float(order.get('amount', 0))
+                    price = float(order.get('price', 0))
+                    order_type = order.get('type', '').lower()
+                    
+                    if not all([exchange_order_id, symbol, side, amount, price]):
+                        continue
+                        
+                    # Skip if we're already tracking this order
+                    if exchange_order_id in self._external_stop_loss_orders:
+                        continue
+                        
+                    # Check if this order could be a stop loss
+                    token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                    
+                    # Must have a position in this token to have a stop loss
+                    if token not in position_map:
+                        continue
+                        
+                    position = position_map[token]
+                    
+                    # Check if this order matches stop loss pattern
+                    is_stop_loss = False
+                    
+                    if position['side'] == 'long' and side == 'sell':
+                        # Long position with sell order - could be stop loss if price is below entry
+                        if price < position['entry_price'] * 0.98:  # Allow 2% buffer for approximation
+                            is_stop_loss = True
+                            
+                    elif position['side'] == 'short' and side == 'buy':
+                        # Short position with buy order - could be stop loss if price is above entry  
+                        if price > position['entry_price'] * 1.02:  # Allow 2% buffer for approximation
+                            is_stop_loss = True
+                    
+                    if is_stop_loss:
+                        # Track this as an external stop loss order
+                        self._external_stop_loss_orders[exchange_order_id] = {
+                            'token': token,
+                            'symbol': symbol,
+                            'trigger_price': price,
+                            'side': side,
+                            'amount': amount,
+                            'position_side': position['side'],
+                            'detected_at': datetime.now(timezone.utc),
+                            'entry_price': position['entry_price']
+                        }
+                        newly_detected += 1
+                        logger.info(f"🛑 Detected external stop loss order: {token} {side.upper()} {amount} @ ${price:.2f} (protecting {position['side'].upper()} position)")
+                        
+                except Exception as e:
+                    logger.error(f"Error analyzing order for stop loss detection: {e}")
+                    continue
+                    
+            if newly_detected > 0:
+                logger.info(f"🔍 Detected {newly_detected} new external stop loss orders")
+                
+        except Exception as e:
+            logger.error(f"❌ Error checking external stop loss orders: {e}")
+            
+    async def _cleanup_external_stop_loss_tracking(self):
+        """Clean up external stop loss orders that are no longer active."""
+        try:
+            if not self._external_stop_loss_orders:
+                return
+                
+            # Get current open orders
+            open_orders = self.trading_engine.get_orders()
+            if not open_orders:
+                # No open orders, clear all tracking
+                removed_count = len(self._external_stop_loss_orders)
+                self._external_stop_loss_orders.clear()
+                if removed_count > 0:
+                    logger.info(f"🧹 Cleared {removed_count} external stop loss orders (no open orders)")
+                return
+                
+            # Get set of current order IDs
+            current_order_ids = {order.get('id') for order in open_orders if order.get('id')}
+            
+            # Remove any tracked stop loss orders that are no longer open
+            to_remove = []
+            for order_id, stop_loss_info in self._external_stop_loss_orders.items():
+                if order_id not in current_order_ids:
+                    to_remove.append(order_id)
+                    
+            for order_id in to_remove:
+                stop_loss_info = self._external_stop_loss_orders[order_id]
+                del self._external_stop_loss_orders[order_id]
+                logger.info(f"🗑️ Removed external stop loss tracking for {stop_loss_info['token']} order {order_id} (no longer open)")
+                
+            if to_remove:
+                logger.info(f"🧹 Cleaned up {len(to_remove)} external stop loss orders")
+                
+        except Exception as e:
+            logger.error(f"❌ Error cleaning up external stop loss tracking: {e}") 

+ 90 - 1
src/notifications/notification_manager.py

@@ -482,4 +482,93 @@ class NotificationManager:
                 )
                 logger.info("Generic notification sent")
         except Exception as e:
-            logger.error(f"Failed to send generic notification: {e}") 
+            logger.error(f"Failed to send generic notification: {e}")
+    
+    async def send_stop_loss_execution_notification(self, stop_loss_info: Dict, symbol: str, side: str, amount: float, price: float, action_type: str, timestamp: str):
+        """Send notification for external stop loss execution."""
+        try:
+            token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+            
+            # Extract stop loss details
+            trigger_price = stop_loss_info.get('trigger_price', price)
+            position_side = stop_loss_info.get('position_side', 'unknown')
+            entry_price = stop_loss_info.get('entry_price', 0)
+            detected_at = stop_loss_info.get('detected_at')
+            
+            # Format timestamp
+            try:
+                time_obj = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
+                time_str = time_obj.strftime('%H:%M:%S')
+            except:
+                time_str = "Unknown"
+            
+            # Calculate P&L if we have entry price
+            pnl_info = ""
+            if entry_price > 0:
+                if position_side == 'long':
+                    pnl = amount * (price - entry_price)
+                    pnl_percent = ((price - entry_price) / entry_price) * 100
+                else:  # short
+                    pnl = amount * (entry_price - price) 
+                    pnl_percent = ((entry_price - price) / entry_price) * 100
+                
+                pnl_emoji = "🟢" if pnl >= 0 else "🔴"
+                pnl_info = f"""
+{pnl_emoji} <b>Stop Loss P&L:</b>
+• Entry Price: ${entry_price:,.2f}
+• Exit Price: ${price:,.2f}
+• Realized P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)
+• Result: {"PROFIT" if pnl >= 0 else "LOSS PREVENTION"}"""
+
+            # Determine stop loss effectiveness
+            effectiveness = ""
+            if entry_price > 0 and trigger_price > 0:
+                if position_side == 'long':
+                    # For longs, stop loss should trigger below entry
+                    if trigger_price < entry_price:
+                        loss_percent = ((trigger_price - entry_price) / entry_price) * 100
+                        effectiveness = f"• Loss Limited: {loss_percent:.1f}% ✅"
+                    else:
+                        effectiveness = "• Unusual: Stop above entry 🟡"
+                else:  # short
+                    # For shorts, stop loss should trigger above entry  
+                    if trigger_price > entry_price:
+                        loss_percent = ((entry_price - trigger_price) / entry_price) * 100
+                        effectiveness = f"• Loss Limited: {loss_percent:.1f}% ✅"
+                    else:
+                        effectiveness = "• Unusual: Stop below entry 🟡"
+
+            trade_value = amount * price
+            position_emoji = "📈" if position_side == 'long' else "📉"
+            
+            message = f"""
+🛑 <b>STOP LOSS EXECUTED</b>
+
+{position_emoji} <b>{position_side.upper()} Position Protected:</b>
+• Token: {token}
+• Position Type: {position_side.upper()}
+• Stop Loss Size: {amount} {token}
+• Trigger Price: ${trigger_price:,.2f}
+• Execution Price: ${price:,.2f}
+• Exit Value: ${trade_value:,.2f}
+
+🎯 <b>Stop Loss Details:</b>
+• Status: EXECUTED ✅
+• Order Side: {side.upper()}
+• Action Type: {action_type.replace('_', ' ').title()}
+{effectiveness}
+
+{pnl_info}
+
+⏰ <b>Execution Time:</b> {time_str}
+🤖 <b>Source:</b> External Hyperliquid Order
+📊 <b>Risk Management:</b> Loss prevention successful
+
+💡 Your external stop loss order worked as intended!
+            """
+            
+            await self.send_notification(message.strip())
+            logger.info(f"🛑 Stop loss execution notification sent: {token} {position_side} @ ${price:.2f}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error sending stop loss execution notification: {e}") 

+ 55 - 0
src/trading/trading_engine.py

@@ -210,6 +210,29 @@ class TradingEngine:
                 else:
                     logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
             
+            # 🆕 Create trade cycle for this entry order (only for main orders, not stop losses)
+            if exchange_oid:
+                entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
+                if entry_order_record:
+                    trade_cycle_id = self.stats.create_trade_cycle(
+                        symbol=symbol,
+                        side='buy',
+                        entry_order_id=entry_order_record['id'],
+                        stop_loss_price=stop_loss_price,
+                        trade_type='bot'
+                    )
+                    
+                    if trade_cycle_id and stop_loss_price:
+                        # Get the stop loss order that was just created
+                        sl_order_record = self.stats.get_order_by_bot_ref_id(sl_bot_order_ref_id)
+                        if sl_order_record:
+                            self.stats.link_stop_loss_to_trade_cycle(trade_cycle_id, sl_order_record['id'], stop_loss_price)
+                            logger.info(f"📊 Created trade cycle {trade_cycle_id} with linked stop loss for BUY {symbol}")
+                        else:
+                            logger.info(f"📊 Created trade cycle {trade_cycle_id} for BUY {symbol}")
+                    elif trade_cycle_id:
+                        logger.info(f"📊 Created trade cycle {trade_cycle_id} for BUY {symbol}")
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -333,6 +356,29 @@ class TradingEngine:
                 else:
                     logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
             
+            # 🆕 Create trade cycle for this entry order (only for main orders, not stop losses)
+            if exchange_oid:
+                entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
+                if entry_order_record:
+                    trade_cycle_id = self.stats.create_trade_cycle(
+                        symbol=symbol,
+                        side='sell',
+                        entry_order_id=entry_order_record['id'],
+                        stop_loss_price=stop_loss_price,
+                        trade_type='bot'
+                    )
+                    
+                    if trade_cycle_id and stop_loss_price:
+                        # Get the stop loss order that was just created
+                        sl_order_record = self.stats.get_order_by_bot_ref_id(sl_bot_order_ref_id)
+                        if sl_order_record:
+                            self.stats.link_stop_loss_to_trade_cycle(trade_cycle_id, sl_order_record['id'], stop_loss_price)
+                            logger.info(f"📊 Created trade cycle {trade_cycle_id} with linked stop loss for SELL {symbol}")
+                        else:
+                            logger.info(f"📊 Created trade cycle {trade_cycle_id} for SELL {symbol}")
+                    elif trade_cycle_id:
+                        logger.info(f"📊 Created trade cycle {trade_cycle_id} for SELL {symbol}")
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -423,6 +469,9 @@ class TradingEngine:
                 if cancelled_sl_count > 0:
                     logger.info(f"🛑 Cancelled {cancelled_sl_count} pending stop losses for {symbol} due to manual exit order")
             
+            # NOTE: Exit orders do not create new trade cycles - they close existing ones
+            # The MarketMonitor will handle closing the trade cycle when the exit order fills
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -498,6 +547,9 @@ class TradingEngine:
             else:
                 logger.warning(f"No exchange_order_id received for SL order {order_db_id} ({bot_order_ref_id}).")
             
+            # NOTE: Stop loss orders are protective orders for existing positions
+            # They do not create new trade cycles - they protect existing trade cycles
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -574,6 +626,9 @@ class TradingEngine:
             else:
                 logger.warning(f"No exchange_order_id received for TP order {order_db_id} ({bot_order_ref_id}).")
             
+            # NOTE: Take profit orders are protective orders for existing positions
+            # They do not create new trade cycles - they protect existing trade cycles
+            
             return {
                 "success": True,
                 "order_placed_details": {

+ 331 - 0
src/trading/trading_stats.py

@@ -146,6 +146,52 @@ class TradingStats:
             """,
             """
             CREATE INDEX IF NOT EXISTS idx_orders_status_type ON orders (status, type);
+            """,
+            """
+            CREATE TABLE IF NOT EXISTS trade_cycles (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                symbol TEXT NOT NULL,
+                side TEXT NOT NULL, -- 'long' or 'short'
+                status TEXT NOT NULL, -- 'pending_open', 'open', 'closed', 'cancelled'
+                
+                -- Opening details
+                entry_order_id INTEGER, -- FK to orders table
+                entry_fill_id TEXT, -- FK to trades table (exchange_fill_id)
+                entry_price REAL,
+                entry_amount REAL,
+                entry_timestamp TEXT,
+                
+                -- Closing details  
+                exit_order_id INTEGER, -- FK to orders table
+                exit_fill_id TEXT, -- FK to trades table
+                exit_price REAL,
+                exit_amount REAL,
+                exit_timestamp TEXT,
+                exit_type TEXT, -- 'stop_loss', 'take_profit', 'manual', 'external'
+                
+                -- P&L and metrics
+                realized_pnl REAL,
+                pnl_percentage REAL,
+                duration_seconds INTEGER,
+                
+                -- Risk management
+                stop_loss_price REAL,
+                stop_loss_order_id INTEGER, -- FK to orders table
+                take_profit_price REAL,
+                take_profit_order_id INTEGER,
+                
+                -- Metadata
+                trade_type TEXT DEFAULT 'manual', -- 'manual', 'bot', 'external'
+                notes TEXT,
+                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
+                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
+                
+                -- Foreign key constraints
+                FOREIGN KEY (entry_order_id) REFERENCES orders (id),
+                FOREIGN KEY (exit_order_id) REFERENCES orders (id),
+                FOREIGN KEY (stop_loss_order_id) REFERENCES orders (id),
+                FOREIGN KEY (take_profit_order_id) REFERENCES orders (id)
+            )
             """
         ]
         for query in queries:
@@ -1345,3 +1391,288 @@ class TradingStats:
             return {'period_days': days, 'total_external_trades': 0, 'external_cancellations': 0}
 
     # --- End Order Table Management ---
+
+    # =============================================================================
+    # TRADE LIFECYCLE MANAGEMENT
+    # =============================================================================
+    
+    def create_trade_cycle(self, symbol: str, side: str, entry_order_id: int, 
+                          stop_loss_price: Optional[float] = None, 
+                          take_profit_price: Optional[float] = None,
+                          trade_type: str = 'manual') -> Optional[int]:
+        """Create a new trade cycle when an entry order is placed."""
+        try:
+            query = """
+                INSERT INTO trade_cycles (
+                    symbol, side, status, entry_order_id, stop_loss_price, 
+                    take_profit_price, trade_type, created_at, updated_at
+                ) VALUES (?, ?, 'pending_open', ?, ?, ?, ?, ?, ?)
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (symbol, side.lower(), entry_order_id, stop_loss_price, 
+                     take_profit_price, trade_type, timestamp, timestamp)
+            
+            cursor = self.conn.execute(query, params)
+            trade_cycle_id = cursor.lastrowid
+            self.conn.commit()
+            
+            logger.info(f"📊 Created trade cycle {trade_cycle_id}: {side.upper()} {symbol} (pending open)")
+            return trade_cycle_id
+            
+        except Exception as e:
+            logger.error(f"❌ Error creating trade cycle: {e}")
+            return None
+    
+    def update_trade_cycle_opened(self, trade_cycle_id: int, entry_fill_id: str, 
+                                 entry_price: float, entry_amount: float, 
+                                 entry_timestamp: str) -> bool:
+        """Update trade cycle when entry order is filled (trade opened)."""
+        try:
+            query = """
+                UPDATE trade_cycles 
+                SET status = 'open',
+                    entry_fill_id = ?,
+                    entry_price = ?,
+                    entry_amount = ?,
+                    entry_timestamp = ?,
+                    updated_at = ?
+                WHERE id = ?
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (entry_fill_id, entry_price, entry_amount, entry_timestamp, timestamp, trade_cycle_id)
+            
+            self._execute_query(query, params)
+            
+            logger.info(f"📈 Trade cycle {trade_cycle_id} opened: {entry_amount} @ ${entry_price:.2f}")
+            return True
+            
+        except Exception as e:
+            logger.error(f"❌ Error updating trade cycle opened: {e}")
+            return False
+    
+    def update_trade_cycle_closed(self, trade_cycle_id: int, exit_fill_id: str,
+                                 exit_price: float, exit_amount: float, 
+                                 exit_timestamp: str, exit_type: str,
+                                 exit_order_id: Optional[int] = None) -> bool:
+        """Update trade cycle when exit order is filled (trade closed)."""
+        try:
+            # Get trade cycle details to calculate P&L
+            trade_cycle = self.get_trade_cycle(trade_cycle_id)
+            if not trade_cycle:
+                logger.error(f"Trade cycle {trade_cycle_id} not found for closing")
+                return False
+            
+            # Calculate P&L and duration
+            entry_price = trade_cycle['entry_price']
+            entry_timestamp_str = trade_cycle['entry_timestamp']
+            side = trade_cycle['side']
+            
+            # Calculate P&L
+            if side == 'long':
+                realized_pnl = exit_amount * (exit_price - entry_price)
+            else:  # short
+                realized_pnl = exit_amount * (entry_price - exit_price)
+            
+            pnl_percentage = (realized_pnl / (exit_amount * entry_price)) * 100
+            
+            # Calculate duration
+            duration_seconds = 0
+            try:
+                entry_dt = datetime.fromisoformat(entry_timestamp_str.replace('Z', '+00:00'))
+                exit_dt = datetime.fromisoformat(exit_timestamp.replace('Z', '+00:00'))
+                duration_seconds = int((exit_dt - entry_dt).total_seconds())
+            except Exception as e:
+                logger.warning(f"Could not calculate trade duration: {e}")
+            
+            query = """
+                UPDATE trade_cycles 
+                SET status = 'closed',
+                    exit_order_id = ?,
+                    exit_fill_id = ?,
+                    exit_price = ?,
+                    exit_amount = ?,
+                    exit_timestamp = ?,
+                    exit_type = ?,
+                    realized_pnl = ?,
+                    pnl_percentage = ?,
+                    duration_seconds = ?,
+                    updated_at = ?
+                WHERE id = ?
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (exit_order_id, exit_fill_id, exit_price, exit_amount, 
+                     exit_timestamp, exit_type, realized_pnl, pnl_percentage, 
+                     duration_seconds, timestamp, trade_cycle_id)
+            
+            self._execute_query(query, params)
+            
+            pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+            logger.info(f"{pnl_emoji} Trade cycle {trade_cycle_id} closed: {exit_type} @ ${exit_price:.2f} | P&L: ${realized_pnl:.2f} ({pnl_percentage:+.2f}%) | Duration: {duration_seconds}s")
+            return True
+            
+        except Exception as e:
+            logger.error(f"❌ Error updating trade cycle closed: {e}")
+            return False
+    
+    def update_trade_cycle_cancelled(self, trade_cycle_id: int, reason: str = "order_cancelled") -> bool:
+        """Update trade cycle when entry order is cancelled (trade never opened)."""
+        try:
+            query = """
+                UPDATE trade_cycles 
+                SET status = 'cancelled',
+                    notes = ?,
+                    updated_at = ?
+                WHERE id = ?
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (f"Cancelled: {reason}", timestamp, trade_cycle_id)
+            
+            self._execute_query(query, params)
+            
+            logger.info(f"❌ Trade cycle {trade_cycle_id} cancelled: {reason}")
+            return True
+            
+        except Exception as e:
+            logger.error(f"❌ Error updating trade cycle cancelled: {e}")
+            return False
+    
+    def link_stop_loss_to_trade_cycle(self, trade_cycle_id: int, stop_loss_order_id: int, 
+                                     stop_loss_price: float) -> bool:
+        """Link a stop loss order to an open trade cycle."""
+        try:
+            query = """
+                UPDATE trade_cycles 
+                SET stop_loss_order_id = ?,
+                    stop_loss_price = ?,
+                    updated_at = ?
+                WHERE id = ? AND status = 'open'
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (stop_loss_order_id, stop_loss_price, timestamp, trade_cycle_id)
+            
+            self._execute_query(query, params)
+            
+            logger.info(f"🛑 Linked stop loss order {stop_loss_order_id} (${stop_loss_price:.2f}) to trade cycle {trade_cycle_id}")
+            return True
+            
+        except Exception as e:
+            logger.error(f"❌ Error linking stop loss to trade cycle: {e}")
+            return False
+    
+    def link_take_profit_to_trade_cycle(self, trade_cycle_id: int, take_profit_order_id: int,
+                                       take_profit_price: float) -> bool:
+        """Link a take profit order to an open trade cycle."""
+        try:
+            query = """
+                UPDATE trade_cycles 
+                SET take_profit_order_id = ?,
+                    take_profit_price = ?,
+                    updated_at = ?
+                WHERE id = ? AND status = 'open'
+            """
+            timestamp = datetime.now(timezone.utc).isoformat()
+            params = (take_profit_order_id, take_profit_price, timestamp, trade_cycle_id)
+            
+            self._execute_query(query, params)
+            
+            logger.info(f"🎯 Linked take profit order {take_profit_order_id} (${take_profit_price:.2f}) to trade cycle {trade_cycle_id}")
+            return True
+            
+        except Exception as e:
+            logger.error(f"❌ Error linking take profit to trade cycle: {e}")
+            return False
+    
+    def get_trade_cycle(self, trade_cycle_id: int) -> Optional[Dict[str, Any]]:
+        """Get a trade cycle by ID."""
+        query = "SELECT * FROM trade_cycles WHERE id = ?"
+        return self._fetchone_query(query, (trade_cycle_id,))
+    
+    def get_trade_cycle_by_entry_order(self, entry_order_id: int) -> Optional[Dict[str, Any]]:
+        """Get a trade cycle by entry order ID."""
+        query = "SELECT * FROM trade_cycles WHERE entry_order_id = ?"
+        return self._fetchone_query(query, (entry_order_id,))
+    
+    def get_open_trade_cycles(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
+        """Get all open trade cycles, optionally filtered by symbol."""
+        if symbol:
+            query = "SELECT * FROM trade_cycles WHERE status = 'open' AND symbol = ? ORDER BY created_at DESC"
+            return self._fetch_query(query, (symbol,))
+        else:
+            query = "SELECT * FROM trade_cycles WHERE status = 'open' ORDER BY created_at DESC"
+            return self._fetch_query(query)
+    
+    def get_trade_cycles_by_status(self, status: str, limit: int = 50) -> List[Dict[str, Any]]:
+        """Get trade cycles by status."""
+        query = "SELECT * FROM trade_cycles WHERE status = ? ORDER BY updated_at DESC LIMIT ?"
+        return self._fetch_query(query, (status, limit))
+    
+    def get_recent_trade_cycles(self, limit: int = 20) -> List[Dict[str, Any]]:
+        """Get recent trade cycles (all statuses)."""
+        query = "SELECT * FROM trade_cycles ORDER BY updated_at DESC LIMIT ?"
+        return self._fetch_query(query, (limit,))
+    
+    def get_trade_cycle_performance_stats(self) -> Dict[str, Any]:
+        """Get comprehensive trade cycle performance statistics."""
+        try:
+            # Get closed trades for analysis
+            closed_trades = self.get_trade_cycles_by_status('closed', limit=1000)
+            
+            if not closed_trades:
+                return {
+                    'total_closed_trades': 0,
+                    'win_rate': 0,
+                    'avg_win': 0,
+                    'avg_loss': 0,
+                    'profit_factor': 0,
+                    'total_pnl': 0,
+                    'avg_duration_minutes': 0,
+                    'best_trade': 0,
+                    'worst_trade': 0,
+                    'stop_loss_rate': 0,
+                    'take_profit_rate': 0
+                }
+            
+            # Calculate statistics
+            wins = [t for t in closed_trades if t['realized_pnl'] > 0]
+            losses = [t for t in closed_trades if t['realized_pnl'] < 0]
+            
+            total_pnl = sum(t['realized_pnl'] for t in closed_trades)
+            win_rate = (len(wins) / len(closed_trades)) * 100 if closed_trades else 0
+            avg_win = sum(t['realized_pnl'] for t in wins) / len(wins) if wins else 0
+            avg_loss = sum(t['realized_pnl'] for t in losses) / len(losses) if losses else 0
+            profit_factor = abs(avg_win * len(wins) / (avg_loss * len(losses))) if losses and avg_loss != 0 else float('inf')
+            
+            # Duration analysis
+            durations = [t['duration_seconds'] for t in closed_trades if t['duration_seconds']]
+            avg_duration_minutes = (sum(durations) / len(durations) / 60) if durations else 0
+            
+            # Best/worst trades
+            best_trade = max(t['realized_pnl'] for t in closed_trades)
+            worst_trade = min(t['realized_pnl'] for t in closed_trades)
+            
+            # Exit type analysis
+            stop_loss_trades = [t for t in closed_trades if t['exit_type'] == 'stop_loss']
+            take_profit_trades = [t for t in closed_trades if t['exit_type'] == 'take_profit']
+            stop_loss_rate = (len(stop_loss_trades) / len(closed_trades)) * 100 if closed_trades else 0
+            take_profit_rate = (len(take_profit_trades) / len(closed_trades)) * 100 if closed_trades else 0
+            
+            return {
+                'total_closed_trades': len(closed_trades),
+                'win_rate': win_rate,
+                'avg_win': avg_win,
+                'avg_loss': avg_loss,
+                'profit_factor': profit_factor,
+                'total_pnl': total_pnl,
+                'avg_duration_minutes': avg_duration_minutes,
+                'best_trade': best_trade,
+                'worst_trade': worst_trade,
+                'stop_loss_rate': stop_loss_rate,
+                'take_profit_rate': take_profit_rate,
+                'winning_trades': len(wins),
+                'losing_trades': len(losses),
+                'breakeven_trades': len(closed_trades) - len(wins) - len(losses)
+            }
+            
+        except Exception as e:
+            logger.error(f"❌ Error calculating trade cycle performance: {e}")
+            return {}