Pārlūkot izejas kodu

Increment BOT_VERSION to 2.2.141 and refactor stop loss order handling in TradingEngine and MarketMonitor.

- Updated BOT_VERSION for the upcoming release.
- Refined stop loss order implementation to utilize limit orders instead of stop-market orders, enhancing execution logic.
- Introduced a new method for placing limit stop-loss orders linked to trade lifecycles, improving order management.
- Simplified stop loss handling in MarketMonitor, ensuring clearer logging and error handling for order placements.
Carles Sentis 2 dienas atpakaļ
vecāks
revīzija
a37ed896e4

+ 1 - 1
src/clients/hyperliquid_client.py

@@ -490,7 +490,7 @@ class HyperliquidClient:
                 'market',  # Order type to execute when triggered
                 side, 
                 amount, 
-                stop_price_arg, # Use trigger price for slippage protection
+                None, # For stop-market, price is None; execution price determined by market when triggered
                 params=trigger_params
             )
             

+ 66 - 172
src/monitoring/market_monitor.py

@@ -1370,191 +1370,85 @@ class MarketMonitor:
             for position_trade in trades_needing_sl:
                 try:
                     symbol = position_trade['symbol']
-                    token = symbol.split('/')[0] if '/' in symbol else symbol
+                    # Ensure token is derived correctly for formatter, handling potential None symbol early
+                    token = symbol.split('/')[0] if symbol and '/' in symbol else (symbol if symbol else "TOKEN")
                     stop_loss_price = position_trade['stop_loss_price']
                     position_side = position_trade['position_side'] # 'long' or 'short'
-                    # current_amount = position_trade.get('current_position_size', 0) # Amount not needed for SL placement logic here
+                    current_amount = position_trade.get('current_position_size', 0)
                     lifecycle_id = position_trade['trade_lifecycle_id']
+
+                    if not all([symbol, stop_loss_price, position_side, abs(current_amount) > 1e-9, lifecycle_id]):
+                        logger.warning(f"Skipping SL activation for lifecycle {lifecycle_id} due to incomplete data: sym={symbol}, sl_price={stop_loss_price}, side={position_side}, amt={current_amount}")
+                        continue
                     
-                    # 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} for SL activation of {lifecycle_id}: {price_error}")
-                    
-                    # Determine stop loss side based on position side
-                    sl_side = 'sell' if position_side == 'long' else 'buy'
+                    # MODIFICATION: Remove immediate market execution logic.
+                    # Always proceed to place the limit stop-loss order via the new engine method.
+                    # current_price = None
+                    # try:
+                    #     market_data = self.trading_engine.get_market_data(symbol)
+                    #     if market_data and market_data.get('ticker'):
+                    #         current_price = float(market_data['ticker'].get('last', 0))
+                    # except Exception as price_error:
+                    #     logger.warning(f"Could not fetch current price for {symbol} for SL activation of {lifecycle_id}: {price_error}")
                     
-                    trigger_already_hit = False
-                    trigger_reason = ""
+                    # sl_side = 'sell' if position_side == 'long' else 'buy'
+                    # trigger_already_hit = False
+                    # trigger_reason = ""
+                    # if current_price and current_price > 0 and stop_loss_price and stop_loss_price > 0:
+                    #     # ... (old trigger_already_hit logic removed)
                     
-                    if current_price and current_price > 0 and stop_loss_price and stop_loss_price > 0:
-                        current_price_str = formatter.format_price_with_symbol(current_price, token)
-                        stop_loss_price_str = formatter.format_price_with_symbol(stop_loss_price, token)
-                        if sl_side == 'sell' and current_price <= stop_loss_price:
-                            trigger_already_hit = True
-                            trigger_reason = f"LONG SL: Current {current_price_str} ≤ Stop {stop_loss_price_str}"
-                        elif sl_side == 'buy' and current_price >= stop_loss_price:
-                            trigger_already_hit = True
-                            trigger_reason = f"SHORT SL: Current {current_price_str} ≥ Stop {stop_loss_price_str}"
+                    # if trigger_already_hit:
+                    #     # ... (old immediate market exit logic removed)
+                    # else:
+                    # Normal activation - place SL limit order using the new engine method
                     
