Browse Source

Refactor InfoCommands and TradingCommands to remove deprecated auto-sync logic for orphaned positions. Updated notification handling to include trade lifecycle IDs for better tracking of orders. Enhanced MarketMonitor to utilize cached data for orders and positions, improving efficiency. Improved logging for better traceability of trade actions and notifications.

Carles Sentis 3 ngày trước cách đây
mục cha
commit
b185e1c107

+ 39 - 252
src/commands/info_commands.py

@@ -124,130 +124,21 @@ class InfoCommands:
             await context.bot.send_message(chat_id=chat_id, text="❌ Trading statistics not available.")
             return
         
-        # 🆕 AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records
-        # Use cached data from MarketMonitor if available (updated every heartbeat)
-        if (hasattr(self.trading_engine, 'market_monitor') and 
-            self.trading_engine.market_monitor and 
-            hasattr(self.trading_engine.market_monitor, 'get_cached_positions')):
-            
-            cache_age = self.trading_engine.market_monitor.get_cache_age_seconds()
-            if cache_age < 60:  # Use cached data if less than 1 minute old
-                exchange_positions = self.trading_engine.market_monitor.get_cached_positions() or []
-                logger.debug(f"Using cached positions for auto-sync (age: {cache_age:.1f}s)")
-            else:
-                exchange_positions = self.trading_engine.get_positions() or []
-                logger.debug("Using fresh API call for auto-sync (cache too old)")
-        else:
-            exchange_positions = self.trading_engine.get_positions() or []
-            logger.debug("Using fresh API call for auto-sync (no cache available)")
-        
-        synced_positions = []
+        # 🆕 AUTO-SYNC logic removed as per user request.
+        # Assuming heartbeat updates the DB sufficiently.
+        sync_msg = "" 
         
-        for exchange_pos in exchange_positions:
-            symbol = exchange_pos.get('symbol')
-            contracts = float(exchange_pos.get('contracts', 0))
-            
-            if symbol and abs(contracts) > 0:
-                # Check if we have a trade lifecycle record for this position
-                existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
-                
-                if not existing_trade:
-                    # 🚨 ORPHANED POSITION: Auto-create trade lifecycle record using exchange data
-                    entry_price = float(exchange_pos.get('entryPrice', 0))
-                    position_side = 'long' if contracts > 0 else 'short'
-                    order_side = 'buy' if contracts > 0 else 'sell'
-                    
-                    # ✅ Use exchange data - no need to estimate!
-                    if entry_price > 0:
-                        logger.info(f"🔄 Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)")
-                    else:
-                        # Fallback only if exchange truly doesn't provide entry price
-                        entry_price = await self._estimate_entry_price_for_orphaned_position(symbol, contracts)
-                        logger.warning(f"🔄 Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (estimated)")
-                    
-                    # Create trade lifecycle for external position
-                    lifecycle_id = stats.create_trade_lifecycle(
-                        symbol=symbol,
-                        side=order_side,
-                        entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
-                        trade_type='external'
-                    )
-                    
-                    if lifecycle_id:
-                        # Update to position_opened status
-                        success = stats.update_trade_position_opened(
-                            lifecycle_id=lifecycle_id,
-                            entry_price=entry_price,
-                            entry_amount=abs(contracts),
-                            exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}"
-                        )
-                        
-                        if success:
-                            synced_positions.append(symbol)
-                            logger.info(f"✅ Successfully synced orphaned position for {symbol}")
-                            
-                            # 🆕 Send immediate notification for auto-synced position
-                            token = symbol.split('/')[0] if '/' in symbol else symbol
-                            unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
-                            position_value = float(exchange_pos.get('notional', 0))
-                            liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
-                            leverage = float(exchange_pos.get('leverage', 1))
-                            pnl_percentage = float(exchange_pos.get('percentage', 0))
-                            
-                            pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
-                            notification_text = (
-                                f"🔄 <b>Position Auto-Synced</b>\n\n"
-                                f"🎯 Token: {token}\n"
-                                f"📈 Direction: {position_side.upper()}\n"
-                                f"📏 Size: {abs(contracts):.6f} {token}\n"
-                                f"💰 Entry: ${entry_price:,.4f}\n"
-                                f"💵 Value: ${position_value:,.2f}\n"
-                                f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
-                            )
-                            
-                            if leverage > 1:
-                                notification_text += f"⚡ Leverage: {leverage:.1f}x\n"
-                            if liquidation_price > 0:
-                                notification_text += f"⚠️ Liquidation: ${liquidation_price:,.2f}\n"
-                            
-                            notification_text += (
-                                f"\n📍 Reason: Position opened outside bot\n"
-                                f"⏰ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                                f"✅ Position now tracked in bot\n"
-                                f"💡 Use /sl {token} [price] to set stop loss"
-                            )
-                            
-                            # Send notification via trading engine's notification manager
-                            if self.notification_manager:
-                                try:
-                                    await self.notification_manager.send_generic_notification(notification_text)
-                                    logger.info(f"📤 Sent auto-sync notification for {symbol}")
-                                except Exception as e:
-                                    logger.error(f"❌ Failed to send auto-sync notification: {e}")
-                            else:
-                                logger.warning(f"⚠️ No notification manager available for auto-sync notification")
-                        else:
-                            logger.error(f"❌ Failed to sync orphaned position for {symbol}")
-                    else:
-                        logger.error(f"❌ Failed to create lifecycle for orphaned position {symbol}")
-        
-        if synced_positions:
-            sync_msg = f"🔄 <b>Auto-synced {len(synced_positions)} orphaned position(s):</b> {', '.join([s.split('/')[0] for s in synced_positions])}\n\n"
-        else:
-            sync_msg = ""
-        
-        # Get open positions from unified trades table (now including any newly synced ones)
+        # Get open positions from unified trades table
         open_positions = stats.get_open_positions()
         
