|
@@ -19,27 +19,36 @@ class MarketMonitor:
|
|
|
"""Handles external trade monitoring and market events."""
|
|
|
|
|
|
def __init__(self, trading_engine, notification_manager=None):
|
|
|
- """Initialize the market monitor."""
|
|
|
+ """Initialize market monitor."""
|
|
|
self.trading_engine = trading_engine
|
|
|
self.notification_manager = notification_manager
|
|
|
- self.client = trading_engine.client
|
|
|
- self._last_processed_trade_time = datetime.now(timezone.utc) - timedelta(seconds=120)
|
|
|
self._monitoring_active = False
|
|
|
+ self._monitor_task = None
|
|
|
|
|
|
-
|
|
|
- self._external_stop_loss_orders = {}
|
|
|
+
|
|
|
+ self.last_known_orders = set()
|
|
|
+ self.last_known_positions = {}
|
|
|
+
|
|
|
+
|
|
|
+ self.price_alarms = {}
|
|
|
+ self.next_alarm_id = 1
|
|
|
+
|
|
|
+
|
|
|
+ self.external_stop_losses = {}
|
|
|
+
|
|
|
+
|
|
|
+ self.cached_positions = []
|
|
|
+ self.cached_orders = []
|
|
|
+ self.cached_balance = None
|
|
|
+ self.last_cache_update = None
|
|
|
|
|
|
|
|
|
-
|
|
|
self.last_processed_trade_time: Optional[datetime] = None
|
|
|
|
|
|
|
|
|
self.alarm_manager = AlarmManager()
|
|
|
|
|
|
-
|
|
|
- self.last_known_orders = set()
|
|
|
- self.last_known_positions = {}
|
|
|
-
|
|
|
+
|
|
|
self._load_state()
|
|
|
|
|
|
async def start(self):
|
|
@@ -156,6 +165,9 @@ class MarketMonitor:
|
|
|
try:
|
|
|
loop_count = 0
|
|
|
while self._monitoring_active:
|
|
|
+
|
|
|
+ await self._update_cached_data()
|
|
|
+
|
|
|
|
|
|
await self._activate_pending_stop_losses_from_trades()
|
|
|
|
|
@@ -188,6 +200,50 @@ class MarketMonitor:
|
|
|
await asyncio.sleep(5)
|
|
|
await self._monitor_loop()
|
|
|
|
|
|
+ async def _update_cached_data(self):
|
|
|
+ """🆕 Continuously update cached exchange data every heartbeat."""
|
|
|
+ try:
|
|
|
+
|
|
|
+ fresh_positions = self.trading_engine.get_positions() or []
|
|
|
+ fresh_orders = self.trading_engine.get_orders() or []
|
|
|
+ fresh_balance = self.trading_engine.get_balance()
|
|
|
+
|
|
|
+
|
|
|
+ self.cached_positions = fresh_positions
|
|
|
+ self.cached_orders = fresh_orders
|
|
|
+ self.cached_balance = fresh_balance
|
|
|
+ self.last_cache_update = datetime.now(timezone.utc)
|
|
|
+
|
|
|
+ logger.debug(f"🔄 Updated cache: {len(fresh_positions)} positions, {len(fresh_orders)} orders")
|
|
|
+
|
|
|
+
|
|
|
+ if len(fresh_positions) != len(self.last_known_positions):
|
|
|
+ logger.info(f"📊 Position count changed: {len(self.last_known_positions)} → {len(fresh_positions)}")
|
|
|
+
|
|
|
+ if len(fresh_orders) != len(self.last_known_orders):
|
|
|
+ logger.info(f"📋 Order count changed: {len(self.last_known_orders)} → {len(fresh_orders)}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error updating cached data: {e}")
|
|
|
+
|
|
|
+ def get_cached_positions(self):
|
|
|
+ """Get cached positions (updated every heartbeat)."""
|
|
|
+ return self.cached_positions or []
|
|
|
+
|
|
|
+ def get_cached_orders(self):
|
|
|
+ """Get cached orders (updated every heartbeat)."""
|
|
|
+ return self.cached_orders or []
|
|
|
+
|
|
|
+ def get_cached_balance(self):
|
|
|
+ """Get cached balance (updated every heartbeat)."""
|
|
|
+ return self.cached_balance
|
|
|
+
|
|
|
+ def get_cache_age_seconds(self):
|
|
|
+ """Get age of cached data in seconds."""
|
|
|
+ if not self.last_cache_update:
|
|
|
+ return float('inf')
|
|
|
+ return (datetime.now(timezone.utc) - self.last_cache_update).total_seconds()
|
|
|
+
|
|
|
async def _check_order_fills(self):
|
|
|
"""Check for filled orders and send notifications."""
|
|
|
try:
|
|
@@ -517,9 +573,9 @@ class MarketMonitor:
|
|
|
|
|
|
is_external_stop_loss = False
|
|
|
stop_loss_info = None
|
|
|
- if exchange_order_id_from_fill and exchange_order_id_from_fill in self._external_stop_loss_orders:
|
|
|
+ if exchange_order_id_from_fill and exchange_order_id_from_fill in self.external_stop_losses:
|
|
|
is_external_stop_loss = True
|
|
|
- stop_loss_info = self._external_stop_loss_orders[exchange_order_id_from_fill]
|
|
|
+ stop_loss_info = self.external_stop_losses[exchange_order_id_from_fill]
|
|
|
logger.info(f"🛑 EXTERNAL STOP LOSS EXECUTION: {token} - Order {exchange_order_id_from_fill} filled @ ${price:.2f}")
|
|
|
|
|
|
logger.info(f"🔍 Processing {'external stop loss' if is_external_stop_loss else 'external trade'}: {trade_id} - {side} {amount} {full_symbol} @ ${price:.2f}")
|
|
@@ -555,7 +611,7 @@ class MarketMonitor:
|
|
|
)
|
|
|
|
|
|
|
|
|
- del self._external_stop_loss_orders[exchange_order_id_from_fill]
|
|
|
+ del self.external_stop_losses[exchange_order_id_from_fill]
|
|
|
|
|
|
logger.info(f"🛑 Processed external stop loss execution: {side} {amount} {full_symbol} @ ${price:.2f} (long_closed)")
|
|
|
|
|
@@ -1435,7 +1491,7 @@ class MarketMonitor:
|
|
|
continue
|
|
|
|
|
|
|
|
|
- if exchange_order_id in self._external_stop_loss_orders:
|
|
|
+ if exchange_order_id in self.external_stop_losses:
|
|
|
continue
|
|
|
|
|
|
|
|
@@ -1462,7 +1518,7 @@ class MarketMonitor:
|
|
|
|
|
|
if is_stop_loss:
|
|
|
|
|
|
- self._external_stop_loss_orders[exchange_order_id] = {
|
|
|
+ self.external_stop_losses[exchange_order_id] = {
|
|
|
'token': token,
|
|
|
'symbol': symbol,
|
|
|
'trigger_price': price,
|
|
@@ -1488,15 +1544,15 @@ class MarketMonitor:
|
|
|
async def _cleanup_external_stop_loss_tracking(self):
|
|
|
"""Clean up external stop loss orders that are no longer active."""
|
|
|
try:
|
|
|
- if not self._external_stop_loss_orders:
|
|
|
+ if not self.external_stop_losses:
|
|
|
return
|
|
|
|
|
|
|
|
|
open_orders = self.trading_engine.get_orders()
|
|
|
if not open_orders:
|
|
|
|
|
|
- removed_count = len(self._external_stop_loss_orders)
|
|
|
- self._external_stop_loss_orders.clear()
|
|
|
+ removed_count = len(self.external_stop_losses)
|
|
|
+ self.external_stop_losses.clear()
|
|
|
if removed_count > 0:
|
|
|
logger.info(f"🧹 Cleared {removed_count} external stop loss orders (no open orders)")
|
|
|
return
|
|
@@ -1506,13 +1562,13 @@ class MarketMonitor:
|
|
|
|
|
|
|
|
|
to_remove = []
|
|
|
- for order_id, stop_loss_info in self._external_stop_loss_orders.items():
|
|
|
+ for order_id, stop_loss_info in self.external_stop_losses.items():
|
|
|
if order_id not in current_order_ids:
|
|
|
to_remove.append(order_id)
|
|
|
|
|
|
for order_id in to_remove:
|
|
|
- stop_loss_info = self._external_stop_loss_orders[order_id]
|
|
|
- del self._external_stop_loss_orders[order_id]
|
|
|
+ stop_loss_info = self.external_stop_losses[order_id]
|
|
|
+ del self.external_stop_losses[order_id]
|
|
|
logger.info(f"🗑️ Removed external stop loss tracking for {stop_loss_info['token']} order {order_id} (no longer open)")
|
|
|
|
|
|
if to_remove:
|
|
@@ -1528,8 +1584,8 @@ class MarketMonitor:
|
|
|
if not stats:
|
|
|
return
|
|
|
|
|
|
-
|
|
|
- exchange_positions = self.trading_engine.get_positions() or []
|
|
|
+
|
|
|
+ exchange_positions = self.cached_positions or []
|
|
|
synced_count = 0
|
|
|
|
|
|
for exchange_pos in exchange_positions:
|