-                    if trigger_already_hit:
-                        logger.warning(f"🚨 IMMEDIATE SL EXECUTION (Trades Table): {token} (Lifecycle: {lifecycle_id[:8]}) - {trigger_reason}. Executing market exit.")
+                    logger.info(f"Attempting to place LIMIT stop loss for lifecycle {lifecycle_id} ({position_side} {token} @ SL {formatter.format_price(stop_loss_price, symbol)})")
+                    sl_result = await self.trading_engine.place_limit_stop_for_lifecycle(
+                        lifecycle_id=lifecycle_id,
+                        symbol=symbol,
+                        sl_price=stop_loss_price,
+                        position_side=position_side,
+                        amount_to_cover=abs(current_amount) # Ensure positive amount
+                    )
                         
-                        try:
-                            # Execute market order to close position
-                            exit_result = await self.trading_engine.execute_exit_order(token) # Assumes token is sufficient for exit
-                            
-                            if exit_result.get('success'):
-                                exit_order_id = exit_result.get('order_placed_details', {}).get('exchange_order_id', 'N/A')
-                                logger.info(f"✅ Immediate {position_side.upper()} SL execution successful for {token} (Lifecycle: {lifecycle_id[:8]}). Market order {exit_order_id} placed.")
-                                # The actual lifecycle closure will be handled by _check_external_trades when this market order fill is processed.
-                                # We can mark the SL as "handled" on the lifecycle to prevent re-triggering here.
-                                stats.link_stop_loss_to_trade(lifecycle_id, f"immediate_market_exit_{exit_order_id}", stop_loss_price)
+                    if sl_result.get('success'):
+                        placed_sl_details = sl_result.get('order_placed_details', {})
+                        sl_exchange_order_id = placed_sl_details.get('exchange_order_id')
+                        sl_db_order_id = placed_sl_details.get('order_db_id')
+                        stop_loss_price_str_log = formatter.format_price_with_symbol(stop_loss_price, token)
 
-
-                                if self.notification_manager:
-                                    # Re-fetch formatted prices for notification if not already strings
-                                    current_price_str_notify = formatter.format_price_with_symbol(current_price, token) if current_price else "N/A"
-                                    stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token) if stop_loss_price else "N/A"
-                                    await self.notification_manager.send_generic_notification(
-                                        f"🚨 <b>Immediate Stop Loss Execution</b>\n\n"
-                                        f"🆕 <b>Source: Unified Trades Table</b>\n"
-                                        f"Token: {token}\n"
-                                        f"Lifecycle ID: {lifecycle_id[:8]}...\n"
-                                        f"Position Type: {position_side.upper()}\n"
-                                        f"SL Trigger Price: {stop_loss_price_str_notify}\n"
-                                        f"Current Market Price: {current_price_str_notify}\n"
-                                        f"Trigger Logic: {trigger_reason}\n"
-                                        f"Action: Market close order placed immediately\n"
-                                        f"Exit Order ID: {exit_order_id}\n"
-                                        f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                    )
-                            else:
-                                logger.error(f"❌ Failed to execute immediate market SL for {token} (Lifecycle: {lifecycle_id[:8]}): {exit_result.get('error')}")
+                        logger.info(f"✅ Successfully processed SL request for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price {stop_loss_price_str_log}, Exchange SL Order ID: {sl_exchange_order_id or 'N/A'}, DB ID: {sl_db_order_id or 'N/A'}")
                         
-                        except Exception as exec_error:
-                            logger.error(f"❌ Exception during immediate market SL execution for {token} (Lifecycle: {lifecycle_id[:8]}): {exec_error}")
-                    
-                    else:
-                        # Normal activation - place stop loss order (which creates a 'pending_trigger' in DB)
-                        try:
-                            # The execute_stop_loss_order should create the 'pending_trigger' order in the DB
-                            # and return details, including the DB ID of this 'pending_trigger' order.
-                            sl_result = await self.trading_engine.execute_stop_loss_order(
-                                token=token, 
-                                stop_price=stop_loss_price
-                                # trade_lifecycle_id_for_sl=lifecycle_id # REMOVED: TE.execute_stop_loss_order handles linking
-                            )
+                        if self.notification_manager and sl_exchange_order_id:
+                            # Fetch current price for notification context if possible
+                            current_price_for_notification = None
+                            try:
+                                market_data_notify = self.trading_engine.get_market_data(symbol)
+                                if market_data_notify and market_data_notify.get('ticker'):
+                                    current_price_for_notification = float(market_data_notify['ticker'].get('last', 0))
+                            except:
+                                pass # Ignore errors fetching price for notification
+
+                            current_price_str_notify = formatter.format_price_with_symbol(current_price_for_notification, token) if current_price_for_notification else 'Unknown'
+                            stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token)
                             
