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