-        positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}"
+        positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}" # sync_msg will be empty
         
         if open_positions:
             total_unrealized = 0
             total_position_value = 0
             
-            # Also get fresh exchange data for display
-            fresh_exchange_positions = self.trading_engine.get_positions() or []
-            exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
+            # Removed: fresh_exchange_positions = self.trading_engine.get_positions() or []
+            # Removed: exchange_data_map = {pos.get('symbol'): pos for pos in fresh_exchange_positions}
             
             for position_trade in open_positions:
                 symbol = position_trade['symbol']
@@ -257,51 +148,41 @@ class InfoCommands:
                 current_amount = position_trade['current_position_size']
                 trade_type = position_trade.get('trade_type', 'manual')
                 
-                # 🆕 Use fresh exchange data if available (most accurate)
-                exchange_pos = exchange_data_map.get(symbol)
-                if exchange_pos:
-                    # Use exchange's official data
-                    unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
-                    mark_price = float(exchange_pos.get('markPrice') or 0)
-                    position_value = float(exchange_pos.get('notional', 0))
-                    liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
-                    margin_used = float(exchange_pos.get('initialMargin', 0))
-                    leverage = float(exchange_pos.get('leverage', 1))
-                    pnl_percentage = float(exchange_pos.get('percentage', 0))
-                    
-                    # Get mark price from market data if not in position data
-                    if mark_price <= 0:
-                        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))
-                        except:
-                            mark_price = entry_price  # Fallback
+                # 🆕 Data now comes directly from position_trade (DB)
+                # Fields like unrealized_pnl, mark_price, etc., are expected to be in position_trade
+                # or calculated based on DB data.
+
+                # Attempt to get live data from position_trade, otherwise use defaults or calculate.
+                # It's assumed the database record (position_trade) is updated by the heartbeat
+                # and contains the necessary information like PnL, mark price, etc.
+                
+                mark_price = position_trade.get('mark_price', entry_price) # Default to entry if not available
+                
+                # Calculate unrealized PnL if not directly available or needs recalculation with current mark_price
+                if 'unrealized_pnl' in position_trade:
+                    unrealized_pnl = position_trade['unrealized_pnl']
                 else:
-                    # Fallback to our calculation if exchange data unavailable
-                    unrealized_pnl = position_trade.get('unrealized_pnl', 0)
-                    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))
-                            
-                            # Calculate 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
-                    
+                    if position_side == 'long':
+                        unrealized_pnl = current_amount * (mark_price - entry_price)
+                    else:  # Short position
+                        unrealized_pnl = current_amount * (entry_price - mark_price)
+
+                position_value = position_trade.get('position_value')
+                if position_value is None: # Calculate if not in DB
                     position_value = abs(current_amount) * mark_price
-                    liquidation_price = None
-                    margin_used = None
-                    leverage = None
-                    pnl_percentage = (unrealized_pnl / position_value * 100) if position_value > 0 else 0
+
+                liquidation_price = position_trade.get('liquidation_price') # Optional, might not be in DB
+                margin_used = position_trade.get('margin_used') # Optional
+                leverage = position_trade.get('leverage') # Optional
                 
-                total_position_value += position_value
-                total_unrealized += unrealized_pnl
+                pnl_percentage = position_trade.get('pnl_percentage')
+                if pnl_percentage is None and position_value and position_value > 0 : # Calculate if not in DB
+                     pnl_percentage = (unrealized_pnl / position_value * 100) 
+                elif pnl_percentage is None:
+                    pnl_percentage = 0
+
+                total_position_value += position_value if position_value else 0
+                total_unrealized += unrealized_pnl if unrealized_pnl else 0
                 
                 # Position emoji and formatting
                 if position_side == 'long':