-                            if sl_result.get('success'):
-                                # The 'order_placed_details' should contain the exchange_order_id of the *actual* SL order
-                                # if it was placed directly, OR the db_id of the 'pending_trigger' order.
-                                # For this flow, execute_stop_loss_order is expected to manage the DB record for 'pending_trigger'.
-                                # We then link this 'pending_trigger' order's concept (e.g. its future exchange_order_id if known, or a reference)
-                                # to the trade lifecycle.
-                                
-                                # Assuming sl_result might give us the exchange_order_id if the SL is directly placed,
-                                # or a reference to the DB order that is now 'pending_trigger'.
-                                # TradingStats.link_stop_loss_to_trade expects the actual exchange order ID of the stop loss.
-                                # This implies execute_stop_loss_order directly places it or we have a two step.
-                                # Given _check_pending_triggers, execute_stop_loss_order likely sets up the DB record.
-                                # For now, let's assume sl_result gives an identifier for the placed SL concept.
-                                
-                                sl_exchange_order_id = sl_result.get('order_placed_details', {}).get('exchange_order_id') # This might be of the actual order or the trigger
-                                sl_db_order_id = sl_result.get('order_placed_details', {}).get('order_db_id') # ID of the order in 'orders' table
-
-                                # We need to link the trade lifecycle to the concept of the SL order.
-                                # If sl_exchange_order_id is directly available (e.g. for exchange-based SLs), use it.
-                                # If it's a DB-managed trigger, the sl_db_order_id (of 'pending_trigger' type) is key.
-                                # TradingStats.link_stop_loss_to_trade primarily expects the 'stop_loss_order_id' (exchange ID).
-                                # This part needs to be clear:
-                                # Option A: execute_stop_loss_order directly places a conditional order on exchange, returns its ID.
-                                # Option B: execute_stop_loss_order creates a 'pending_trigger' in DB. _check_pending_triggers later places it.
-                                # If B, then link_stop_loss_to_trade might need to store the DB ID of the trigger,
-                                # or wait until _check_pending_triggers successfully places it.
-                                # The current TradingStats.link_stop_loss_to_trade seems to expect an exchange_order_id.
-
-                                # For now, assume execute_stop_loss_order in TE handles creation of DB order,
-                                # and if it gets an exchange_order_id immediately (e.g. for a true stop-market order), it's returned.
-                                # If it only creates a pending_trigger, the exchange_order_id might be null initially in order_placed_details.
-
-                                if sl_exchange_order_id: # If an actual exchange order ID was returned for the SL
-                                    stats.link_stop_loss_to_trade(lifecycle_id, sl_exchange_order_id, stop_loss_price)
-                                    stop_loss_price_str_log = formatter.format_price_with_symbol(stop_loss_price, token)
-                                    logger.info(f"✅ Activated {position_side.upper()} stop loss for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price {stop_loss_price_str_log}, Exchange SL Order ID: {sl_exchange_order_id}")
-                                    if self.notification_manager:
-                                        current_price_str_notify = formatter.format_price_with_symbol(current_price, token) if current_price else 'Unknown'
-                                        stop_loss_price_str_notify = formatter.format_price_with_symbol(stop_loss_price, token)
-                                        await self.notification_manager.send_generic_notification(
-                                            f"🛑 <b>Stop Loss Activated</b>\n\n"
-                                            f"🆕 <b>Source: Unified Trades Table</b>\n"
-                                            f"Token: {token}\n"
-                                            f"Lifecycle ID: {lifecycle_id[:8]}...\n"
-                                            f"Position Type: {position_side.upper()}\n"
-                                            f"Stop Loss Price: {stop_loss_price_str_notify}\n"
-                                            f"Current Price: {current_price_str_notify}\n"
-                                            f"Exchange SL Order ID: {sl_exchange_order_id}\n" # Actual exchange order
-                                            f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                        )
-                                elif sl_db_order_id: # If a DB order (pending_trigger) was created
-                                    # If execute_stop_loss_order just creates a pending_trigger in DB,
-                                    # we should still mark the lifecycle's stop_loss_price.
-                                    # The stop_loss_order_id in the lifecycle might remain NULL until _check_pending_triggers places it.
-                                    # Or, link_stop_loss_to_trade could be adapted to also store the trigger_db_id.
-                                    # For simplicity now, assume link_stop_loss_to_trade focuses on exchange_order_id.
-                                    # We'll rely on _check_pending_triggers to eventually place it.
-                                    # The key is that the lifecycle now has stop_loss_price set.
-                                    # We need to make sure get_pending_stop_loss_activations() correctly excludes
-                                    # lifecycles where SL processing has begun (even if only a DB trigger exists).
-                                    # TradingStats.link_stop_loss_to_trade is what sets stop_loss_order_id.
-                                    # If we don't have an exchange ID yet, we can't call it.
-                                    # This means get_pending_stop_loss_activations will keep returning this trade
-                                    # until an exchange SL ID is linked.
-                                    # This suggests execute_stop_loss_order SHOULD try to place and get an ID,
-                                    # or the flow is more complex.
-                                    
-                                    # Revised Assumption: execute_stop_loss_order either:
-                                    # 1. Places SL on exchange, returns exchange_order_id -> link_stop_loss_to_trade.
-                                    # 2. Creates a 'pending_trigger' DB order AND updates lifecycle's stop_loss_order_id with a temporary ref or the trigger_db_id.
-                                    # Let's assume for now, if sl_db_order_id is returned, it means a DB trigger was set up.
-                                    # The lifecycle's stop_loss_order_id will be set once the actual exchange order is placed by _check_pending_triggers.
-                                    # This means get_pending_stop_loss_activations might need refinement.
-                                    # For now, if we have a db_order_id, we assume the process has started.
-
-                                    logger.info(f"✅ Initiated {position_side.upper()} stop loss process for {token} (Lifecycle: {lifecycle_id[:8]}): SL Price ${stop_loss_price:.4f}. DB Trigger Order ID: {sl_db_order_id}. Waiting for market trigger.")
-                                    if self.notification_manager:
-                                         await self.notification_manager.send_generic_notification(
-                                            f"🛡️ <b>Stop Loss Armed (Pending Trigger)</b>\n\n"
-                                            f"Token: {token}\n"
-                                            f"Lifecycle ID: {lifecycle_id[:8]}...\n"
-                                            f"Position Type: {position_side.upper()}\n"
-                                            f"Stop Loss Price: ${stop_loss_price:.4f}\n"
-                                            f"Current Price: ${current_price:.4f if current_price else 'Unknown'}\n"
-                                            f"Status: Monitoring market to place SL order at trigger.\n"
-                                            f"DB Trigger ID: {sl_db_order_id}\n"
-                                            f"Time: {datetime.now().strftime('%H:%M:%S')}"
-                                        )
-                                    # To prevent re-processing by get_pending_stop_loss_activations,
-                                    # we need to signify that SL setup for this price is in progress.
-                                    # This might involve setting a temporary value in trade.stop_loss_order_id
-                                    # or adding another field like 'stop_loss_status'.
-                                    # For now, this log indicates it's being handled.
-                                    # A simple way is to link it with a temporary ID.
-                                    stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_db_order_id}", stop_loss_price)
+                            await self.notification_manager.send_generic_notification(
+                                f"🛡️ <b>Stop Loss LIMIT Order Placed</b>\n\n"
+                                f"Token: {token}\n"
+                                f"Lifecycle ID: {lifecycle_id[:8]}...\n"
+                                f"Position Type: {position_side.upper()}\n"
+                                f"Stop Loss Price: {stop_loss_price_str_notify}\n"
+                                f"Amount: {formatter.format_amount(abs(current_amount), token)}\n"
+                                f"Current Price: {current_price_str_notify}\n"
+                                f"Exchange SL Order ID: {sl_exchange_order_id}\n"
+                                f"Time: {datetime.now().strftime('%H:%M:%S')}"
+                            )
+                        elif not sl_exchange_order_id:
+                             logger.warning(f"SL Limit order for {token} (Lifecycle: {lifecycle_id[:8]}) placed in DB (ID: {sl_db_order_id}) but no exchange ID returned immediately.")
 
