Procházet zdrojové kódy

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 před 2 dny
rodič
revize
a37ed896e4

+ 1 - 1
src/clients/hyperliquid_client.py

@@ -490,7 +490,7 @@ class HyperliquidClient:
                 'market',  # Order type to execute when triggered
                 'market',  # Order type to execute when triggered
                 side, 
                 side, 
                 amount, 
                 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
                 params=trigger_params
             )
             )
             
             

+ 66 - 172
src/monitoring/market_monitor.py

@@ -1370,191 +1370,85 @@ class MarketMonitor:
             for position_trade in trades_needing_sl:
             for position_trade in trades_needing_sl:
                 try:
                 try:
                     symbol = position_trade['symbol']
                     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']
                     stop_loss_price = position_trade['stop_loss_price']
                     position_side = position_trade['position_side'] # 'long' or 'short'
                     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']
                     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:
                 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}")
                     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.
             # DO NOT record trade here. MarketMonitor will handle fills.
             # action_type = self.stats.record_trade_with_enhanced_tracking(...)
             # 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
             # 🆕 PHASE 4: Create trade lifecycle for this entry order
+            lifecycle_id = None # Initialize lifecycle_id
             if exchange_oid:
             if exchange_oid:
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 if entry_order_record:
                 if entry_order_record:
@@ -354,21 +356,17 @@ class TradingEngine:
                         symbol=symbol,
                         symbol=symbol,
                         side='buy',
                         side='buy',
                         entry_order_id=exchange_oid,  # Store exchange order ID
                         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'
                         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 {
             return {
                 "success": True,
                 "success": True,
@@ -385,7 +383,8 @@ class TradingEngine:
                 # "action_type": action_type, # Removed as trade is not recorded here
                 # "action_type": action_type, # Removed as trade is not recorded here
                 "token_amount": token_amount,
                 "token_amount": token_amount,
                 # "actual_price": final_price_for_stats, # Removed as fill is not processed here
                 # "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
                 "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
             }
         except ZeroDivisionError as e:
         except ZeroDivisionError as e:
@@ -477,25 +476,27 @@ class TradingEngine:
             # DO NOT record trade here. MarketMonitor will handle fills.
             # DO NOT record trade here. MarketMonitor will handle fills.
             # action_type = self.stats.record_trade_with_enhanced_tracking(...)
             # 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
             # 🆕 PHASE 4: Create trade lifecycle for this entry order
+            lifecycle_id = None # Initialize lifecycle_id
             if exchange_oid:
             if exchange_oid:
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 entry_order_record = self.stats.get_order_by_exchange_id(exchange_oid)
                 if entry_order_record:
                 if entry_order_record:
@@ -503,20 +504,18 @@ class TradingEngine:
                         symbol=symbol,
                         symbol=symbol,
                         side='sell',
                         side='sell',
                         entry_order_id=exchange_oid,  # Store exchange order ID
                         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'
                         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 {
             return {
                 "success": True,
                 "success": True,
@@ -531,7 +530,8 @@ class TradingEngine:
                     "price_requested": order_placement_price if order_type_for_stats == 'limit' else None
                     "price_requested": order_placement_price if order_type_for_stats == 'limit' else None
                 },
                 },
                 "token_amount": token_amount,
                 "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
                 "trade_lifecycle_id": lifecycle_id if 'lifecycle_id' in locals() else None # Return lifecycle_id
             }
             }
         except ZeroDivisionError as e:
         except ZeroDivisionError as e:
@@ -658,34 +658,33 @@ class TradingEngine:
             elif position_type == "SHORT" and stop_price <= entry_price:
             elif position_type == "SHORT" and stop_price <= entry_price:
                 return {"success": False, "error": "Stop loss price should be above entry price for short positions"}
                 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
             # 1. Generate bot_order_ref_id and record order placement intent
             bot_order_ref_id = uuid.uuid4().hex
             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(
             order_db_id = self.stats.record_order_placed(
                 symbol=symbol, side=exit_side, order_type=order_type_for_stats,
                 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'
                 bot_order_ref_id=bot_order_ref_id, status='pending_submission'
             )
             )
 
 
             if not order_db_id:
             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:
             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)
                 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:
             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)
                 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')
             exchange_oid = exchange_order_data.get('id')
             
             
@@ -693,24 +692,28 @@ class TradingEngine:
             if exchange_oid:
             if exchange_oid:
                 self.stats.update_order_status(
                 self.stats.update_order_status(
                     order_db_id=order_db_id, 
                     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
                     set_exchange_order_id=exchange_oid
                 )
                 )
             else:
             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
             # NOTE: Stop loss orders are protective orders for existing positions
             # They do not create new trade cycles - they protect existing trade cycles
             # They do not create new trade cycles - they protect existing trade cycles
             
             
             # Fetch the lifecycle_id for the current position
             # Fetch the lifecycle_id for the current position
             lifecycle_id = None
             lifecycle_id = None
+            active_trade_lc = None # Define active_trade_lc to ensure it's available for the if block
             if self.stats:
             if self.stats:
                 active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
                 active_trade_lc = self.stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
                 if active_trade_lc:
                 if active_trade_lc:
                     lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
                     lifecycle_id = active_trade_lc.get('trade_lifecycle_id')
                     if exchange_oid: # If SL order placed successfully on exchange
                     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)
                         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 {
             return {
                 "success": True,
                 "success": True,
@@ -952,6 +955,75 @@ class TradingEngine:
             logger.error(f"Error in execute_coo_order for {token}: {e}")
             logger.error(f"Error in execute_coo_order for {token}: {e}")
             return {"success": False, "error": str(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:
     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."""
         """Check if an order (by its exchange ID) was recorded by this bot in the orders table."""
         if not self.stats:
         if not self.stats:

+ 1 - 1
trading_bot.py

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