|
@@ -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:
|