+                    else:
+                        logger.error(f"❌ Failed to place SL limit order for {token} (Lifecycle: {lifecycle_id[:8]}): {sl_result.get('error')}")
 
-                                else: # No exchange_id and no db_id returned, but success true? Unlikely.
-                                     logger.warning(f"⚠️ Stop loss activation for {token} (Lifecycle: {lifecycle_id[:8]}) reported success but no order ID (Exchange or DB) provided.")
-                            else:
-                                logger.error(f"❌ Failed to activate SL for {token} (Lifecycle: {lifecycle_id[:8]}): {sl_result.get('error')}")
-                        
-                        except Exception as activation_error:
-                            logger.error(f"❌ Exception during SL activation for {token} (Lifecycle: {lifecycle_id[:8]}): {activation_error}")
-                
                 except Exception as trade_error:
                     logger.error(f"❌ Error processing position trade for SL activation (Lifecycle: {position_trade.get('trade_lifecycle_id','N/A')}): {trade_error}")
             

+ 147 - 75
src/trading/trading_engine.py

@@ -328,25 +328,27 @@ class TradingEngine:
             # DO NOT record trade here. MarketMonitor will handle fills.
             # action_type = self.stats.record_trade_with_enhanced_tracking(...)
             
-            if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
-                # Record the pending SL order in the orders table
-                sl_bot_order_ref_id = uuid.uuid4().hex
-                sl_order_db_id = self.stats.record_order_placed(
-                    symbol=symbol,
-                    side='sell', # SL for a long is a sell
-                    order_type='STOP_LIMIT_TRIGGER', # Indicates a conditional order that will become a limit order
-                    amount_requested=token_amount,
-                    price=stop_loss_price, # This is the trigger price, and also the limit price for the SL order
-                    bot_order_ref_id=sl_bot_order_ref_id,
-                    status='pending_trigger',
-                    parent_bot_order_ref_id=bot_order_ref_id # Link to the main buy order
-                )
-                if sl_order_db_id:
-                    logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
-                else:
-                    logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
+            # MODIFICATION: Remove SL order recording at this stage. SL price is stored with lifecycle.
+            # if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
+            #     # Record the pending SL order in the orders table
+            #     sl_bot_order_ref_id = uuid.uuid4().hex
+            #     sl_order_db_id = self.stats.record_order_placed(
+            #         symbol=symbol,
+            #         side='sell', # SL for a long is a sell
+            #         order_type='STOP_LIMIT_TRIGGER', # Indicates a conditional order that will become a limit order
+            #         amount_requested=token_amount,
+            #         price=stop_loss_price, # This is the trigger price, and also the limit price for the SL order
+            #         bot_order_ref_id=sl_bot_order_ref_id,
+            #         status='pending_trigger',
+            #         parent_bot_order_ref_id=bot_order_ref_id # Link to the main buy order
+            #     )
+            #     if sl_order_db_id:
+            #         logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
+            #     else:
+            #         logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
             
             # 🆕 PHASE 4: Create trade lifecycle for this entry order
