Procházet zdrojové kódy

Refactor trading logic to unify position tracking and enhance command handling - Updated the trading bot to utilize a unified trades table as the single source of truth for open positions, improving accuracy in position management. Enhanced InfoCommands to provide detailed statistics on active trades and open positions, including improved error handling and user notifications. Updated TradingEngine and TradingStats to support new lifecycle management for trades, streamlining the overall trading process.

Carles Sentis před 4 dny
rodič
revize
3cb4cb089b

+ 0 - 1
src/bot/core.py

@@ -83,7 +83,6 @@ 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))

+ 184 - 68
src/commands/info_commands.py

@@ -106,79 +106,99 @@ class InfoCommands:
             await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
             return
         
-        positions = self.trading_engine.get_positions()
+        # 🧹 PHASE 4: Use unified trades table as the single source of truth
+        stats = self.trading_engine.get_stats()
+        if not stats:
+            await context.bot.send_message(chat_id=chat_id, text="❌ Trading statistics not available.")
+            return
         
-        if positions is not None:  # Successfully fetched (could be empty list)
-            positions_text = "📈 <b>Open Positions</b>\n\n"
-            
-            # Filter for actual open positions
-            open_positions = [p for p in positions if float(p.get('contracts', 0)) != 0]
-            
-            if open_positions:
-                total_unrealized = 0
-                total_position_value = 0
+        # Get open positions from unified trades table
+        open_positions = stats.get_open_positions()
+        
+        positions_text = "📈 <b>Open Positions</b>\n\n"
+        
+        if open_positions:
+            total_unrealized = 0
+            total_position_value = 0
+            
+            for position_trade in open_positions:
+                symbol = position_trade['symbol']
+                token = symbol.split('/')[0] if '/' in symbol else symbol
+                position_side = position_trade['position_side']  # 'long' or 'short'
+                entry_price = position_trade['entry_price']
+                current_amount = position_trade['current_position_size']
+                unrealized_pnl = position_trade.get('unrealized_pnl', 0)
                 
-                for position in open_positions:
-                    symbol = position.get('symbol', '').replace('/USDC:USDC', '')
-                    
-                    # Use the new position direction logic
-                    position_type, exit_side, contracts = self.trading_engine.get_position_direction(position)
-                    
-                    # Use correct CCXT field names
-                    entry_price = float(position.get('entryPrice', 0))
-                    mark_price = float(position.get('markPrice') or 0)
-                    unrealized_pnl = float(position.get('unrealizedPnl', 0))
-                    
-                    # If markPrice is not available, try to get current market price
-                    if mark_price == 0:
-                        try:
-                            market_data = self.trading_engine.get_market_data(position.get('symbol', ''))
-                            if market_data and market_data.get('ticker'):
-                                mark_price = float(market_data['ticker'].get('last', entry_price))
-                        except:
-                            mark_price = entry_price  # Fallback to entry price
-                    
-                    # Calculate position value
-                    position_value = abs(contracts) * mark_price
-                    total_position_value += position_value
-                    total_unrealized += unrealized_pnl
-                    
-                    # Position emoji and formatting
-                    if position_type == "LONG":
-                        pos_emoji = "🟢"
-                        direction = "LONG"
-                    else:
-                        pos_emoji = "🔴"
-                        direction = "SHORT"
-                    
-                    pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-                    pnl_percent = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
-                    
-                    # Format prices with proper precision for this token
-                    formatter = get_formatter()
-                    entry_price_str = formatter.format_price_with_symbol(entry_price, symbol)
-                    mark_price_str = formatter.format_price_with_symbol(mark_price, symbol)
-                    
-                    positions_text += f"{pos_emoji} <b>{symbol} ({direction})</b>\n"
-                    positions_text += f"   📏 Size: {abs(contracts):.6f} {symbol}\n"
-                    positions_text += f"   💰 Entry: {entry_price_str}\n"
-                    positions_text += f"   📊 Mark: {mark_price_str}\n"
-                    positions_text += f"   💵 Value: ${position_value:,.2f}\n"
-                    positions_text += f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percent:+.2f}%)\n\n"
+                # Get current market price
+                mark_price = entry_price  # Fallback
+                try:
+                    market_data = self.trading_engine.get_market_data(symbol)
+                    if market_data and market_data.get('ticker'):
+                        mark_price = float(market_data['ticker'].get('last', entry_price))
+                        
+                        # Update unrealized PnL with current price
+                        if position_side == 'long':
+                            unrealized_pnl = current_amount * (mark_price - entry_price)
+                        else:  # Short position
+                            unrealized_pnl = current_amount * (entry_price - mark_price)
+                except:
+                    pass  # Use entry price as fallback
                 
-                # Portfolio summary
-                portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
-                positions_text += f"💼 <b>Total Portfolio:</b>\n"
-                positions_text += f"   💵 Total Value: ${total_position_value:,.2f}\n"
-                positions_text += f"   {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n"
+                # Calculate position value
+                position_value = abs(current_amount) * mark_price
+                total_position_value += position_value
+                total_unrealized += unrealized_pnl
                 
-            else:
-                positions_text += "📭 No open positions\n\n"
-                positions_text += "💡 Use /long or /short to open a position"
+                # Position emoji and formatting
+                if position_side == 'long':
+                    pos_emoji = "🟢"
+                    direction = "LONG"
+                else:  # Short position
+                    pos_emoji = "🔴"
+                    direction = "SHORT"
+                
+                pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
+                pnl_percent = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
+                
+                # Format prices with proper precision for this token
+                formatter = get_formatter()
+                entry_price_str = formatter.format_price_with_symbol(entry_price, token)
+                mark_price_str = formatter.format_price_with_symbol(mark_price, token)
+                
+                positions_text += f"{pos_emoji} <b>{token} ({direction})</b>\n"
+                positions_text += f"   📏 Size: {abs(current_amount):.6f} {token}\n"
+                positions_text += f"   💰 Entry: {entry_price_str}\n"
+                positions_text += f"   📊 Mark: {mark_price_str}\n"
+                positions_text += f"   💵 Value: ${position_value:,.2f}\n"
+                positions_text += f"   {pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percent:+.2f}%)\n"
+                
+                # Show stop loss if linked
+                if position_trade.get('stop_loss_price'):
+                    sl_price = position_trade['stop_loss_price']
+                    sl_status = "Pending" if not position_trade.get('stop_loss_order_id') else "Active"
+                    positions_text += f"   🛑 Stop Loss: {formatter.format_price_with_symbol(sl_price, token)} ({sl_status})\n"
+                
+                # Show take profit if linked
+                if position_trade.get('take_profit_price'):
+                    tp_price = position_trade['take_profit_price']
+                    tp_status = "Pending" if not position_trade.get('take_profit_order_id') else "Active"
+                    positions_text += f"   🎯 Take Profit: {formatter.format_price_with_symbol(tp_price, token)} ({tp_status})\n"
+                
+                positions_text += f"   🆔 Lifecycle ID: {position_trade['trade_lifecycle_id'][:8]}\n\n"
+            
+            # Portfolio summary
+            portfolio_emoji = "🟢" if total_unrealized >= 0 else "🔴"
+            positions_text += f"💼 <b>Total Portfolio:</b>\n"
+            positions_text += f"   💵 Total Value: ${total_position_value:,.2f}\n"
+            positions_text += f"   {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
+            positions_text += f"🆕 <b>Source:</b> Unified Trades Table (Phase 4)\n"
+            positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
             
