|
@@ -54,6 +54,7 @@ class MarketMonitor:
|
|
|
async def start(self):
|
|
|
"""Start the market monitor."""
|
|
|
if self._monitoring_active:
|
|
|
+ logger.warning("Market monitor is already active")
|
|
|
return
|
|
|
|
|
|
self._monitoring_active = True
|
|
@@ -62,7 +63,7 @@ class MarketMonitor:
|
|
|
# Initialize tracking
|
|
|
await self._initialize_tracking()
|
|
|
|
|
|
- # Start monitoring task
|
|
|
+ # Start the monitoring loop
|
|
|
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
|
|
|
|
|
async def stop(self):
|
|
@@ -136,29 +137,36 @@ class MarketMonitor:
|
|
|
async def _initialize_tracking(self):
|
|
|
"""Initialize order and position tracking."""
|
|
|
try:
|
|
|
- # Get current open orders to initialize tracking
|
|
|
- orders = self.trading_engine.get_orders()
|
|
|
- if orders:
|
|
|
+ # Initialize order tracking
|
|
|
+ try:
|
|
|
+ orders = self.trading_engine.get_orders() or []
|
|
|
self.last_known_orders = {order.get('id') for order in orders if order.get('id')}
|
|
|
- logger.info(f"📋 Initialized tracking with {len(self.last_known_orders)} open orders")
|
|
|
-
|
|
|
- # Get current positions for P&L tracking
|
|
|
- positions = self.trading_engine.get_positions()
|
|
|
- if positions:
|
|
|
- for position in positions:
|
|
|
- symbol = position.get('symbol')
|
|
|
- contracts = float(position.get('contracts', 0))
|
|
|
- entry_price = float(position.get('entryPx', 0))
|
|
|
-
|
|
|
- if symbol and contracts != 0:
|
|
|
- self.last_known_positions[symbol] = {
|
|
|
- 'contracts': contracts,
|
|
|
- 'entry_price': entry_price
|
|
|
- }
|
|
|
- logger.info(f"📊 Initialized tracking with {len(self.last_known_positions)} positions")
|
|
|
+ logger.info(f"📋 Initialized tracking with {len(orders)} open orders")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Failed to initialize order tracking: {e}")
|
|
|
+ self.last_known_orders = set()
|
|
|
+
|
|
|
+ # Initialize position tracking
|
|
|
+ try:
|
|
|
+ positions = self.trading_engine.get_positions() or []
|
|
|
+ self.last_known_positions = {
|
|
|
+ pos.get('symbol'): pos for pos in positions
|
|
|
+ if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 0
|
|
|
+ }
|
|
|
+ logger.info(f"📊 Initialized tracking with {len(positions)} positions")
|
|
|
|
|
|
+ # 🆕 IMMEDIATE AUTO-SYNC: Check for orphaned positions right after initialization
|
|
|
+ if positions:
|
|
|
+ await self._immediate_startup_auto_sync()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Failed to initialize position tracking: {e}")
|
|
|
+ self.last_known_positions = {}
|
|
|
+
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error initializing tracking: {e}")
|
|
|
+ logger.error(f"❌ Failed to initialize tracking: {e}")
|
|
|
+ self.last_known_orders = set()
|
|
|
+ self.last_known_positions = {}
|
|
|
|
|
|
async def _monitor_loop(self):
|
|
|
"""Main monitoring loop that runs every BOT_HEARTBEAT_SECONDS."""
|
|
@@ -1628,10 +1636,10 @@ class MarketMonitor:
|
|
|
if lifecycle_id:
|
|
|
# Update to position_opened status
|
|
|
success = stats.update_trade_position_opened(
|
|
|
- lifecycle_id=lifecycle_id,
|
|
|
- entry_price=entry_price,
|
|
|
- entry_amount=abs(contracts),
|
|
|
- exchange_fill_id=f"external_fill_{int(datetime.now().timestamp())}"
|
|
|
+ lifecycle_id,
|
|
|
+ entry_price,
|
|
|
+ abs(contracts),
|
|
|
+ f"external_fill_{int(datetime.now().timestamp())}"
|
|
|
)
|
|
|
|
|
|
if success:
|
|
@@ -1728,4 +1736,113 @@ class MarketMonitor:
|
|
|
logger.info(f"🧹 _handle_orphaned_position deprecated: use _auto_sync_orphaned_positions instead")
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error handling orphaned position: {e}", exc_info=True)
|
|
|
+ logger.error(f"❌ Error handling orphaned position: {e}", exc_info=True)
|
|
|
+
|
|
|
+ async def _immediate_startup_auto_sync(self):
|
|
|
+ """🆕 Immediately check for and sync orphaned positions on startup."""
|
|
|
+ try:
|
|
|
+ logger.info("🔍 Checking for orphaned positions on startup...")
|
|
|
+
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if not stats:
|
|
|
+ logger.warning("⚠️ TradingStats not available for startup auto-sync")
|
|
|
+ return
|
|
|
+
|
|
|
+ # Get fresh exchange positions
|
|
|
+ exchange_positions = self.trading_engine.get_positions() or []
|
|
|
+ synced_count = 0
|
|
|
+
|
|
|
+ for exchange_pos in exchange_positions:
|
|
|
+ symbol = exchange_pos.get('symbol')
|
|
|
+ contracts = float(exchange_pos.get('contracts', 0))
|
|
|
+
|
|
|
+ if symbol and abs(contracts) > 0:
|
|
|
+ # Check if we have a trade lifecycle record for this position
|
|
|
+ existing_trade = stats.get_trade_by_symbol_and_status(symbol, 'position_opened')
|
|
|
+
|
|
|
+ if not existing_trade:
|
|
|
+ # 🚨 ORPHANED POSITION: Auto-create trade lifecycle record
|
|
|
+ entry_price = float(exchange_pos.get('entryPrice', 0))
|
|
|
+ position_side = 'long' if contracts > 0 else 'short'
|
|
|
+ order_side = 'buy' if contracts > 0 else 'sell'
|
|
|
+
|
|
|
+ logger.info(f"🔄 STARTUP: Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price} (exchange data)")
|
|
|
+
|
|
|
+ # Create trade lifecycle for external position
|
|
|
+ lifecycle_id = stats.create_trade_lifecycle(
|
|
|
+ symbol=symbol,
|
|
|
+ side=order_side,
|
|
|
+ entry_order_id=f"startup_sync_{int(datetime.now().timestamp())}",
|
|
|
+ trade_type='external'
|
|
|
+ )
|
|
|
+
|
|
|
+ if lifecycle_id:
|
|
|
+ # Update to position_opened status
|
|
|
+ success = stats.update_trade_position_opened(
|
|
|
+ lifecycle_id,
|
|
|
+ entry_price,
|
|
|
+ abs(contracts),
|
|
|
+ f"startup_fill_{int(datetime.now().timestamp())}"
|
|
|
+ )
|
|
|
+
|
|
|
+ if success:
|
|
|
+ synced_count += 1
|
|
|
+ logger.info(f"✅ STARTUP: Successfully synced orphaned position for {symbol}")
|
|
|
+
|
|
|
+ # 🆕 Send immediate notification
|
|
|
+ await self._send_startup_auto_sync_notification(exchange_pos, symbol, position_side, abs(contracts), entry_price)
|
|
|
+ else:
|
|
|
+ logger.error(f"❌ STARTUP: Failed to sync orphaned position for {symbol}")
|
|
|
+ else:
|
|
|
+ logger.error(f"❌ STARTUP: Failed to create lifecycle for orphaned position {symbol}")
|
|
|
+
|
|
|
+ if synced_count > 0:
|
|
|
+ logger.info(f"🎉 STARTUP: Auto-synced {synced_count} orphaned position(s) and sent notifications")
|
|
|
+ else:
|
|
|
+ logger.info("✅ STARTUP: No orphaned positions found - all positions already tracked")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ Error in startup auto-sync: {e}")
|
|
|
+
|
|
|
+ async def _send_startup_auto_sync_notification(self, exchange_pos, symbol, position_side, contracts, entry_price):
|
|
|
+ """Send notification for positions auto-synced on startup."""
|
|
|
+ try:
|
|
|
+ if not self.notification_manager:
|
|
|
+ logger.warning("⚠️ No notification manager available for startup auto-sync notification")
|
|
|
+ return
|
|
|
+
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+ unrealized_pnl = float(exchange_pos.get('unrealizedPnl', 0))
|
|
|
+ position_value = float(exchange_pos.get('notional', 0))
|
|
|
+ liquidation_price = float(exchange_pos.get('liquidationPrice', 0))
|
|
|
+ leverage = float(exchange_pos.get('leverage', 1))
|
|
|
+ pnl_percentage = float(exchange_pos.get('percentage', 0))
|
|
|
+
|
|
|
+ pnl_emoji = "🟢" if unrealized_pnl >= 0 else "🔴"
|
|
|
+ notification_text = (
|
|
|
+ f"🚨 <b>Bot Startup: Position Auto-Synced</b>\n\n"
|
|
|
+ f"🎯 Token: {token}\n"
|
|
|
+ f"📈 Direction: {position_side.upper()}\n"
|
|
|
+ f"📏 Size: {contracts:.6f} {token}\n"
|
|
|
+ f"💰 Entry: ${entry_price:,.4f}\n"
|
|
|
+ f"💵 Value: ${position_value:,.2f}\n"
|
|
|
+ f"{pnl_emoji} P&L: ${unrealized_pnl:,.2f} ({pnl_percentage:+.2f}%)\n"
|
|
|
+ )
|
|
|
+
|
|
|
+ if leverage > 1:
|
|
|
+ notification_text += f"⚡ Leverage: {leverage:.1f}x\n"
|
|
|
+ if liquidation_price > 0:
|
|
|
+ notification_text += f"⚠️ Liquidation: ${liquidation_price:,.2f}\n"
|
|
|
+
|
|
|
+ notification_text += (
|
|
|
+ f"\n📍 <b>Discovered on bot startup</b>\n"
|
|
|
+ f"⏰ Time: {datetime.now().strftime('%H:%M:%S')}\n\n"
|
|
|
+ f"✅ Position now tracked in bot\n"
|
|
|
+ f"💡 Use /sl {token} [price] to set stop loss"
|
|
|
+ )
|
|
|
+
|
|
|
+ await self.notification_manager.send_generic_notification(notification_text)
|
|
|
+ logger.info(f"📤 STARTUP: Sent auto-sync notification for {symbol}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ STARTUP: Failed to send auto-sync notification for {symbol}: {e}")
|