@@ -521,100 +402,6 @@ class InfoCommands:
             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
-            
-        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:
-            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}")
-    
     async def active_trades_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
         """Handle the /active command to show active trades (using open positions)."""
         chat_id = update.effective_chat.id

+ 10 - 5
src/commands/trading_commands.py

@@ -736,6 +736,7 @@ This action cannot be undone.
             order_details = result.get("order_placed_details", {})
             token_amount = result.get("token_amount", 0)
             price_used = order_details.get("price_requested") or price
+            trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             
             # Create a mock order object for backward compatibility with notification method
             mock_order = {
@@ -744,7 +745,7 @@ This action cannot be undone.
             }
             
             await self.notification_manager.send_long_success_notification(
-                query, token, token_amount, price_used, mock_order, stop_loss_price
+                query, token, token_amount, price_used, mock_order, stop_loss_price, trade_lifecycle_id
             )
         else:
             await query.edit_message_text(f"❌ Long order failed: {result['error']}")
@@ -770,6 +771,7 @@ This action cannot be undone.
             order_details = result.get("order_placed_details", {})
             token_amount = result.get("token_amount", 0)
             price_used = order_details.get("price_requested") or price
+            trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             
             # Create a mock order object for backward compatibility with notification method
             mock_order = {
@@ -778,7 +780,7 @@ This action cannot be undone.
             }
             
             await self.notification_manager.send_short_success_notification(
-                query, token, token_amount, price_used, mock_order, stop_loss_price
+                query, token, token_amount, price_used, mock_order, stop_loss_price, trade_lifecycle_id
             )
         else:
             await query.edit_message_text(f"❌ Short order failed: {result['error']}")
@@ -798,6 +800,7 @@ This action cannot be undone.
             position_type_closed = result.get("position_type_closed", "UNKNOWN")
             contracts_intended_to_close = result.get("contracts_intended_to_close", 0)
             cancelled_stop_losses = result.get("cancelled_stop_losses", 0)
+            trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             
             # For market orders, we won't have the actual execution price until the fill
             # We'll use 0 for now since this will be updated by MarketMonitor when the fill occurs
@@ -812,7 +815,7 @@ This action cannot be undone.
             
             await self.notification_manager.send_exit_success_notification(
                 query, token, position_type_closed, contracts_intended_to_close, 
-                estimated_price, estimated_pnl, mock_order
+                estimated_price, estimated_pnl, mock_order, trade_lifecycle_id
             )
         else:
             await query.edit_message_text(f"❌ Exit order failed: {result['error']}")
@@ -828,9 +831,10 @@ This action cannot be undone.
         result = await self.trading_engine.execute_sl_order(token, stop_price)
         
         if result["success"]:
+            trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             await self.notification_manager.send_sl_success_notification(
                 query, token, result["position_type_for_sl"], result["contracts_for_sl"], 
-                stop_price, result.get("order_placed_details", {})
+                stop_price, result.get("order_placed_details", {}), trade_lifecycle_id
             )
         else:
             await query.edit_message_text(f"❌ Stop loss failed: {result['error']}")
@@ -846,9 +850,10 @@ This action cannot be undone.
         result = await self.trading_engine.execute_tp_order(token, tp_price)
         
         if result["success"]:
+            trade_lifecycle_id = result.get("trade_lifecycle_id") # Extract lifecycle ID
             await self.notification_manager.send_tp_success_notification(
                 query, token, result["position_type_for_tp"], result["contracts_for_tp"], 
-                tp_price, result.get("order_placed_details", {})
+                tp_price, result.get("order_placed_details", {}), trade_lifecycle_id
             )
         else:
             await query.edit_message_text(f"❌ Take profit failed: {result['error']}")

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 456 - 520
src/monitoring/market_monitor.py


+ 117 - 276
src/notifications/notification_manager.py

@@ -6,6 +6,7 @@ Notification Manager - Handles all bot notifications and messages.
 import logging
 from typing import Optional, Dict, Any, List
 from datetime import datetime
+from src.utils.price_formatter import get_formatter
 
 logger = logging.getLogger(__name__)
 
@@ -20,291 +21,131 @@ class NotificationManager:
         """Set the bot application for sending messages."""
         self.bot_application = application
     