-            await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
         else:
-            await context.bot.send_message(chat_id=chat_id, text="❌ Could not fetch positions")
+            positions_text += "📭 No open positions\n\n"
+            positions_text += "💡 Use /long or /short to open a position"
+        
+        await context.bot.send_message(chat_id=chat_id, text=positions_text, parse_mode='HTML')
     
     async def orders_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /orders command."""
@@ -422,8 +442,104 @@ class InfoCommands:
             await update.message.reply_text(message, parse_mode='HTML')
             
         except Exception as e:
+            error_message = f"❌ Error processing cycles command: {str(e)}"
+            await context.bot.send_message(chat_id=chat_id, text=error_message)
             logger.error(f"Error in cycles command: {e}")
-            await update.message.reply_text("❌ Error retrieving trade cycle data.", parse_mode='HTML')
+    
+    async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+        """Handle the /active command to show active trades (Phase 1 testing)."""
+        chat_id = update.effective_chat.id
+        if not self._is_authorized(chat_id):
+            await context.bot.send_message(chat_id=chat_id, text="❌ Unauthorized access.")
+            return
+            
+        try:
+            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
+            
+            # Get all active trades
+            all_active_trades = stats.get_all_active_trades()
+            
+            if not all_active_trades:
+                await context.bot.send_message(
+                    chat_id=chat_id, 
+                    text="📊 <b>Active Trades (Phase 1)</b>\n\n📭 No active trades found.",
+                    parse_mode='HTML'
+                )
+                return
+            
+            # Group by status
+            active_trades_by_status = {}
+            for trade in all_active_trades:
+                status = trade['status']
+                if status not in active_trades_by_status:
+                    active_trades_by_status[status] = []
+                active_trades_by_status[status].append(trade)
+            
+            message_text = "📊 <b>Active Trades (Phase 1)</b>\n\n"
+            
+            # Show each status group
+            for status, trades in active_trades_by_status.items():
+                status_emoji = {
+                    'pending': '⏳',
+                    'active': '🟢', 
+                    'closed': '✅',
+                    'cancelled': '❌'
+                }.get(status, '📊')
+                
+                message_text += f"{status_emoji} <b>{status.upper()}</b> ({len(trades)} trades):\n"
+                
+                for trade in trades[:5]:  # Limit to 5 per status to avoid long messages
+                    symbol = trade['symbol']
+                    token = symbol.split('/')[0] if '/' in symbol else symbol
+                    side = trade['side'].upper()
+                    entry_price = trade.get('entry_price')
+                    entry_amount = trade.get('entry_amount')
+                    realized_pnl = trade.get('realized_pnl', 0)
+                    
+                    message_text += f"  • {side} {token}"
+                    
+                    if entry_price and entry_amount:
+                        message_text += f" | {entry_amount:.6f} @ ${entry_price:.2f}"
+                        
+                    if status == 'closed' and realized_pnl != 0:
+                        pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                        message_text += f" | {pnl_emoji} ${realized_pnl:.2f}"
+                        
+                    if trade.get('stop_loss_price'):
+                        message_text += f" | SL: ${trade['stop_loss_price']:.2f}"
+                        
+                    message_text += "\n"
+                
+                if len(trades) > 5:
+                    message_text += f"  ... and {len(trades) - 5} more\n"
+                    
+                message_text += "\n"
+            
+            # Add summary
+            total_trades = len(all_active_trades)
+            pending_count = len(active_trades_by_status.get('pending', []))
+            active_count = len(active_trades_by_status.get('active', []))
+            closed_count = len(active_trades_by_status.get('closed', []))
+            cancelled_count = len(active_trades_by_status.get('cancelled', []))
+            
+            message_text += f"📈 <b>Summary:</b>\n"
+            message_text += f"  Total: {total_trades} | "
+            message_text += f"Pending: {pending_count} | "
+            message_text += f"Active: {active_count} | "
+            message_text += f"Closed: {closed_count} | "
+            message_text += f"Cancelled: {cancelled_count}\n\n"
+            
+            message_text += f"💡 This is Phase 1 testing - active trades run parallel to trade cycles"
+            
+            await context.bot.send_message(chat_id=chat_id, text=message_text.strip(), parse_mode='HTML')
+            
+        except Exception as e:
+            error_message = f"❌ Error processing active trades command: {str(e)}"
+            await context.bot.send_message(chat_id=chat_id, text=error_message)
+            logger.error(f"Error in active trades command: {e}")
     
     async def market_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /market command."""

+ 234 - 51
src/monitoring/market_monitor.py

@@ -156,6 +156,9 @@ class MarketMonitor:
         try:
             loop_count = 0
             while self._monitoring_active:
+                # 🆕 PHASE 2: Check active trades for pending stop loss activation first (highest priority)
+                await self._activate_pending_stop_losses_from_active_trades()
+                
                 await self._check_order_fills()
                 await self._check_price_alarms()
                 await self._check_external_trades()
@@ -524,36 +527,33 @@ class MarketMonitor:
                         
                         # 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_stop_loss",
-                                timestamp=timestamp_dt.isoformat(),
-                                linked_order_table_id_to_link=None  # External stop losses don't link to bot orders
-                            )
-                            
-                            # 🆕 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
+                            # 🧹 PHASE 3: Close active trade for stop loss execution
+                            active_trade = stats.get_active_trade_by_symbol(full_symbol, status='active')
+                            if active_trade:
+                                entry_price = active_trade.get('entry_price', 0)
+                                active_trade_side = active_trade.get('side')
+                                
+                                # Calculate realized P&L
+                                if active_trade_side == 'buy':  # Long position
+                                    realized_pnl = amount * (price - entry_price)
+                                else:  # Short position  
+                                    realized_pnl = amount * (entry_price - price)
+                                
+                                stats.update_active_trade_closed(
+                                    active_trade['id'], realized_pnl, timestamp_dt.isoformat()
+                                )
+                                logger.info(f"🛑 Active trade {active_trade['id']} closed via external stop loss - P&L: ${realized_pnl:.2f}")
                             
                             # Send specialized stop loss execution notification
                             if self.notification_manager:
                                 await self.notification_manager.send_stop_loss_execution_notification(
-                                    stop_loss_info, full_symbol, side, amount, price, action_type, timestamp_dt.isoformat()
+                                    stop_loss_info, full_symbol, side, amount, price, 'long_closed', timestamp_dt.isoformat()
                                 )
                             
                             # Remove from tracking since it's now executed
                             del self._external_stop_loss_orders[exchange_order_id_from_fill]
                             
