|
@@ -774,7 +774,7 @@ class MarketMonitor:
|
|
|
logger.error(f"❌ Error in automatic risk management check: {e}", exc_info=True)
|
|
|
|
|
|
async def _cleanup_orphaned_stop_losses(self):
|
|
|
- """Clean up pending stop losses that no longer have corresponding positions."""
|
|
|
+ """Clean up pending stop losses that no longer have corresponding positions OR whose parent orders have been cancelled/failed."""
|
|
|
try:
|
|
|
stats = self.trading_engine.get_stats()
|
|
|
if not stats:
|
|
@@ -804,9 +804,40 @@ class MarketMonitor:
|
|
|
for sl_order in pending_stop_losses:
|
|
|
symbol = sl_order.get('symbol')
|
|
|
order_db_id = sl_order.get('id')
|
|
|
+ parent_bot_ref_id = sl_order.get('parent_bot_order_ref_id')
|
|
|
|
|
|
- if symbol not in position_symbols:
|
|
|
- # This stop loss has no corresponding position - cancel it
|
|
|
+ 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()
|
|
|
+
|
|
|
+ # Cancel if parent order failed, was cancelled, or disappeared
|
|
|
+ if parent_status in ['failed_submission', 'failed_submission_no_data', 'cancelled_manually',
|
|
|
+ 'cancelled_externally', 'disappeared_from_exchange']:
|
|
|
+ should_cancel = True
|
|
|
+ cancel_reason = f"parent order {parent_status}"
|
|
|
+ elif parent_status == 'filled':
|
|
|
+ # Parent order filled but no position - position might have been closed externally
|
|
|
+ if symbol not in position_symbols:
|
|
|
+ should_cancel = True
|
|
|
+ cancel_reason = "parent filled but position no longer exists"
|
|
|
+ # If parent is still 'open', 'submitted', or 'partially_filled', keep the stop loss
|
|
|
+ else:
|
|
|
+ # Parent order not found in DB - this is truly orphaned
|
|
|
+ should_cancel = True
|
|
|
+ cancel_reason = "parent order not found in database"
|
|
|
+ else:
|
|
|
+ # No parent reference - fallback to old logic (position-based check)
|
|
|
+ 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_no_position'
|
|
@@ -815,7 +846,7 @@ class MarketMonitor:
|
|
|
if success:
|
|
|
orphaned_count += 1
|
|
|
token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
|
|
|
- logger.info(f"🧹 Cancelled orphaned stop loss for {token} (Order DB ID: {order_db_id}) - no position exists")
|
|
|
+ logger.info(f"🧹 Cancelled orphaned stop loss for {token} (Order DB ID: {order_db_id}) - {cancel_reason}")
|
|
|
|
|
|
if orphaned_count > 0:
|
|
|
logger.info(f"🧹 Cleanup completed: Cancelled {orphaned_count} orphaned stop loss orders")
|
|
@@ -824,9 +855,9 @@ class MarketMonitor:
|
|
|
await self.notification_manager.send_generic_notification(
|
|
|
f"🧹 <b>Cleanup Completed</b>\n\n"
|
|
|
f"Cancelled {orphaned_count} orphaned stop loss order(s)\n"
|
|
|
- f"Reason: No corresponding positions found\n"
|
|
|
+ f"Reason: Parent orders cancelled/failed or positions closed externally\n"
|
|
|
f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
|
|
|
- f"💡 This automatic cleanup ensures stop losses stay synchronized with actual positions."
|
|
|
+ f"💡 This automatic cleanup ensures stop losses stay synchronized with actual orders and positions."
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|