-    async def send_long_success_notification(self, query, token: str, token_amount: float, 
-                                           actual_price: float, order: Dict[str, Any], 
-                                           stop_loss_price: Optional[float] = None):
-        """Send notification for successful long order placement."""
-        order_id = order.get('id', 'N/A')
-        order_type = order.get('type', 'Unknown').title()
-        
-        # Handle None actual_price for market orders
-        if actual_price is None:
-            actual_price = 0.0
-            price_display = "Market Price"
-            price_value = "TBD"
-        else:
-            price_display = "Limit Price" if order_type == "Limit" else "Est. Price"
-            price_value = f"${actual_price:,.2f}"
-        
-        status_message = "ORDER PLACED" if order_type == "Limit" else "ORDER SUBMITTED"
-        
-        success_message = f"""
-✅ <b>Long Order Placed Successfully!</b>
-
-📊 <b>Order Details:</b>
-• Token: {token}
-• Direction: LONG (Buy)
-• Amount: {token_amount:.6f} {token}
-• {price_display}: {price_value}
-• Order Type: {order_type}
-• Order ID: <code>{order_id}</code>
-
-💰 <b>Order Summary:</b>"""
-
-        if actual_price > 0:
-            success_message += f"""
-• Order Value: ${token_amount * actual_price:,.2f}"""
-        else:
-            success_message += f"""
-• Order Value: Market execution"""
+    async def send_long_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
+        """Send notification for successful long order."""
+        try:
+            # Use PriceFormatter for consistent formatting
+            formatter = get_formatter() # Get formatter
             
-        success_message += f"""
-• Status: {status_message} ✅
-• Time: {datetime.now().strftime('%H:%M:%S')}"""
-
-        if order_type == "Market":
-            success_message += f"""
-
-💡 <b>Note:</b> Market order submitted for execution
-• Actual fill price will be determined by market"""
-        else:
-            success_message += f"""
-
-💡 <b>Note:</b> Limit order placed on exchange
-• Will fill when market price reaches {price_value}"""
+            price_str = formatter.format_price_with_symbol(price, token)
+            amount_str = f"{amount:.6f} {token}"
+            value_str = formatter.format_price_with_symbol(amount * price, token)
+            order_id_str = order_details.get('id', 'N/A')
             
-        if stop_loss_price:
-            success_message += f"""
-
-🛑 <b>Pending Stop Loss:</b> ${stop_loss_price:,.2f}
-• Status: Will activate when main order fills
-• Protection: Automatic position closure"""
-        else:
-            success_message += f"""
-
-💡 Consider setting a stop loss with /sl {token} [price] after order fills"""
-
-        success_message += f"""
-
-📊 Use /orders to monitor order status | /positions to view positions
-        """
-        
-        await query.edit_message_text(success_message, parse_mode='HTML')
-        
-        log_price = f"${actual_price:,.2f}" if actual_price > 0 else "Market"
-        logger.info(f"Long order placed: {token_amount:.6f} {token} @ {log_price} ({order_type})")
-    
-    async def send_short_success_notification(self, query, token: str, token_amount: float, 
-                                            actual_price: float, order: Dict[str, Any], 
-                                            stop_loss_price: Optional[float] = None):
-        """Send notification for successful short order placement."""
-        order_id = order.get('id', 'N/A')
-        order_type = order.get('type', 'Unknown').title()
-        
-        # Handle None actual_price for market orders
-        if actual_price is None:
-            actual_price = 0.0
-            price_display = "Market Price"
-            price_value = "TBD"
-        else:
-            price_display = "Limit Price" if order_type == "Limit" else "Est. Price"
-            price_value = f"${actual_price:,.2f}"
-        
-        status_message = "ORDER PLACED" if order_type == "Limit" else "ORDER SUBMITTED"
-        
-        success_message = f"""
-✅ <b>Short Order Placed Successfully!</b>
-
-📊 <b>Order Details:</b>
-• Token: {token}
-• Direction: SHORT (Sell)
-• Amount: {token_amount:.6f} {token}
-• {price_display}: {price_value}
-• Order Type: {order_type}
-• Order ID: <code>{order_id}</code>
-
-💰 <b>Order Summary:</b>"""
-
-        if actual_price > 0:
-            success_message += f"""
-• Order Value: ${token_amount * actual_price:,.2f}"""
-        else:
-            success_message += f"""
-• Order Value: Market execution"""
+            message = (
+                f"✅ Successfully opened <b>LONG</b> position for {amount_str} at ~{price_str}\n\n"
+                f"💰 Value: {value_str}\n"
+                f"🆔 Order ID: <code>{order_id_str}</code>"
+            )
+            if trade_lifecycle_id:
+                message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
+
+            if stop_loss_price:
+                sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
+                message += f"\n🛑 Stop Loss pending at {sl_price_str}"
             