-                            logger.info(f"🛑 Processed external stop loss execution: {side} {amount} {full_symbol} @ ${price:.2f} ({action_type})")
+                            logger.info(f"🛑 Processed external stop loss execution: {side} {amount} {full_symbol} @ ${price:.2f} (long_closed)")
                         
                         else:
                             # Handle as regular external trade
@@ -585,48 +585,112 @@ class MarketMonitor:
                                         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(
+                                # 🧹 PHASE 3: Record trade simply, use active_trades for tracking
+                                stats.record_trade(
                                     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
+                                # Derive action type from trade context for notifications
                                 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(
+                                    # Bot order - determine action from order context
+                                    order_side = order_in_db.get('side', side).lower()
+                                    if order_side == 'buy':
+                                        action_type = 'long_opened'
+                                    elif order_side == 'sell':
+                                        action_type = 'short_opened'
+                                    else:
+                                        action_type = 'position_opened'
+                                else:
+                                    # External trade - determine from current active trades
+                                    existing_active_trade = stats.get_active_trade_by_symbol(full_symbol, status='active')
+                                    if existing_active_trade:
+                                        # Has active position - this is likely a closure
+                                        existing_side = existing_active_trade.get('side')
+                                        if existing_side == 'buy' and side.lower() == 'sell':
+                                            action_type = 'long_closed'
+                                        elif existing_side == 'sell' and side.lower() == 'buy':
+                                            action_type = 'short_closed'
+                                        else:
+                                            action_type = 'position_modified'
+                                    else:
+                                        # No active position - this opens a new one
+                                        if side.lower() == 'buy':
+                                            action_type = 'long_opened'
+                                        else:
+                                            action_type = 'short_opened'
+                                
+                                # 🧹 PHASE 3: Update active trades based on action
+                                if linked_order_db_id:
+                                    # Bot order - update linked active trade
+                                    order_data = stats.get_order_by_db_id(linked_order_db_id)
+                                    if order_data:
+                                        exchange_order_id = order_data.get('exchange_order_id')
+                                        
+                                        # Find active trade by entry order ID
+                                        all_active_trades = stats.get_all_active_trades()
+                                        for at in all_active_trades:
+                                            if at.get('entry_order_id') == exchange_order_id:
+                                                active_trade_id = at['id']
+                                                current_status = at['status']
+                                                
+                                                if current_status == 'pending' and action_type in ['long_opened', 'short_opened']:
+                                                    # Entry order filled - update active trade to active
+                                                    stats.update_active_trade_opened(
+                                                        active_trade_id, price, amount, timestamp_dt.isoformat()
+                                                    )
+                                                    logger.info(f"🆕 Active trade {active_trade_id} opened via fill {trade_id}")
+                                                    
+                                                elif current_status == 'active' and action_type in ['long_closed', 'short_closed']:
+                                                    # Exit order filled - calculate P&L and close active trade
+                                                    entry_price = at.get('entry_price', 0)
+                                                    active_trade_side = at.get('side')
+                                                    
+                                                    # Calculate realized P&L
+                                                    if active_trade_side == 'buy':  # Long position
+                                                        realized_pnl = amount * (price - entry_price)
+                                                    else:  # Short position  
+                                                        realized_pnl = amount * (entry_price - price)
+                                                    
+                                                    stats.update_active_trade_closed(
+                                                        active_trade_id, realized_pnl, timestamp_dt.isoformat()
+                                                    )
+                                                    logger.info(f"🆕 Active trade {active_trade_id} closed via fill {trade_id} - P&L: ${realized_pnl:.2f}")
+                                                break
+                                
+                                elif action_type in ['long_opened', 'short_opened']:
+                                    # External trade that opened a position - create external active trade
+                                    active_trade_id = stats.create_active_trade(
                                         symbol=full_symbol,
-                                        side=side_for_cycle,
+                                        side=side.lower(),
                                         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()
+                                    if active_trade_id:
+                                        stats.update_active_trade_opened(
+                                            active_trade_id, price, amount, timestamp_dt.isoformat()
                                         )
-                                        logger.info(f"📊 Created external trade cycle {trade_cycle_id} for {side.upper()} {full_symbol}")
+                                        logger.info(f"🆕 Created external active trade {active_trade_id} for {side.upper()} {full_symbol}")
+                                
+                                elif action_type in ['long_closed', 'short_closed']:
+                                    # External closure - close any active trade for this symbol
+                                    active_trade = stats.get_active_trade_by_symbol(full_symbol, status='active')
+                                    if active_trade:
+                                        entry_price = active_trade.get('entry_price', 0)
+                                        active_trade_side = active_trade.get('side')
+                                        
+                                        # Calculate realized P&L
+                                        if active_trade_side == 'buy':  # Long position
+                                            realized_pnl = amount * (price - entry_price)
+                                        else:  # Short position  
+                                            realized_pnl = amount * (entry_price - price)
+                                        
+                                        stats.update_active_trade_closed(
+                                            active_trade['id'], realized_pnl, timestamp_dt.isoformat()
+                                        )
+                                        logger.info(f"🆕 External closure: Active trade {active_trade['id']} closed - P&L: ${realized_pnl:.2f}")
                                 
                                 # Track symbol for potential stop loss activation
                                 symbols_with_fills.add(token)
@@ -969,6 +1033,125 @@ class MarketMonitor:
         except Exception as e:
             logger.error(f"❌ Error cleaning up orphaned stop losses: {e}", exc_info=True)
 
+    async def _activate_pending_stop_losses_from_trades(self):
+        """🆕 PHASE 4: Check trades table for pending stop loss activation first (highest priority)"""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                return
+            
+            # Get open positions that need stop loss activation
+            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 '/' in symbol else symbol
+                    stop_loss_price = position_trade['stop_loss_price']
+                    position_side = position_trade['position_side']
+                    current_amount = position_trade.get('current_position_size', 0)
+                    lifecycle_id = position_trade['trade_lifecycle_id']
+                    
+                    # Get current market price
+                    current_price = None
+                    try:
+                        market_data = self.trading_engine.get_market_data(symbol)
+                        if market_data and market_data.get('ticker'):
+                            current_price = float(market_data['ticker'].get('last', 0))
+                    except Exception as price_error:
+                        logger.warning(f"Could not fetch current price for {symbol}: {price_error}")
+                    
+                    # Determine stop loss side based on position side
+                    sl_side = 'sell' if position_side == 'long' else 'buy'  # Long SL = sell, Short SL = buy
+                    
+                    # Check if trigger condition is already met
+                    trigger_already_hit = False
+                    trigger_reason = ""
+                    
+                    if current_price and current_price > 0:
+                        if sl_side == 'sell' and current_price <= stop_loss_price:
+                            # LONG position stop loss - price below trigger
+                            trigger_already_hit = True
+                            trigger_reason = f"LONG SL: Current ${current_price:.4f} ≤ Stop ${stop_loss_price:.4f}"
+                        elif sl_side == 'buy' and current_price >= stop_loss_price:
+                            # SHORT position stop loss - price above trigger  
+                            trigger_already_hit = True
+                            trigger_reason = f"SHORT SL: Current ${current_price:.4f} ≥ Stop ${stop_loss_price:.4f}"
+                    
+                    if trigger_already_hit:
+                        # Execute immediate market close
+                        logger.warning(f"🚨 IMMEDIATE SL EXECUTION (Trades Table): {token} - {trigger_reason}")
+                        
+                        try:
+                            exit_result = await self.trading_engine.execute_exit_order(token)
+                            
+                            if exit_result.get('success'):
+                                logger.info(f"✅ Immediate {position_side.upper()} SL execution successful for {token}")
+                                
+                                if self.notification_manager:
+                                    await self.notification_manager.send_generic_notification(
+                                        f"🚨 <b>Immediate Stop Loss Execution</b>\n\n"
+                                        f"🆕 <b>Source: Unified Trades Table (Phase 4)</b>\n"
+                                        f"Token: {token}\n"
+                                        f"Position Type: {position_side.upper()}\n"
+                                        f"SL Trigger Price: ${stop_loss_price:.4f}\n"
+                                        f"Current Market Price: ${current_price:.4f}\n"
+                                        f"Trigger Logic: {trigger_reason}\n"
+                                        f"Action: Market close order placed immediately\n"
+                                        f"Order ID: {exit_result.get('order_placed_details', {}).get('exchange_order_id', 'N/A')}\n"
+                                        f"Lifecycle ID: {lifecycle_id[:8]}\n"
+                                        f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
+                                        f"⚡ Single source of truth prevents missed stop losses"
+                                    )
+                            else:
+                                logger.error(f"❌ Failed to execute immediate SL for {token}: {exit_result.get('error')}")
+                        
+                        except Exception as exec_error:
+                            logger.error(f"❌ Exception during immediate SL execution for {token}: {exec_error}")
+                    
+                    else:
+                        # Normal activation - place stop loss order
+                        try:
+                            sl_result = await self.trading_engine.execute_stop_loss_order(token, stop_loss_price)
+                            
+                            if sl_result.get('success'):
+                                sl_order_id = sl_result.get('order_placed_details', {}).get('exchange_order_id')
+                                
+                                # Link the stop loss order to the trade lifecycle
+                                stats.link_stop_loss_to_trade(lifecycle_id, sl_order_id, stop_loss_price)
+                                
+                                logger.info(f"✅ Activated {position_side.upper()} stop loss for {token}: ${stop_loss_price:.4f}")
+                                
+                                if self.notification_manager:
+                                    await self.notification_manager.send_generic_notification(
+                                        f"🛑 <b>Stop Loss Activated</b>\n\n"
+                                        f"🆕 <b>Source: Unified Trades Table (Phase 4)</b>\n"
+                                        f"Token: {token}\n"
+                                        f"Position Type: {position_side.upper()}\n"
+                                        f"Stop Loss Price: ${stop_loss_price:.4f}\n"
+                                        f"Current Price: ${current_price:.4f if current_price else 'Unknown'}\n"
+                                        f"Order ID: {sl_order_id or 'N/A'}\n"
+                                        f"Lifecycle ID: {lifecycle_id[:8]}\n"
+                                        f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
+                                        f"🛡️ Your position is now protected"
+                                    )
+                            else:
+                                logger.error(f"❌ Failed to activate SL for {token}: {sl_result.get('error')}")
+                        
+                        except Exception as activation_error:
+                            logger.error(f"❌ Exception during SL activation for {token}: {activation_error}")
+                
+                except Exception as trade_error:
+                    logger.error(f"❌ Error processing position trade for SL activation: {trade_error}")
+            
+        except Exception as e:
+            logger.error(f"❌ Error activating pending stop losses from trades table: {e}", exc_info=True)
+
     async def _activate_pending_stop_losses(self, order_in_db, stats):
         """Activate pending stop losses for a filled order, checking current price for immediate execution."""
         try:

+ 56 - 26
src/trading/trading_engine.py

@@ -72,13 +72,43 @@ class TradingEngine:
     def find_position(self, token: str) -> Optional[Dict[str, Any]]:
         """Find an open position for a token."""
         symbol = f"{token}/USDC:USDC"
-        positions = self.get_positions()
         
-        if positions:
-            for position in positions:
-                if (position.get('symbol') == symbol and 
-                    float(position.get('contracts', 0)) != 0):
-                    return position
+        # 🆕 PHASE 4: Check trades table for open positions (single source of truth)
+        if self.stats:
+            open_trade = self.stats.get_trade_by_symbol_and_status(symbol, status='position_opened')
+            if open_trade:
+                # Convert trades format to position format for compatibility
+                entry_price = open_trade.get('entry_price')
+                current_amount = open_trade.get('current_position_size', 0)
+                position_side = open_trade.get('position_side')
+                
+                if entry_price and current_amount and abs(current_amount) > 0:
+                    return {
+                        'symbol': symbol,
+                        'contracts': abs(current_amount),
+                        'notional': abs(current_amount),
+                        'side': 'long' if position_side == 'long' else 'short',
+                        'size': current_amount,  # Can be negative for short
+                        'entryPx': entry_price,
+                        'unrealizedPnl': open_trade.get('unrealized_pnl', 0),
+                        'marginUsed': abs(current_amount * entry_price),
+                        # Add lifecycle info for debugging
+                        '_lifecycle_id': open_trade.get('trade_lifecycle_id'),
+                        '_trade_id': open_trade.get('id'),
+                        '_source': 'trades_table_phase4'
+                    }
+        
+        # 🔄 Fallback: Check exchange position data
+        try:
+            positions = self.client.get_positions()
+            if positions:
+                for pos in positions:
+                    if pos.get('symbol') == symbol and pos.get('contracts', 0) != 0:
+                        logger.debug(f"Found exchange position for {token}: {pos}")
+                        return pos
+        except Exception as e:
+            logger.warning(f"Could not fetch exchange positions: {e}")
+        
         return None
     
     def get_position_direction(self, position: Dict[str, Any]) -> Tuple[str, str, float]:
@@ -210,28 +240,28 @@ 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)
+            # 🆕 PHASE 4: Create trade lifecycle for this entry order
             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(
+                    lifecycle_id = self.stats.create_trade_lifecycle(
                         symbol=symbol,
                         side='buy',
-                        entry_order_id=entry_order_record['id'],
+                        entry_order_id=exchange_oid,  # Store exchange order ID
                         stop_loss_price=stop_loss_price,
                         trade_type='bot'
                     )
                     
-                    if trade_cycle_id and stop_loss_price:
+                    if lifecycle_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}")
+                            self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_loss_price)
+                            logger.info(f"📊 Created trade lifecycle {lifecycle_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}")
+                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol}")
+                    elif lifecycle_id:
+                        logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol}")
             
             return {
                 "success": True,
@@ -356,28 +386,28 @@ 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)
+            # 🆕 PHASE 4: Create trade lifecycle for this entry order
             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(
+                    lifecycle_id = self.stats.create_trade_lifecycle(
                         symbol=symbol,
                         side='sell',
-                        entry_order_id=entry_order_record['id'],
+                        entry_order_id=exchange_oid,  # Store exchange order ID
                         stop_loss_price=stop_loss_price,
                         trade_type='bot'
                     )
                     
-                    if trade_cycle_id and stop_loss_price:
+                    if lifecycle_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}")
+                            self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_loss_price)
+                            logger.info(f"📊 Created trade lifecycle {lifecycle_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}")
+                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol}")
+                    elif lifecycle_id:
+                        logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol}")
             
             return {
                 "success": True,
@@ -500,7 +530,7 @@ class TradingEngine:
         try:
             symbol = f"{token}/USDC:USDC"
             position_type, exit_side, contracts = self.get_position_direction(position)
-            entry_price = float(position.get('entryPrice', 0))
+            entry_price = float(position.get('entryPx', 0))
             
             # Validate stop loss price based on position direction
             if position_type == "LONG" and stop_price >= entry_price:
@@ -579,7 +609,7 @@ class TradingEngine:
         try:
             symbol = f"{token}/USDC:USDC"
             position_type, exit_side, contracts = self.get_position_direction(position)
-            entry_price = float(position.get('entryPrice', 0))
+            entry_price = float(position.get('entryPx', 0))
             
             # Validate take profit price based on position direction
             if position_type == "LONG" and profit_price <= entry_price:

+ 209 - 388
src/trading/trading_stats.py

@@ -13,6 +13,7 @@ from typing import Dict, List, Any, Optional, Tuple, Union
 import numpy as np
 import math
 from collections import defaultdict
+import uuid
 
 logger = logging.getLogger(__name__)
 
@@ -80,19 +81,37 @@ class TradingStats:
                 value REAL NOT NULL,
                 trade_type TEXT NOT NULL,
                 pnl REAL DEFAULT 0.0,
-                linked_order_table_id INTEGER
-            )
-            """,
-            # pnl on trades table is for individual realized pnl if a trade closes a part of a position.
-            # Overall PNL is derived from cycles or balance changes.
-            """
-            CREATE TABLE IF NOT EXISTS enhanced_positions (
-                symbol TEXT PRIMARY KEY,
-                contracts REAL NOT NULL,
-                avg_entry_price REAL NOT NULL,
-                total_cost_basis REAL NOT NULL,
-                entry_count INTEGER NOT NULL,
-                last_entry_timestamp TEXT
+                linked_order_table_id INTEGER,
+                
+                -- 🆕 PHASE 4: Lifecycle tracking fields (merged from active_trades)
+                status TEXT DEFAULT 'executed', -- 'pending', 'executed', 'position_opened', 'position_closed', 'cancelled'
+                trade_lifecycle_id TEXT, -- Groups related trades into one lifecycle
+                position_side TEXT, -- 'long', 'short', 'flat' - the resulting position side
+                
+                -- Position tracking
+                entry_price REAL,
+                current_position_size REAL DEFAULT 0,
+                
+                -- Order IDs (exchange IDs)
+                entry_order_id TEXT,
+                stop_loss_order_id TEXT,
+                take_profit_order_id TEXT,
+                
+                -- Risk management
+                stop_loss_price REAL,
+                take_profit_price REAL,
+                
+                -- P&L tracking
+                realized_pnl REAL DEFAULT 0,
+                unrealized_pnl REAL DEFAULT 0,
+                
+                -- Timestamps
+                position_opened_at TEXT,
+                position_closed_at TEXT,
+                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
+                
+                -- Notes
+                notes TEXT
             )
             """,
             """
