Răsfoiți Sursa

Enhance stop loss cancellation logic in MarketMonitor - Implemented edge case handling to wait for potential fills before cancelling stop losses. Added checks for recent fills to prevent unnecessary cancellations, improving order management accuracy. Enhanced logging for better traceability of order statuses and actions taken.

Carles Sentis 3 zile în urmă
părinte
comite
5321ab723d
1 a modificat fișierele cu 99 adăugiri și 5 ștergeri
  1. 99 5
      src/monitoring/market_monitor.py

+ 99 - 5
src/monitoring/market_monitor.py

@@ -309,12 +309,36 @@ class MarketMonitor:
                                 f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
                                 f"🤖 Bot status updated automatically"
                             )
+                            
+                        # 🔧 EDGE CASE FIX: Wait briefly before cancelling stop losses 
+                        # Sometimes orders are cancelled externally but fills come through simultaneously
+                        logger.info(f"⏳ Waiting 3 seconds to check for potential fills before cancelling stop losses for {exchange_oid}")
+                        await asyncio.sleep(3)
+                        
+                        # Re-check the order status after waiting - it might have been filled
+                        order_in_db_updated = stats.get_order_by_exchange_id(exchange_oid)
+                        if order_in_db_updated and order_in_db_updated.get('status') in ['filled', 'partially_filled']:
+                            logger.info(f"✅ Order {exchange_oid} was filled during the wait period - NOT cancelling stop losses")
+                            # Don't cancel stop losses - let them be activated normally
+                            continue
+                            
+                        # Additional check: Look for very recent fills that might match this order
+                        recent_fill_found = await self._check_for_recent_fills_for_order(exchange_oid, order_in_db)
+                        if recent_fill_found:
+                            logger.info(f"✅ Found recent fill for order {exchange_oid} - NOT cancelling stop losses")
+                            continue
                     else:
                         # Normal completion/cancellation - update status
                         stats.update_order_status(exchange_order_id=exchange_oid, new_status='disappeared_from_exchange')
                         
-                    # Cancel any pending stop losses linked to this order
+                    # Cancel any pending stop losses linked to this order (only if not filled)
                     if order_in_db.get('bot_order_ref_id'):
+                        # Double-check one more time that the order wasn't filled
+                        final_order_check = stats.get_order_by_exchange_id(exchange_oid)
+                        if final_order_check and final_order_check.get('status') in ['filled', 'partially_filled']:
+                            logger.info(f"🛑 Order {exchange_oid} was filled - preserving stop losses")
+                            continue
+                            
                         cancelled_sl_count = stats.cancel_linked_orders(
                             parent_bot_order_ref_id=order_in_db['bot_order_ref_id'],
                             new_status='cancelled_parent_disappeared'
@@ -348,12 +372,11 @@ class MarketMonitor:
                         f"Source: Direct cancellation on Hyperliquid\n"
                         f"Linked Stop Losses Cancelled: {total_linked_cancelled}\n"
                         f"Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
-                        f"🤖 All bot tracking has been updated automatically\n"
-                        f"💡 Use /orders to verify current order status"
+                        f"💡 Check individual orders for details"
                     )
 
         except Exception as e:
-            logger.error(f"❌ Error processing disappeared bot orders: {e}", exc_info=True)
+            logger.error(f"❌ Error processing disappeared orders: {e}", exc_info=True)
     
     async def _check_price_alarms(self):
         """Check price alarms and trigger notifications."""
@@ -1027,4 +1050,75 @@ class MarketMonitor:
                     # In case of error, still try to activate (safer to have redundant SLs than none)
             
         except Exception as e:
-            logger.error(f"Error in _activate_pending_stop_losses: {e}", exc_info=True) 
+            logger.error(f"Error in _activate_pending_stop_losses: {e}", exc_info=True) 
+
+    async def _check_for_recent_fills_for_order(self, exchange_oid, order_in_db):
+        """Check for very recent fills that might match this order."""
+        try:
+            # Get recent fills from exchange
+            recent_fills = self.trading_engine.get_recent_fills()
+            if not recent_fills:
+                return False
+
+            # Get last processed timestamp from database
+            if not hasattr(self, '_last_processed_trade_time') or self._last_processed_trade_time is None:
+                try:
+                    last_time_str = self.trading_engine.stats._get_metadata('last_processed_trade_time')
+                    if last_time_str:
+                        self._last_processed_trade_time = datetime.fromisoformat(last_time_str)
+                        logger.debug(f"Loaded last_processed_trade_time from DB: {self._last_processed_trade_time}")
+                    else:
+                        # If no last processed time, start from 1 hour ago to avoid processing too much history
+                        self._last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
+                        logger.info("No last_processed_trade_time found, setting to 1 hour ago (UTC).")
+                except Exception as e:
+                    logger.warning(f"Could not load last_processed_trade_time from DB: {e}")
+                    self._last_processed_trade_time = datetime.now(timezone.utc) - timedelta(hours=1)
+
+            # Process new fills
+            for fill in recent_fills:
+                try:
+                    # Parse fill data - CCXT format from fetch_my_trades
+                    trade_id = fill.get('id')  # CCXT uses 'id' for trade ID
+                    timestamp_ms = fill.get('timestamp')  # CCXT uses 'timestamp' (milliseconds)
+                    symbol = fill.get('symbol')  # CCXT uses 'symbol' in full format like 'LTC/USDC:USDC'
+                    side = fill.get('side')  # CCXT uses 'side' ('buy' or 'sell')
+                    amount = float(fill.get('amount', 0))  # CCXT uses 'amount'
+                    price = float(fill.get('price', 0))  # CCXT uses 'price'
+                    
+                    # Convert timestamp
+                    if timestamp_ms:
+                        timestamp_dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
+                    else:
+                        timestamp_dt = datetime.now(timezone.utc)
+                    
+                    # Skip if already processed
+                    if timestamp_dt <= self._last_processed_trade_time:
+                        continue
+                    
+                    # Process as external trade if we reach here
+                    if symbol and side and amount > 0 and price > 0:
+                        # Symbol is already in full format for CCXT
+                        full_symbol = symbol
+                        token = symbol.split('/')[0] if '/' in symbol else symbol.split(':')[0]
+                        
+                        # Check if this might be a bot order fill by looking for exchange order ID
+                        # CCXT might have this in 'info' sub-object with the raw exchange data
+                        exchange_order_id_from_fill = None
+                        if 'info' in fill and isinstance(fill['info'], dict):
+                            # Look for Hyperliquid order ID in the raw response
+                            exchange_order_id_from_fill = fill['info'].get('oid')
+                        
+                        if exchange_order_id_from_fill == exchange_oid:
+                            logger.info(f"✅ Found recent fill for order {exchange_oid} - NOT cancelling stop losses")
+                            return True
+                
+                except Exception as e:
+                    logger.error(f"Error processing fill {fill}: {e}")
+                    continue
+            
+            return False
+
+        except Exception as e:
+            logger.error(f"❌ Error checking for recent fills for order: {e}", exc_info=True)
+            return False