-        success_message += f"""
-• Status: {status_message} ✅
-• Time: {datetime.now().strftime('%H:%M:%S')}"""
-
-        if order_type == "Market":
-            success_message += f"""
-
-💡 <b>Note:</b> Market order submitted for execution
-• Actual fill price will be determined by market"""
-        else:
-            success_message += f"""
+            await query.edit_message_text(text=message, parse_mode='HTML')
+        except Exception as e:
+            logger.error(f"Error sending long success notification: {e}")
 
-💡 <b>Note:</b> Limit order placed on exchange
-• Will fill when market price reaches {price_value}"""
+    async def send_short_success_notification(self, query, token, amount, price, order_details, stop_loss_price=None, trade_lifecycle_id=None):
+        """Send notification for successful short order."""
+        try:
+            formatter = get_formatter() # Get formatter
             
-        if stop_loss_price:
-            success_message += f"""
-
-🛑 <b>Pending Stop Loss:</b> ${stop_loss_price:,.2f}
-• Status: Will activate when main order fills
-• Protection: Automatic position closure"""
-        else:
-            success_message += f"""
-
-💡 Consider setting a stop loss with /sl {token} [price] after order fills"""
-
-        success_message += f"""
+            price_str = formatter.format_price_with_symbol(price, token)
+            amount_str = f"{amount:.6f} {token}"
+            value_str = formatter.format_price_with_symbol(amount * price, token)
+            order_id_str = order_details.get('id', 'N/A')
+            
+            message = (
+                f"✅ Successfully opened <b>SHORT</b> position for {amount_str} at ~{price_str}\n\n"
+                f"💰 Value: {value_str}\n"
+                f"🆔 Order ID: <code>{order_id_str}</code>"
+            )
+            if trade_lifecycle_id:
+                message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
+
+            if stop_loss_price:
+                sl_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
+                message += f"\n🛑 Stop Loss pending at {sl_price_str}"
+            
+            await query.edit_message_text(text=message, parse_mode='HTML')
+        except Exception as e:
+            logger.error(f"Error sending short success notification: {e}")
 
-📊 Use /orders to monitor order status | /positions to view positions
-        """
-        
-        await query.edit_message_text(success_message, parse_mode='HTML')
-        
-        log_price = f"${actual_price:,.2f}" if actual_price > 0 else "Market"
-        logger.info(f"Short order placed: {token_amount:.6f} {token} @ {log_price} ({order_type})")
-    
-    async def send_exit_success_notification(self, query, token: str, position_type: str, 
-                                           contracts: float, actual_price: float, 
-                                           pnl: float, order: Dict[str, Any]):
+    async def send_exit_success_notification(self, query, token, position_type, amount, price, pnl, order_details, trade_lifecycle_id=None):
         """Send notification for successful exit order."""