@@ -148,50 +167,16 @@ 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)
-            )
+            CREATE INDEX IF NOT EXISTS idx_trades_status ON trades (status);
+            """,
+            """
+            CREATE INDEX IF NOT EXISTS idx_trades_lifecycle_id ON trades (trade_lifecycle_id);
+            """,
+            """
+            CREATE INDEX IF NOT EXISTS idx_trades_position_side ON trades (position_side);
+            """,
+            """
+            CREATE INDEX IF NOT EXISTS idx_trades_symbol_status ON trades (symbol, status);
             """
         ]
         for query in queries:
@@ -255,161 +240,16 @@ class TradingStats:
                      exchange_fill_id: Optional[str] = None, trade_type: str = "manual",
                      pnl: Optional[float] = None, timestamp: Optional[str] = None,
                      linked_order_table_id_to_link: Optional[int] = None):
-        """Record a trade (fill) in the database, including optional PNL, specific timestamp, and link to an orders table entry."""
-        ts = timestamp if timestamp else datetime.now(timezone.utc).isoformat()
-        value = amount * price
-        
-        db_pnl = pnl if pnl is not None else 0.0
-
-        query = """
-            INSERT INTO trades (exchange_fill_id, timestamp, symbol, side, amount, price, value, trade_type, pnl, linked_order_table_id)
-            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-        """
-        params = (exchange_fill_id, ts, symbol, side.lower(), amount, price, value, trade_type, db_pnl, linked_order_table_id_to_link)
-        
-        try:
-            self._execute_query(query, params)
-            logger.info(f"Recorded trade: {side.upper()} {amount} {symbol} @ ${price:.2f} (Fill ID: {exchange_fill_id or 'N/A'}, PNL: ${db_pnl:.2f}, Linked Order ID: {linked_order_table_id_to_link or 'N/A'})")
-        except sqlite3.IntegrityError as e: 
-             logger.warning(f"Failed to record trade due to IntegrityError (likely duplicate exchange_fill_id {exchange_fill_id}): {e}")
-
-
-    def get_enhanced_position_state(self, symbol: str) -> Optional[Dict[str, Any]]:
-        """Get current enhanced position state for a symbol from DB."""
-        query = "SELECT * FROM enhanced_positions WHERE symbol = ?"
-        return self._fetchone_query(query, (symbol,))
-
-    def update_enhanced_position_state(self, symbol: str, side: str, amount: float, price: float, 
-                                     timestamp: Optional[str] = None) -> Tuple[str, float]:
-        """Update enhanced position state with a new trade and return action type and realized PNL for this trade."""
+        """Record a trade in the database."""
         if timestamp is None:
             timestamp = datetime.now(timezone.utc).isoformat()
-            
-        position = self.get_enhanced_position_state(symbol)
-        action_type = "unknown"
-        realized_pnl_for_this_trade = 0.0
-
-        current_contracts = position['contracts'] if position else 0.0
-        current_avg_entry = position['avg_entry_price'] if position else 0.0
-        current_cost_basis = position['total_cost_basis'] if position else 0.0
-        current_entry_count = position['entry_count'] if position else 0
-        
-        new_contracts = current_contracts
-        new_avg_entry = current_avg_entry
-        new_cost_basis = current_cost_basis
-        new_entry_count = current_entry_count
-
-        if side.lower() == 'buy':
-            if current_contracts >= 0: # Opening/adding to long
-                action_type = 'long_opened' if current_contracts == 0 else 'long_increased'
-                new_cost_basis += amount * price
-                new_contracts += amount
-                new_avg_entry = new_cost_basis / new_contracts if new_contracts > 0 else 0
-                new_entry_count += 1
-            else: # Reducing short position
-                reduction = min(amount, abs(current_contracts))
-                realized_pnl_for_this_trade = reduction * (current_avg_entry - price) # PNL from short covering
-                
-                new_contracts += reduction
-                # Cost basis for shorts is tricky; avg_entry_price is more key for shorts.
-                # For now, let's assume cost_basis is not directly managed for pure shorts in this way.
-                # The avg_entry_price of the short remains.
-                
-                if new_contracts == 0: # Short fully closed
-                    action_type = 'short_closed'
-                    self._reset_enhanced_position_state(symbol) # Clears the row
-                    logger.info(f"📉 Enhanced position reset (closed): {symbol}. Realized PNL from this reduction: ${realized_pnl_for_this_trade:.2f}")
-                    return action_type, realized_pnl_for_this_trade
-                elif new_contracts > 0: # Flipped to long
-                    action_type = 'short_closed_and_long_opened'
-                    # Reset for the new long position part
-                    new_cost_basis = new_contracts * price # Cost basis for the new long part
-                    new_avg_entry = price
-                    new_entry_count = 1
-                    logger.info(f"⚖️ Enhanced position flipped SHORT to LONG: {symbol}. Realized PNL from short part: ${realized_pnl_for_this_trade:.2f}")
-                else: # Short reduced
-                    action_type = 'short_reduced'
         
-        elif side.lower() == 'sell':
-            if current_contracts <= 0: # Opening/adding to short
-                action_type = 'short_opened' if current_contracts == 0 else 'short_increased'
-                # For shorts, avg_entry_price tracks the average price we sold at to open/increase the short.
-                # total_cost_basis is less intuitive for shorts.
-                # We calculate new_avg_entry if adding to short:
-                # (current_abs_contracts * current_avg_entry + amount * price) / (current_abs_contracts + amount)
-                if current_contracts < 0 : # Adding to existing short
-                    total_sell_value = abs(current_contracts) * current_avg_entry + amount * price
-                    new_contracts -= amount
-                    new_avg_entry = total_sell_value / abs(new_contracts) if new_contracts != 0 else 0
-                else: # Opening new short
-                    new_contracts -= amount
-                    new_avg_entry = price
-                new_entry_count +=1
-
-            else: # Reducing long position
-                reduction = min(amount, current_contracts)
-                realized_pnl_for_this_trade = reduction * (price - current_avg_entry) # PNL from long selling
-                
-                new_contracts -= reduction
-                if new_contracts > 0: # Long reduced
-                    action_type = 'long_reduced'
-                    # Adjust cost basis proportionally: (new_contracts / old_contracts) * old_cost_basis
-                    # Or, simpler: new_cost_basis -= reduction * current_avg_entry
-                    new_cost_basis -= reduction * current_avg_entry 
-                    # Avg entry price of remaining long doesn't change
-                else: # Long position fully closed or flipped
-                    if new_contracts == 0: # Long fully closed
-                        action_type = 'long_closed'
-                        self._reset_enhanced_position_state(symbol)
-                        logger.info(f"📈 Enhanced position reset (closed): {symbol}. Realized PNL from this reduction: ${realized_pnl_for_this_trade:.2f}")
-                        return action_type, realized_pnl_for_this_trade
-                    else: # Flipped to short
-                        action_type = 'long_closed_and_short_opened'
-                        # Reset for new short part
-                        new_avg_entry = price # Avg price of this opening short leg
-                        new_entry_count = 1
-                        new_cost_basis = 0 # Not directly applicable for short open
-                        logger.info(f"⚖️ Enhanced position flipped LONG to SHORT: {symbol}. Realized PNL from long part: ${realized_pnl_for_this_trade:.2f}")
-        
-        # Save updated state to DB
-        upsert_query = """
-            INSERT INTO enhanced_positions (symbol, contracts, avg_entry_price, total_cost_basis, entry_count, last_entry_timestamp)
-            VALUES (?, ?, ?, ?, ?, ?)
-            ON CONFLICT(symbol) DO UPDATE SET
-                contracts = excluded.contracts,
-                avg_entry_price = excluded.avg_entry_price,
-                total_cost_basis = excluded.total_cost_basis,
-                entry_count = excluded.entry_count,
-                last_entry_timestamp = excluded.last_entry_timestamp
-        """
-        self._execute_query(upsert_query, (symbol, new_contracts, new_avg_entry, new_cost_basis, new_entry_count, timestamp))
-        
-        side_log = "LONG" if new_contracts > 0 else "SHORT" if new_contracts < 0 else "FLAT"
-        if new_contracts != 0:
-            logger.info(f"📊 Enhanced position ({action_type}): {symbol} {side_log} {abs(new_contracts):.6f} @ avg ${(new_avg_entry if new_avg_entry else 0.0):.2f}")
-        
-        return action_type, realized_pnl_for_this_trade
-
-    def _reset_enhanced_position_state(self, symbol: str):
-        """Reset enhanced position state when position is fully closed by deleting from DB."""
-        self._execute_query("DELETE FROM enhanced_positions WHERE symbol = ?", (symbol,))
-
-    def record_trade_with_enhanced_tracking(self, symbol: str, side: str, amount: float, price: float,
-                                          exchange_fill_id: Optional[str] = None, trade_type: str = "manual", 
-                                          timestamp: Optional[str] = None,
-                                          linked_order_table_id_to_link: Optional[int] = None) -> str:
-        """Record a trade and update enhanced position tracking. 
-           The linked_order_table_id_to_link should be passed if this fill corresponds to a known order in the 'orders' table.
-        """
-        trade_timestamp_to_use = timestamp if timestamp else datetime.now(timezone.utc).isoformat()
-        
-        action_type, realized_pnl = self.update_enhanced_position_state(symbol, side, amount, price, timestamp=trade_timestamp_to_use)
-
-        self.record_trade(symbol, side, amount, price, exchange_fill_id, trade_type, 
-                          pnl=realized_pnl, timestamp=trade_timestamp_to_use, 
-                          linked_order_table_id_to_link=linked_order_table_id_to_link)
-        
-        return action_type
+        value = amount * price
+        self._execute_query(
+            "INSERT INTO trades (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl, linked_order_table_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            (symbol, side, amount, price, value, trade_type, timestamp, exchange_fill_id, pnl or 0.0, linked_order_table_id_to_link)
+        )
+        logger.info(f"📈 Trade recorded: {side.upper()} {amount:.6f} {symbol} @ ${price:.2f} (${value:.2f}) [{trade_type}]")
 
     def get_all_trades(self) -> List[Dict[str, Any]]:
         """Fetch all trades from the database, ordered by timestamp."""
@@ -742,8 +582,8 @@ class TradingStats:
         }
 
     def _get_open_positions_count_from_db(self) -> int:
-        """Helper to get count of active enhanced positions."""
-        row = self._fetchone_query("SELECT COUNT(*) as count FROM enhanced_positions WHERE contracts != 0")
+        """🧹 PHASE 4: Get count of open positions from enhanced trades table."""
+        row = self._fetchone_query("SELECT COUNT(DISTINCT symbol) as count FROM trades WHERE status = 'position_opened'")
         return row['count'] if row else 0
 
     def format_stats_message(self, current_balance: Optional[float] = None) -> str:
@@ -1393,286 +1233,267 @@ class TradingStats:
     # --- End Order Table Management ---
 
     # =============================================================================
-    # TRADE LIFECYCLE MANAGEMENT
+    # TRADE LIFECYCLE MANAGEMENT - PHASE 4: UNIFIED TRADES TABLE
     # =============================================================================
     
-    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."""
+    def create_trade_lifecycle(self, symbol: str, side: str, entry_order_id: Optional[str] = None,
+                              stop_loss_price: Optional[float] = None, take_profit_price: Optional[float] = None,
+                              trade_type: str = 'manual') -> Optional[str]:
+        """Create a new trade lifecycle when an entry order is placed."""
         try:
