瀏覽代碼

Increment BOT_VERSION to 2.2.134 and enhance MarketMonitor and TradingEngine with improved stop loss handling.

- Updated BOT_VERSION for the upcoming release.
- Refined logic in MarketMonitor for handling orphaned stop loss orders, improving cancellation criteria and logging for better traceability.
- Enhanced TradingEngine to ensure proper linking of stop loss orders to trades, with improved error handling and logging for missing order IDs.
Carles Sentis 2 天之前
父節點
當前提交
e96259f3e5
共有 3 個文件被更改,包括 31 次插入26 次删除
  1. 23 19
      src/monitoring/market_monitor.py
  2. 7 6
      src/trading/trading_engine.py
  3. 1 1
      trading_bot.py

+ 23 - 19
src/monitoring/market_monitor.py

@@ -1222,7 +1222,6 @@ class MarketMonitor:
             if not stats:
                 return
 
-            # Get all pending stop loss triggers
             pending_stop_losses = stats.get_orders_by_status('pending_trigger', 'stop_limit_trigger')
             
             if not pending_stop_losses:
@@ -1230,18 +1229,16 @@ class MarketMonitor:
 
             logger.debug(f"Checking {len(pending_stop_losses)} pending stop losses for orphaned orders")
 
-            # Get current positions to check against
-            current_positions = self.cached_positions or [] # Use cache
+            current_positions = self.cached_positions or [] 
             position_symbols = set()
             
-            if current_positions: # No need for 'or []' here as it's handled by default [] from cache
+            if current_positions:
                 for pos in current_positions:
                     symbol = pos.get('symbol')
                     contracts = float(pos.get('contracts', 0))
                     if symbol and contracts != 0:
                         position_symbols.add(symbol)
 
-            # Check each pending stop loss
             orphaned_count = 0
             for sl_order in pending_stop_losses:
                 symbol = sl_order.get('symbol')
@@ -1251,12 +1248,10 @@ class MarketMonitor:
                 should_cancel = False
                 cancel_reason = ""
                 
-                # Check if parent order exists and its status
                 if parent_bot_ref_id:
                     parent_order = stats.get_order_by_bot_ref_id(parent_bot_ref_id)
                     if parent_order:
                         parent_status = parent_order.get('status', '').lower()
-                        parent_order_id = parent_order.get('id') # DB ID of parent order
                         
                         if parent_order.get('exchange_order_id'):
                             entry_oid = parent_order['exchange_order_id']
@@ -1269,7 +1264,6 @@ class MarketMonitor:
                                     should_cancel = True
                                     cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and lifecycle explicitly cancelled"
                                 elif not lc_pending and not lc_opened:
-                                    # No pending or opened lifecycle, and not explicitly cancelled. Implies order didn't lead to an active trade.
                                     should_cancel = True
                                     cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no active/pending/cancelled lifecycle found"
                                 else:
@@ -1280,35 +1274,44 @@ class MarketMonitor:
                                     should_cancel = False
                             
                             elif parent_status in ['failed_submission', 'failed_submission_no_data', 'cancelled_manually', 'disappeared_from_exchange']:
-                                if not lc_opened: # If no 'position_opened' lifecycle, safe to cancel SL
+                                if not lc_opened: 
                                     should_cancel = True
                                     cancel_reason = f"parent order ({entry_oid[:6]}...) {parent_status} and no 'position_opened' lifecycle"
                                 else:
                                     logger.info(f"SL {order_db_id} for parent {parent_bot_ref_id} (status {parent_status}) - lifecycle is 'position_opened'. SL not cancelled by this rule.")
                                     should_cancel = False
                             elif parent_status == 'filled':
-                                if symbol not in position_symbols: # Position itself is gone
+                                if symbol not in position_symbols: 
                                     should_cancel = True
                                     cancel_reason = "parent filled but actual position no longer exists"
-                                # else: SL is valid for the open position
-                            else: # Parent order does not have an exchange_order_id, should not happen for entry orders
+                            # If parent_status is 'open' and has exchange_order_id, it falls through, should_cancel remains False (correct).
+                            
+                        else: # Parent order does not have an exchange_order_id in the fetched DB record
+                            # Possible states for parent_status here: 'pending_submission', 'open' (anomalous), or other non-live states.
+                            if parent_status in ['open', 'pending_submission', 'submitted']: # 'submitted' can be another pre-fill active state
+                                logger.info(f"SL Cleanup: Parent order {parent_order.get('id')} (status '{parent_status}') is missing exchange_id in DB record. SL {sl_order.get('id')} will NOT be cancelled by this rule, assuming parent is still active or pending.")
+                                should_cancel = False # Preserve SL if parent is in an active or attempting-to-be-active state
+                                if parent_status == 'open': # This specific case is a data anomaly
+                                    logger.warning(f"SL Cleanup: DATA ANOMALY - Parent order {parent_order.get('id')} status is 'open' but fetched DB record shows no exchange_id. Investigate DB state for order {parent_order.get('id')}.")
+                            else:
+                                # Parent is in a non-live/non-pending status (e.g., 'failed_submission' before getting an ID)
+                                # and never got an exchange_id. It's likely safe to cancel the SL.
                                 should_cancel = True
-                                cancel_reason = f"parent order {parent_status} but no exchange_order_id to check lifecycle"
-                        else: # Parent order not found in DB - this is truly orphaned
-                            should_cancel = True
-                            cancel_reason = "parent order not found in database"
+                                cancel_reason = f"parent order {parent_status} (no exch_id) and status indicates it's not live/pending."
+                    else: # Parent order not found in DB by bot_order_ref_id
+                        should_cancel = True
+                        cancel_reason = "parent order not found in database"
                 else:
-                    # No parent reference - fallback to old logic (position-based check)
+                    # No parent_bot_ref_id on the SL order itself.
                     # This SL is not tied to a bot order, so cancel if no position for the symbol.
                     if symbol not in position_symbols:
                         should_cancel = True
                         cancel_reason = "no position exists and no parent reference"
                 
                 if should_cancel:
-                    # Cancel this orphaned stop loss
                     success = stats.update_order_status(
                         order_db_id=order_db_id,
-                        new_status='cancelled_orphaned' # Generic orphaned status
+                        new_status='cancelled_orphaned'
                     )
                     
                     if success:
@@ -1331,6 +1334,7 @@ class MarketMonitor:
         except Exception as e:
             logger.error(f"❌ Error cleaning up orphaned stop losses: {e}", exc_info=True)
 
+
     async def _activate_pending_stop_losses_from_trades(self):
         """🆕 PHASE 4: Check trades table for pending stop loss activation first (highest priority)"""
         try:

+ 7 - 6
src/trading/trading_engine.py

@@ -361,11 +361,12 @@ class TradingEngine:
                     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, exchange_oid, stop_loss_price)
-                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} with linked stop loss for BUY {symbol}")
+                        # 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.info(f"📊 Created trade lifecycle {lifecycle_id} for BUY {symbol}")
+                            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}")
             
@@ -510,8 +511,8 @@ class TradingEngine:
                         # 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, exchange_oid, stop_loss_price)
-                            logger.info(f"📊 Created trade lifecycle {lifecycle_id} with linked stop loss for SELL {symbol}")
+                            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:

+ 1 - 1
trading_bot.py

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