-        order_id = order.get('id', 'N/A')
-        pnl_emoji = "🟢" if pnl >= 0 else "🔴"
-        action = "SELL" if position_type == "LONG" else "BUY"
-        
-        # Check if stop losses were cancelled
-        cancelled_sls = order.get('cancelled_stop_losses', 0)
-        
-        # Check if this is a market order without actual price yet
-        is_market_order_pending = actual_price == 0
-        
-        success_message = f"""
-✅ <b>{position_type} Position Exit Order Placed!</b>
-
-📊 <b>Exit Details:</b>
-• Token: {token}
-• Position: {position_type}
-• Action: {action} (Close)
-• Amount: {contracts:.6f} {token}
-• Order Type: Market Order
-• Order ID: <code>{order_id}</code>"""
-
-        if is_market_order_pending:
-            success_message += f"""
-
-⏳ <b>Order Status:</b>
-• Status: SUBMITTED ✅
-• Execution: Pending market fill
-• Time: {datetime.now().strftime('%H:%M:%S')}
-
-💡 <b>Note:</b> Actual execution price and P&L will be shown when the order fills."""
-        else:
-            success_message += f"""
-• Exit Price: ${actual_price:,.2f}
-
-💰 <b>Trade Summary:</b>
-• Exit Value: ${contracts * actual_price:,.2f}
-• {pnl_emoji} Realized P&L: ${pnl:,.2f}
-• Status: FILLED ✅
-• Time: {datetime.now().strftime('%H:%M:%S')}"""
-        
-        if cancelled_sls > 0:
-            success_message += f"""
-
-🛑 <b>Cleanup:</b>
-• Cancelled {cancelled_sls} pending stop loss order(s)
-• All protective orders removed"""
-        
-        if is_market_order_pending:
-            success_message += f"""
-
-📊 Position exit order submitted successfully
-💡 Use /orders to monitor order status"""
-        else:
-            success_message += f"""
-
-📊 <b>Result:</b> Position fully closed
-💡 Use /stats to view updated performance metrics."""
-        
-        await query.edit_message_text(success_message, parse_mode='HTML')
-        
-        log_message = f"Exit order placed: {contracts:.6f} {token}"
-        if not is_market_order_pending:
-            log_message += f" @ ${actual_price:,.2f} (P&L: ${pnl:,.2f})"
-        if cancelled_sls > 0:
-            log_message += f" | Cancelled {cancelled_sls} SLs"
-        
-        logger.info(log_message)
-    
-    async def send_sl_success_notification(self, query, token: str, position_type: str, 
-                                         contracts: float, stop_price: float, 
-                                         order: Dict[str, Any]):
-        """Send notification for successful stop loss order."""
-        order_id = order.get('id', 'N/A')
-        action = "SELL" if position_type == "LONG" else "BUY"
-        
-        success_message = f"""
-✅ <b>Stop Loss Order Set Successfully!</b>
+        try:
+            formatter = get_formatter() # Get formatter
+            
+            # Price is the execution price, PnL is calculated based on it
+            # For market orders, price might be approximate or from fill later
+            price_str = formatter.format_price_with_symbol(price, token) if price > 0 else "Market Price"
+            amount_str = f"{amount:.6f} {token}"
+            pnl_str = formatter.format_price_with_symbol(pnl)
+            pnl_emoji = "🟢" if pnl >= 0 else "🔴"
+            order_id_str = order_details.get('id', 'N/A')
+            cancelled_sl_count = order_details.get('cancelled_stop_losses', 0)
+            
+            message = (
+                f"✅ Successfully closed <b>{position_type}</b> position for {amount_str}\n\n"
+                f"🆔 Exit Order ID: <code>{order_id_str}</code>\n"
+                # P&L and price are more reliably determined when MarketMonitor processes the fill.
+                # This notification confirms the exit order was PLACED.
+                f"⏳ Awaiting fill confirmation for final price and P&L."
+            )
+            if trade_lifecycle_id:
+                message += f"\n🆔 Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code> (Closed)"
+            
+            if cancelled_sl_count > 0:
+                message += f"\n⚠️ Cancelled {cancelled_sl_count} linked stop loss order(s)."
+                
+            await query.edit_message_text(text=message, parse_mode='HTML')
+        except Exception as e:
+            logger.error(f"Error sending exit success notification: {e}")
 
-📊 <b>Stop Loss Details:</b>
-• Token: {token}
-• Position: {position_type}
-• Size: {contracts:.6f} contracts
-• Stop Price: ${stop_price:,.2f}
-• Action: {action} (Close {position_type})
-• Order Type: Limit Order
-• Order ID: <code>{order_id}</code>
-
-🛑 <b>Risk Management:</b>
-• Status: ACTIVE ✅
-• Trigger: When price reaches ${stop_price:,.2f}
-• Protection: Automatic position closure
-• Time: {datetime.now().strftime('%H:%M:%S')}
-
-💡 <b>Note:</b> The stop loss order will execute automatically when the market price reaches your stop price.
-
-📊 Use /orders to view all active orders.
-        """
-        
-        await query.edit_message_text(success_message, parse_mode='HTML')
-        logger.info(f"Stop loss set: {token} @ ${stop_price:,.2f}")
-    
-    async def send_tp_success_notification(self, query, token: str, position_type: str, 
-                                         contracts: float, tp_price: float, 
-                                         order: Dict[str, Any]):
-        """Send notification for successful take profit order."""
-        order_id = order.get('id', 'N/A')
-        action = "SELL" if position_type == "LONG" else "BUY"
-        
-        success_message = f"""
-✅ <b>Take Profit Order Set Successfully!</b>
+    async def send_sl_success_notification(self, query, token, position_type, amount, stop_price, order_details, trade_lifecycle_id=None):
+        """Send notification for successful stop loss order setup."""
+        try:
+            formatter = get_formatter() # Get formatter
+            
+            stop_price_str = formatter.format_price_with_symbol(stop_price, token)
+            amount_str = f"{amount:.6f} {token}"
+            order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
+            
+            message = (
+                f"🛑 Successfully set <b>STOP LOSS</b> for {position_type} {amount_str}\n\n"
+                f"🎯 Trigger Price: {stop_price_str}\n"
+                f"🆔 SL Order ID: <code>{order_id_str}</code>"
+            )
+            if trade_lifecycle_id:
+                message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
+
+            await query.edit_message_text(text=message, parse_mode='HTML')
+        except Exception as e:
+            logger.error(f"Error sending SL success notification: {e}")
 
-📊 <b>Take Profit Details:</b>
-• Token: {token}
-• Position: {position_type}
-• Size: {contracts:.6f} contracts
-• Take Profit Price: ${tp_price:,.2f}
-• Action: {action} (Close {position_type})
-• Order Type: Limit Order
-• Order ID: <code>{order_id}</code>
-
-🎯 <b>Profit Management:</b>
-• Status: ACTIVE ✅
-• Trigger: When price reaches ${tp_price:,.2f}
-• Action: Automatic profit taking
-• Time: {datetime.now().strftime('%H:%M:%S')}
-
-💡 <b>Note:</b> The take profit order will execute automatically when the market price reaches your target price.
-
-📊 Use /orders to view all active orders.
-        """
-        
-        await query.edit_message_text(success_message, parse_mode='HTML')
-        logger.info(f"Take profit set: {token} @ ${tp_price:,.2f}")
+    async def send_tp_success_notification(self, query, token, position_type, amount, tp_price, order_details, trade_lifecycle_id=None):
+        """Send notification for successful take profit order setup."""
+        try:
+            formatter = get_formatter() # Get formatter
+            
+            tp_price_str = formatter.format_price_with_symbol(tp_price, token)
+            amount_str = f"{amount:.6f} {token}"
+            order_id_str = order_details.get('exchange_order_id', 'N/A') # From order_placed_details
+
+            message = (
+                f"🎯 Successfully set <b>TAKE PROFIT</b> for {position_type} {amount_str}\n\n"
+                f"💰 Target Price: {tp_price_str}\n"
+                f"🆔 TP Order ID: <code>{order_id_str}</code>"
+            )
+            if trade_lifecycle_id:
+                message += f"\n🆔 Linked to Lifecycle ID: <code>{trade_lifecycle_id[:8]}...</code>"
+            
+            await query.edit_message_text(text=message, parse_mode='HTML')
+        except Exception as e:
+            logger.error(f"Error sending TP success notification: {e}")
     
     async def send_coo_success_notification(self, query, token: str, cancelled_count: int, 
                                           failed_count: int, cancelled_linked_sls: int = 0,

+ 37 - 5
src/trading/trading_engine.py

@@ -319,7 +319,8 @@ class TradingEngine:
                 # "action_type": action_type, # Removed as trade is not recorded here
                 "token_amount": token_amount,
                 # "actual_price": final_price_for_stats, # Removed as fill is not processed here
-                "stop_loss_pending": stop_loss_price is not None
+                "stop_loss_pending": stop_loss_price is not None,
+                "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
         except ZeroDivisionError as e:
             logger.error(f"Error executing long order due to ZeroDivisionError (likely price issue): {e}. LimitArg: {limit_price_arg}, CurrentPrice: {current_price if 'current_price' in locals() else 'N/A'}")
@@ -463,7 +464,8 @@ class TradingEngine:
                     "price_requested": order_placement_price if order_type_for_stats == 'limit' else None
                 },
                 "token_amount": token_amount,