+            lifecycle_id = str(uuid.uuid4())
+            
             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', ?, ?, ?, ?, ?, ?)
+                INSERT INTO trades (
+                    symbol, side, amount, price, value, trade_type, timestamp,
+                    status, trade_lifecycle_id, position_side, entry_order_id,
+                    stop_loss_price, take_profit_price, updated_at
+                ) VALUES (?, ?, 0, 0, 0, ?, ?, 'pending', ?, 'flat', ?, ?, ?, ?)
             """
             timestamp = datetime.now(timezone.utc).isoformat()
-            params = (symbol, side.lower(), entry_order_id, stop_loss_price, 
-                     take_profit_price, trade_type, timestamp, timestamp)
+            params = (symbol, side.lower(), trade_type, timestamp, lifecycle_id, 
+                     entry_order_id, stop_loss_price, take_profit_price, timestamp)
             
-            cursor = self.conn.execute(query, params)
-            trade_cycle_id = cursor.lastrowid
-            self.conn.commit()
+            self._execute_query(query, params)
             
-            logger.info(f"📊 Created trade cycle {trade_cycle_id}: {side.upper()} {symbol} (pending open)")
-            return trade_cycle_id
+            logger.info(f"📊 Created trade lifecycle {lifecycle_id}: {side.upper()} {symbol} (pending)")
+            return lifecycle_id
             
         except Exception as e:
-            logger.error(f"❌ Error creating trade cycle: {e}")
+            logger.error(f"❌ Error creating trade lifecycle: {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)."""
+    def update_trade_position_opened(self, lifecycle_id: str, entry_price: float, 
+                                   entry_amount: float, exchange_fill_id: str) -> bool:
+        """Update trade when position is opened (entry order filled)."""
         try:
             query = """
-                UPDATE trade_cycles 
-                SET status = 'open',
-                    entry_fill_id = ?,
+                UPDATE trades 
+                SET status = 'position_opened',
+                    amount = ?,
+                    price = ?,
+                    value = ?,
                     entry_price = ?,
-                    entry_amount = ?,
-                    entry_timestamp = ?,
+                    current_position_size = ?,
+                    position_side = CASE 
+                        WHEN side = 'buy' THEN 'long'
+                        WHEN side = 'sell' THEN 'short'
+                        ELSE position_side
+                    END,
+                    exchange_fill_id = ?,
+                    position_opened_at = ?,
                     updated_at = ?
-                WHERE id = ?
+                WHERE trade_lifecycle_id = ? AND status = 'pending'
             """
             timestamp = datetime.now(timezone.utc).isoformat()
-            params = (entry_fill_id, entry_price, entry_amount, entry_timestamp, timestamp, trade_cycle_id)
+            value = entry_amount * entry_price
+            params = (entry_amount, entry_price, value, entry_price, entry_amount,
+                     exchange_fill_id, timestamp, timestamp, lifecycle_id)
             
             self._execute_query(query, params)
             
-            logger.info(f"📈 Trade cycle {trade_cycle_id} opened: {entry_amount} @ ${entry_price:.2f}")
+            logger.info(f"📈 Trade lifecycle {lifecycle_id} position opened: {entry_amount} @ ${entry_price:.2f}")
             return True
             
         except Exception as e:
-            logger.error(f"❌ Error updating trade cycle opened: {e}")
+            logger.error(f"❌ Error updating trade position 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)."""
+    def update_trade_position_closed(self, lifecycle_id: str, exit_price: float,
+                                   realized_pnl: float, exchange_fill_id: str) -> bool:
+        """Update trade when position is fully 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 = ?,
+                UPDATE trades 
+                SET status = 'position_closed',
+                    current_position_size = 0,
+                    position_side = 'flat',
                     realized_pnl = ?,
-                    pnl_percentage = ?,
-                    duration_seconds = ?,
+                    position_closed_at = ?,
                     updated_at = ?
-                WHERE id = ?
+                WHERE trade_lifecycle_id = ? AND status = 'position_opened'
             """
             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)
+            params = (realized_pnl, timestamp, timestamp, lifecycle_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")
+            logger.info(f"{pnl_emoji} Trade lifecycle {lifecycle_id} position closed: P&L ${realized_pnl:.2f}")
             return True
             
         except Exception as e:
-            logger.error(f"❌ Error updating trade cycle closed: {e}")
+            logger.error(f"❌ Error updating trade position 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)."""
+    def update_trade_cancelled(self, lifecycle_id: str, reason: str = "order_cancelled") -> bool:
+        """Update trade when entry order is cancelled (never opened)."""
         try:
             query = """
-                UPDATE trade_cycles 
+                UPDATE trades 
                 SET status = 'cancelled',
                     notes = ?,
                     updated_at = ?
-                WHERE id = ?
+                WHERE trade_lifecycle_id = ? AND status = 'pending'
             """
             timestamp = datetime.now(timezone.utc).isoformat()
-            params = (f"Cancelled: {reason}", timestamp, trade_cycle_id)
+            params = (f"Cancelled: {reason}", timestamp, lifecycle_id)
             
             self._execute_query(query, params)
             
-            logger.info(f"❌ Trade cycle {trade_cycle_id} cancelled: {reason}")
+            logger.info(f"❌ Trade lifecycle {lifecycle_id} cancelled: {reason}")
             return True
             
         except Exception as e:
-            logger.error(f"❌ Error updating trade cycle cancelled: {e}")
+            logger.error(f"❌ Error updating trade 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."""
+    def link_stop_loss_to_trade(self, lifecycle_id: str, stop_loss_order_id: str,
+                               stop_loss_price: float) -> bool:
+        """Link a stop loss order to a trade lifecycle."""
         try:
             query = """
-                UPDATE trade_cycles 
+                UPDATE trades 
                 SET stop_loss_order_id = ?,
                     stop_loss_price = ?,
                     updated_at = ?
-                WHERE id = ? AND status = 'open'
+                WHERE trade_lifecycle_id = ? AND status = 'position_opened'
             """
             timestamp = datetime.now(timezone.utc).isoformat()
-            params = (stop_loss_order_id, stop_loss_price, timestamp, trade_cycle_id)
+            params = (stop_loss_order_id, stop_loss_price, timestamp, lifecycle_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}")
+            logger.info(f"🛑 Linked stop loss order {stop_loss_order_id} (${stop_loss_price:.2f}) to trade {lifecycle_id}")
             return True
             
         except Exception as e:
-            logger.error(f"❌ Error linking stop loss to trade cycle: {e}")
+            logger.error(f"❌ Error linking stop loss to trade: {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."""
+    def link_take_profit_to_trade(self, lifecycle_id: str, take_profit_order_id: str,
+                                 take_profit_price: float) -> bool:
+        """Link a take profit order to a trade lifecycle."""
         try:
             query = """
-                UPDATE trade_cycles 
+                UPDATE trades 
                 SET take_profit_order_id = ?,
                     take_profit_price = ?,
                     updated_at = ?
-                WHERE id = ? AND status = 'open'
+                WHERE trade_lifecycle_id = ? AND status = 'position_opened'
             """
             timestamp = datetime.now(timezone.utc).isoformat()
-            params = (take_profit_order_id, take_profit_price, timestamp, trade_cycle_id)
+            params = (take_profit_order_id, take_profit_price, timestamp, lifecycle_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}")
+            logger.info(f"🎯 Linked take profit order {take_profit_order_id} (${take_profit_price:.2f}) to trade {lifecycle_id}")
             return True
             
         except Exception as e:
-            logger.error(f"❌ Error linking take profit to trade cycle: {e}")
+            logger.error(f"❌ Error linking take profit to trade: {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_by_lifecycle_id(self, lifecycle_id: str) -> Optional[Dict[str, Any]]:
+        """Get trade by lifecycle ID."""
+        query = "SELECT * FROM trades WHERE trade_lifecycle_id = ?"
+        return self._fetchone_query(query, (lifecycle_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_trade_by_symbol_and_status(self, symbol: str, status: str = 'position_opened') -> Optional[Dict[str, Any]]:
+        """Get trade by symbol and status."""
+        query = "SELECT * FROM trades WHERE symbol = ? AND status = ? ORDER BY updated_at DESC LIMIT 1"
+        return self._fetchone_query(query, (symbol, status))
     
-    def get_open_trade_cycles(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
-        """Get all open trade cycles, optionally filtered by symbol."""
+    def get_open_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
+        """Get all open positions, optionally filtered by symbol."""
         if symbol:
-            query = "SELECT * FROM trade_cycles WHERE status = 'open' AND symbol = ? ORDER BY created_at DESC"
+            query = "SELECT * FROM trades WHERE status = 'position_opened' AND symbol = ? ORDER BY position_opened_at DESC"
             return self._fetch_query(query, (symbol,))
         else:
-            query = "SELECT * FROM trade_cycles WHERE status = 'open' ORDER BY created_at DESC"
+            query = "SELECT * FROM trades WHERE status = 'position_opened' ORDER BY position_opened_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 ?"
+    def get_trades_by_status(self, status: str, limit: int = 50) -> List[Dict[str, Any]]:
+        """Get trades by status."""
+        query = "SELECT * FROM trades 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_pending_stop_loss_activations(self) -> List[Dict[str, Any]]:
+        """Get open positions that need stop loss activation."""
+        query = """
+            SELECT * FROM trades 
+            WHERE status = 'position_opened' 
+            AND stop_loss_price IS NOT NULL 
+            AND stop_loss_order_id IS NULL
+            ORDER BY updated_at ASC
+        """
+        return self._fetch_query(query)
     
-    def get_trade_cycle_performance_stats(self) -> Dict[str, Any]:
-        """Get comprehensive trade cycle performance statistics."""
+    def cleanup_old_cancelled_trades(self, days_old: int = 7) -> int:
+        """Clean up old cancelled trades (optional - for housekeeping)."""
         try:
-            # Get closed trades for analysis
-            closed_trades = self.get_trade_cycles_by_status('closed', limit=1000)
+            cutoff_date = (datetime.now(timezone.utc) - timedelta(days=days_old)).isoformat()
+            
+            # Count before deletion
+            count_query = """
+                SELECT COUNT(*) as count FROM trades 
+                WHERE status = 'cancelled' AND updated_at < ?
+            """
+            count_result = self._fetchone_query(count_query, (cutoff_date,))
+            count_to_delete = count_result['count'] if count_result else 0
             
-            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
-                }
+            if count_to_delete > 0:
+                delete_query = """
+                    DELETE FROM trades 
+                    WHERE status = 'cancelled' AND updated_at < ?
+                """
+                self._execute_query(delete_query, (cutoff_date,))
+                logger.info(f"🧹 Cleaned up {count_to_delete} old cancelled trades (older than {days_old} days)")
             
-            # 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]
+            return count_to_delete
             
-            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')
+        except Exception as e:
+            logger.error(f"❌ Error cleaning up old cancelled trades: {e}")
+            return 0
+    
+    def confirm_position_with_exchange(self, symbol: str, exchange_position_size: float, 
+                                     exchange_open_orders: List[Dict]) -> bool:
+        """🆕 PHASE 4: Confirm position status with exchange before updating status."""
+        try:
+            # Get current trade status
+            current_trade = self.get_trade_by_symbol_and_status(symbol, 'position_opened')
             
-            # 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
+            if not current_trade:
+                return True  # No open position to confirm
             
-            # 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)
+            lifecycle_id = current_trade['trade_lifecycle_id']
+            has_open_orders = len([o for o in exchange_open_orders if o.get('symbol') == symbol]) > 0
             
-            # 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
+            # Only close position if exchange confirms no position AND no pending orders
+            if abs(exchange_position_size) < 1e-8 and not has_open_orders:
+                # Calculate realized P&L based on position side
+                position_side = current_trade['position_side']
+                entry_price = current_trade['entry_price']
+                current_amount = current_trade['current_position_size']
+                
+                # For a closed position, we need to calculate final P&L
+                # This would typically come from the closing trade, but for confirmation we estimate
+                estimated_pnl = current_trade.get('realized_pnl', 0)
+                
+                success = self.update_trade_position_closed(
+                    lifecycle_id, 
+                    entry_price,  # Using entry price as estimate since position is confirmed closed
+                    estimated_pnl,
+                    "exchange_confirmed_closed"
+                )
+                
+                if success:
+                    logger.info(f"✅ Confirmed position closed for {symbol} with exchange")
+                    
+                return success
             
-            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)
-            }
+            return True  # Position still exists on exchange, no update needed
             
         except Exception as e:
-            logger.error(f"❌ Error calculating trade cycle performance: {e}")
-            return {}
+            logger.error(f"❌ Error confirming position with exchange: {e}")
+            return False