+            lifecycle_id = None # Initialize lifecycle_id
             if exchange_oid:
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 if entry_order_record:
@@ -354,21 +356,17 @@ class TradingEngine:
                         symbol=symbol,
                         side='buy',
                         entry_order_id=exchange_oid,  # Store exchange order ID
-                        stop_loss_price=stop_loss_price,
+                        stop_loss_price=stop_loss_price, # Store SL price with lifecycle
+                        take_profit_price=None, # Assuming TP is handled separately or not in this command
                         trade_type='bot'
                     )
                     
-                    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)
-                        # sl_order_db_id is the database ID of the pending 'STOP_LIMIT_TRIGGER' order recorded earlier.
-                        if sl_order_db_id: # Ensure the SL order was successfully recorded and we have its DB ID
-                            self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
-                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for BUY {symbol}")
-                        else:
-                            logger.error(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol} with stop_loss_price, but could not link pending SL ID as sl_order_db_id was not available from earlier recording step.")
-                    elif lifecycle_id:
-                        logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol}")
+                    if lifecycle_id:
+                        logger.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol} with SL price {stop_loss_price if stop_loss_price else 'N/A'}")
+                        # MODIFICATION: Do not link a pending DB trigger SL here.
+                        # if stop_loss_price and sl_order_db_id: # sl_order_db_id is no longer created here
+                        #    self.stats.link_stop_loss_to_trade(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
+                        #    logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for BUY {symbol}")
             
             return {
                 "success": True,
@@ -385,7 +383,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_activation": stop_loss_price is not None, # Indicates SL needs to be placed after fill
+                "stop_loss_price_if_pending": stop_loss_price,
                 "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
         except ZeroDivisionError as e:
@@ -477,25 +476,27 @@ class TradingEngine:
             # DO NOT record trade here. MarketMonitor will handle fills.
             # action_type = self.stats.record_trade_with_enhanced_tracking(...)
             
-            if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
-                # Record the pending SL order in the orders table
-                sl_bot_order_ref_id = uuid.uuid4().hex
-                sl_order_db_id = self.stats.record_order_placed(
-                    symbol=symbol,
-                    side='buy', # SL for a short is a buy
-                    order_type='STOP_LIMIT_TRIGGER', 
-                    amount_requested=token_amount,
-                    price=stop_loss_price, 
-                    bot_order_ref_id=sl_bot_order_ref_id,
-                    status='pending_trigger',
-                    parent_bot_order_ref_id=bot_order_ref_id # Link to the main sell order
-                )
-                if sl_order_db_id:
-                    logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
-                else:
-                    logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
+            # MODIFICATION: Remove SL order recording at this stage. SL price is stored with lifecycle.
+            # if stop_loss_price and exchange_oid and exchange_oid != 'N/A':
+            #     # Record the pending SL order in the orders table
+            #     sl_bot_order_ref_id = uuid.uuid4().hex
+            #     sl_order_db_id = self.stats.record_order_placed(
+            #         symbol=symbol,
+            #         side='buy', # SL for a short is a buy
+            #         order_type='STOP_LIMIT_TRIGGER', 
+            #         amount_requested=token_amount,
+            #         price=stop_loss_price, 
+            #         bot_order_ref_id=sl_bot_order_ref_id,
+            #         status='pending_trigger',
+            #         parent_bot_order_ref_id=bot_order_ref_id # Link to the main sell order
+            #     )
+            #     if sl_order_db_id:
+            #         logger.info(f"Pending Stop Loss order recorded in DB: ID {sl_order_db_id}, BotRef {sl_bot_order_ref_id}, ParentBotRef {bot_order_ref_id}")
+            #     else:
+            #         logger.error(f"Failed to record pending SL order in DB for parent BotRef {bot_order_ref_id}")
             
             # 🆕 PHASE 4: Create trade lifecycle for this entry order
+            lifecycle_id = None # Initialize lifecycle_id
             if exchange_oid:
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 if entry_order_record:
@@ -503,20 +504,18 @@ class TradingEngine:
                         symbol=symbol,
                         side='sell',
                         entry_order_id=exchange_oid,  # Store exchange order ID
-                        stop_loss_price=stop_loss_price,
+                        stop_loss_price=stop_loss_price, # Store SL price with lifecycle
+                        take_profit_price=None, # Assuming TP is handled separately
                         trade_type='bot'
                     )
                     
-                    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(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
-                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for SELL {symbol}")
-                        else:
-                            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}")
+                    if lifecycle_id:
+                        logger.info(f"📊 Created trade lifecycle {lifecycle_id} for SELL {symbol} with SL price {stop_loss_price if stop_loss_price else 'N/A'}")
+                        # MODIFICATION: Do not link a pending DB trigger SL here.
+                        # 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(lifecycle_id, f"pending_db_trigger_{sl_order_db_id}", stop_loss_price)
+                        #    logger.info(f"📊 Created trade lifecycle {lifecycle_id} and linked pending SL (ID: pending_db_trigger_{sl_order_db_id}) for SELL {symbol}")
             
             return {
                 "success": True,
@@ -531,7 +530,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_activation": stop_loss_price is not None, # Indicates SL needs to be placed after fill
+                "stop_loss_price_if_pending": stop_loss_price,
                 "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
         except ZeroDivisionError as e:
@@ -658,34 +658,33 @@ class TradingEngine:
             elif position_type == "SHORT" and stop_price <= entry_price:
                 return {"success": False, "error": "Stop loss price should be above entry price for short positions"}
             
-            order_type_for_stats = 'stop_market' # Changed from 'limit'
+            order_type_for_stats = 'limit' # MODIFICATION: SL from /sl command is now a direct limit order
 
             # 1. Generate bot_order_ref_id and record order placement intent
             bot_order_ref_id = uuid.uuid4().hex
-            # For stop_market, the 'price' recorded is the trigger price.
+            # For a direct limit SL, the 'price' recorded is the limit price.
             order_db_id = self.stats.record_order_placed(
                 symbol=symbol, side=exit_side, order_type=order_type_for_stats,
-                amount_requested=contracts, price=stop_price, # price here is the trigger price
+                amount_requested=contracts, price=stop_price, # price here is the limit price for the SL
                 bot_order_ref_id=bot_order_ref_id, status='pending_submission'
             )
 
             if not order_db_id:
-                logger.error(f"Failed to record SL order intent in DB for {symbol} with bot_ref_id {bot_order_ref_id}")
-                return {"success": False, "error": "Failed to record SL order intent in database."}
+                logger.error(f"Failed to record SL limit order intent in DB for {symbol} (direct /sl command) with bot_ref {bot_order_ref_id}")
+                return {"success": False, "error": "Failed to record SL limit order intent in database."}
 
-            # 2. Place stop-market order using the updated client method
-            logger.info(f"Placing STOP LOSS (STOP MARKET {exit_side.upper()}) order ({bot_order_ref_id}) for {formatter.format_amount(contracts, token)} {symbol} at trigger {formatter.format_price_with_symbol(stop_price, token)}")
-            # The client method now expects 'stop_price_arg' for the trigger price.
-            exchange_order_data, error_msg = self.client.place_stop_loss_order(symbol, exit_side, contracts, stop_price_arg=stop_price)
+            # 2. Place a direct LIMIT order for the stop loss
+            logger.info(f"Placing direct LIMIT STOP LOSS ({exit_side.upper()}) order ({bot_order_ref_id}) for {formatter.format_amount(contracts, token)} {symbol} at limit price {formatter.format_price_with_symbol(stop_price, token)}")
+            exchange_order_data, error_msg = self.client.place_limit_order(symbol, exit_side, contracts, price=stop_price)
             
             if error_msg:
-                logger.error(f"Stop loss order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
+                logger.error(f"Direct SL Limit order placement failed for {symbol} ({bot_order_ref_id}): {error_msg}")
                 self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
-                return {"success": False, "error": f"Stop loss order placement failed: {error_msg}"}
+                return {"success": False, "error": f"Direct SL Limit order placement failed: {error_msg}"}
             if not exchange_order_data:
-                logger.error(f"Stop loss order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data and no error.")
+                logger.error(f"Direct SL Limit order placement call failed for {symbol} ({bot_order_ref_id}). Client returned no data/error.")
                 self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
-                return {"success": False, "error": "Stop loss order placement failed (no order object or error)."}
+                return {"success": False, "error": "Direct SL Limit order placement failed (no order object or error from client)."}
 
             exchange_oid = exchange_order_data.get('id')
             
@@ -693,24 +692,28 @@ class TradingEngine:
             if exchange_oid:
                 self.stats.update_order_status(
                     order_db_id=order_db_id, 
-                    new_status='open', # SL/TP limit orders are 'open' until triggered/filled
+                    new_status='open', # Limit orders are 'open' until filled
                     set_exchange_order_id=exchange_oid
                 )
             else:
-                logger.warning(f"No exchange_order_id received for SL order {order_db_id} ({bot_order_ref_id}).")
+                logger.warning(f"No exchange_order_id received for SL limit order {order_db_id} ({bot_order_ref_id}).")
             
             # NOTE: Stop loss orders are protective orders for existing positions
             # They do not create new trade cycles - they protect existing trade cycles
             
             # Fetch the lifecycle_id for the current position
             lifecycle_id = None
+            active_trade_lc = None # Define active_trade_lc to ensure it's available for the if block
             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
+                        # Ensure that if an old SL (e.g. stop-market) existed and is being replaced,
+                        # it might need to be cancelled first. However, current flow assumes this /sl places a new/updated one.
+                        # For simplicity, link_stop_loss_to_trade will update if one already exists or insert.
                         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}")
+                        logger.info(f"🛡️ Linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol} (from /sl command)")
             
             return {
                 "success": True,
@@ -952,6 +955,75 @@ class TradingEngine:
             logger.error(f"Error in execute_coo_order for {token}: {e}")
             return {"success": False, "error": str(e)}
     
+    async def place_limit_stop_for_lifecycle(self, lifecycle_id: str, symbol: str, sl_price: float, position_side: str, amount_to_cover: float) -> Dict[str, Any]:
+        """Places a limit stop-loss order for an active trade lifecycle."""
+        formatter = get_formatter()
+        token = symbol.split('/')[0] if '/' in symbol else symbol
+
+        if not all([lifecycle_id, symbol, sl_price > 0, position_side in ['long', 'short'], amount_to_cover > 0]):
+            err_msg = f"Invalid parameters for place_limit_stop_for_lifecycle: lc_id={lifecycle_id}, sym={symbol}, sl_price={sl_price}, pos_side={position_side}, amt={amount_to_cover}"
+            logger.error(err_msg)
+            return {"success": False, "error": err_msg}
+
+        sl_order_side = 'sell' if position_side == 'long' else 'buy'
+        order_type_for_stats = 'limit' # Explicitly a limit order
+
+        # 1. Generate bot_order_ref_id and record order placement intent
+        bot_order_ref_id = uuid.uuid4().hex
+        order_db_id = self.stats.record_order_placed(
+            symbol=symbol, side=sl_order_side, order_type=order_type_for_stats,
+            amount_requested=amount_to_cover, price=sl_price, 
+            bot_order_ref_id=bot_order_ref_id, status='pending_submission'
+        )
+
+        if not order_db_id:
+            msg = f"Failed to record SL limit order intent in DB for {symbol} (Lifecycle: {lifecycle_id}) with bot_ref {bot_order_ref_id}"
+            logger.error(msg)
+            return {"success": False, "error": msg}
+
+        # 2. Place the limit order on the exchange
+        logger.info(f"Placing LIMIT STOP LOSS ({sl_order_side.upper()}) for lifecycle {lifecycle_id[:8]} ({bot_order_ref_id}): {formatter.format_amount(amount_to_cover, token)} {symbol} @ {formatter.format_price_with_symbol(sl_price, token)}")
+        exchange_order_data, error_msg = self.client.place_limit_order(symbol, sl_order_side, amount_to_cover, sl_price)
+
+        if error_msg:
+            logger.error(f"SL Limit order placement failed for {symbol} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}): {error_msg}")
+            self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission', bot_order_ref_id=bot_order_ref_id)
+            return {"success": False, "error": f"SL Limit order placement failed: {error_msg}"}
+        if not exchange_order_data:
+            logger.error(f"SL Limit order placement call failed for {symbol} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}). Client returned no data/error.")
+            self.stats.update_order_status(order_db_id=order_db_id, new_status='failed_submission_no_data', bot_order_ref_id=bot_order_ref_id)
+            return {"success": False, "error": "SL Limit order placement failed (no order object or error from client)."}
+
+        exchange_oid = exchange_order_data.get('id')
+
+        # 3. Update order in DB with exchange_order_id and status
+        if exchange_oid:
+            self.stats.update_order_status(
+                order_db_id=order_db_id, 
+                new_status='open', # Limit orders are 'open' until filled
+                set_exchange_order_id=exchange_oid
+            )
+            # 4. Link this exchange SL order to the trade lifecycle
+            self.stats.link_stop_loss_to_trade(lifecycle_id, exchange_oid, sl_price)
+            logger.info(f"🛡️ Successfully placed and linked SL limit order {exchange_oid} to lifecycle {lifecycle_id} for {symbol}")
+        else:
+            logger.warning(f"No exchange_order_id received for SL limit order {order_db_id} ({bot_order_ref_id}, LC: {lifecycle_id[:8]}). Status remains pending_submission.")
+
+        return {
+            "success": True,
+            "order_placed_details": {
+                "bot_order_ref_id": bot_order_ref_id,
+                "exchange_order_id": exchange_oid,
+                "order_db_id": order_db_id,
+                "symbol": symbol,
+                "side": sl_order_side,
+                "type": order_type_for_stats,
+                "amount_requested": amount_to_cover,
+                "price_requested": sl_price
+            },
+            "trade_lifecycle_id": lifecycle_id
+        }
+    
     def is_bot_trade(self, exchange_order_id: str) -> bool:
         """Check if an order (by its exchange ID) was recorded by this bot in the orders table."""
         if not self.stats:

+ 1 - 1
trading_bot.py

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