-                "stop_loss_pending": stop_loss_price is not None
+                "stop_loss_pending": stop_loss_price is not None,
+                "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
         except ZeroDivisionError as e:
             logger.error(f"Error executing short order due to ZeroDivisionError (likely price issue): {e}. LimitArg: {limit_price_arg}, CurrentPrice: {current_price if 'current_price' in locals() else 'N/A'}")
@@ -543,6 +545,13 @@ class TradingEngine:
             # 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
             
+            # Fetch the lifecycle ID of the position being closed
+            lifecycle_id_to_close = None
+            if self.stats:
+                active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                if active_trade_lc:
+                    lifecycle_id_to_close = active_trade_lc.get('trade_lifecycle_id')
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -556,7 +565,8 @@ class TradingEngine:
                 },
                 "position_type_closed": position_type, # Info about the position it intends to close
                 "contracts_intended_to_close": contracts_to_close,
-                "cancelled_stop_losses": cancelled_sl_count if self.stats else 0
+                "cancelled_stop_losses": cancelled_sl_count if self.stats else 0,
+                "trade_lifecycle_id": lifecycle_id_to_close # Return lifecycle_id of the closed position
             }
         except Exception as e:
             logger.error(f"Error executing exit order: {e}")
@@ -621,6 +631,16 @@ class TradingEngine:
             # NOTE: Stop loss orders are protective orders for existing positions
             # They do not create new trade cycles - they protect existing trade cycles
             
+            # Fetch the lifecycle_id for the current position
+            lifecycle_id = None
+            if self.stats:
+                active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                if active_trade_lc:
+                    lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
+                    if exchange_oid: # If SL order placed successfully on exchange
+                        self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, stop_price)
+                        logger.info(f"🛡️ Linked SL order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -635,7 +655,8 @@ class TradingEngine:
                 },
                 "position_type_for_sl": position_type, # Info about the position it's protecting
                 "contracts_for_sl": contracts,
-                "stop_price_set": stop_price
+                "stop_price_set": stop_price,
+                "trade_lifecycle_id": lifecycle_id # Return lifecycle_id
             }
         except Exception as e:
             logger.error(f"Error executing stop loss order: {e}")
@@ -700,6 +721,16 @@ class TradingEngine:
             # NOTE: Take profit orders are protective orders for existing positions
             # They do not create new trade cycles - they protect existing trade cycles
             
+            # Fetch the lifecycle_id for the current position
+            lifecycle_id = None
+            if self.stats:
+                active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
+                if active_trade_lc:
+                    lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
+                    if exchange_oid: # If TP order placed successfully on exchange
+                        self.stats.link_take_profit_to_trade(lifecycle_id, exchange_oid, profit_price)
+                        logger.info(f"🎯 Linked TP order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
+            
             return {
                 "success": True,
                 "order_placed_details": {
@@ -714,7 +745,8 @@ class TradingEngine:
                 },
                 "position_type_for_tp": position_type, # Info about the position it's for
                 "contracts_for_tp": contracts,
-                "profit_price_set": profit_price
+                "profit_price_set": profit_price,
+                "trade_lifecycle_id": lifecycle_id # Return lifecycle_id
             }
         except Exception as e:
             logger.error(f"Error executing take profit order: {e}")

+ 25 - 8
src/trading/trading_stats.py

@@ -739,15 +739,9 @@ class TradingStats:
             }
 
         # Calculate completed cycles specifically for these trades
-        # This re-uses calculate_completed_trade_cycles but on a subset, which is not ideal.
-        # A more optimized way would be to adapt calculate_completed_trade_cycles to take trades as input,
-        # or query cycles if they were stored. For now, this mirrors the old approach.
-        
         # To correctly calculate cycles for *only* this token, we need to run the FIFO logic
         # on trades filtered for this token.
         # The global `calculate_completed_trade_cycles` uses *all* trades.
-        # We need a version of cycle calculation that can be limited or we filter its output.
-        
         all_completed_cycles = self.calculate_completed_trade_cycles()
         token_cycles = [c for c in all_completed_cycles if _normalize_token_case(c['token']) == upper_token]
 
@@ -767,6 +761,9 @@ class TradingStats:
         # Performance based on this token's completed cycles
         perf_stats = self.get_token_performance().get(upper_token, {}) # Re-use general calculation logic
 
+        # Filter for recent closed trades
+        recent_closed_trades = [t for t in all_trades_for_token_symbol_prefix if t.get('status') == 'position_closed']
+
         return {
             'token': upper_token,
             'total_trades': total_individual_orders,
@@ -786,8 +783,8 @@ class TradingStats:
             'expectancy': perf_stats.get('expectancy', 0.0),
             'total_wins': perf_stats.get('total_wins',0),
             'total_losses': perf_stats.get('total_losses',0),
-            'recent_trades': all_trades_for_token_symbol_prefix[-5:], # Last 5 individual orders for this token
-            # 'cycles': token_cycles # Optionally include raw cycle data
+            'recent_trades': recent_closed_trades[-5:], # Last 5 CLOSET trades for this token
+            'cycles': token_cycles # Optionally include raw cycle data
         }
 
     def _get_aggregated_period_stats_from_cycles(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
@@ -1429,6 +1426,26 @@ class TradingStats:
         query = "SELECT * FROM trades WHERE status = ? ORDER BY updated_at DESC LIMIT ?"
         return self._fetch_query(query, (status, limit))
     
+    def get_lifecycle_by_entry_order_id(self, entry_exchange_order_id: str, status: Optional[str] = None) -> Optional[Dict[str, Any]]:
+        """Get a trade lifecycle by its entry_order_id (exchange ID) and optionally by status."""
+        if status:
+            query = "SELECT * FROM trades WHERE entry_order_id = ? AND status = ? LIMIT 1"
+            params = (entry_exchange_order_id, status)
+        else:
+            query = "SELECT * FROM trades WHERE entry_order_id = ? LIMIT 1"
+            params = (entry_exchange_order_id,)
+        return self._fetchone_query(query, params)
+
+    def get_lifecycle_by_sl_order_id(self, sl_exchange_order_id: str, status: str = 'position_opened') -> Optional[Dict[str, Any]]:
+        """Get an active trade lifecycle by its stop_loss_order_id (exchange ID)."""
+        query = "SELECT * FROM trades WHERE stop_loss_order_id = ? AND status = ? LIMIT 1"
+        return self._fetchone_query(query, (sl_exchange_order_id, status))
+
+    def get_lifecycle_by_tp_order_id(self, tp_exchange_order_id: str, status: str = 'position_opened') -> Optional[Dict[str, Any]]:
+        """Get an active trade lifecycle by its take_profit_order_id (exchange ID)."""
+        query = "SELECT * FROM trades WHERE take_profit_order_id = ? AND status = ? LIMIT 1"
+        return self._fetchone_query(query, (tp_exchange_order_id, status))
+    
     def get_pending_stop_loss_activations(self) -> List[Dict[str, Any]]:
         """Get open positions that need stop loss activation."""
         query = """

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác