|
@@ -1970,7 +1970,99 @@ class MarketMonitor:
|
|
|
if synced_count == 0 and exchange_positions:
|
|
|
logger.info("✅ STARTUP: All existing exchange positions are already tracked.")
|
|
|
elif synced_count > 0:
|
|
|
- logger.info(f"🎉 STARTUP: Auto-synced {synced_count} orphaned position(s).")
|
|
|
+ logger.info(f"🎉 STARTUP: Auto-synced {synced_count} orphaned position(s) (Exchange had pos, Bot did not).")
|
|
|
+
|
|
|
+ # --- NEW LOGIC FOR STARTUP: Bot thinks position is open, but exchange does not --- #
|
|
|
+ logger.info("🔍 STARTUP: Checking for discrepancies (Bot has pos, Exchange does not)...")
|
|
|
+ bot_open_lifecycles = stats.get_trades_by_status('position_opened')
|
|
|
+
|
|
|
+ # Create a map of current exchange positions for quick lookup: symbol -> position_data
|
|
|
+ current_exchange_positions_map = {}
|
|
|
+ for ex_pos in (exchange_positions or []): # Use the exchange_positions fetched at the start of this method
|
|
|
+ if ex_pos.get('symbol') and abs(float(ex_pos.get('contracts', 0))) > 1e-9:
|
|
|
+ current_exchange_positions_map[ex_pos.get('symbol')] = ex_pos
|
|
|
+
|
|
|
+ closed_due_to_discrepancy_startup = 0
|
|
|
+ if bot_open_lifecycles:
|
|
|
+ for lc in bot_open_lifecycles:
|
|
|
+ symbol = lc.get('symbol')
|
|
|
+ lc_id = lc.get('trade_lifecycle_id')
|
|
|
+ token_for_log_discrepancy = symbol.split('/')[0] if symbol and '/' in symbol else symbol
|
|
|
+
|
|
|
+ if symbol not in current_exchange_positions_map:
|
|
|
+ logger.warning(f"🔄 STARTUP (Discrepancy): Bot lifecycle {lc_id} for {symbol} is 'position_opened', but NO position found on exchange. Closing lifecycle.")
|
|
|
+
|
|
|
+ entry_price = lc.get('entry_price', 0)
|
|
|
+ position_side = lc.get('position_side')
|
|
|
+ position_size_for_pnl = lc.get('current_position_size', 0)
|
|
|
+ exit_price_for_calc = 0
|
|
|
+ price_source_info = "unknown"
|
|
|
+
|
|
|
+ try:
|
|
|
+ # recent_fills for startup sync might be empty if called too early
|
|
|
+ # self.trading_engine.get_recent_fills might not be fully populated yet or too broad.
|
|
|
+ # Using a limited fetch here, but it might be better to rely on mark/entry price at startup for simplicity if fills are problematic.
|
|
|
+ recent_fills_for_startup_sync = self.trading_engine.get_recent_fills(symbol=symbol, limit=10)
|
|
|
+ if recent_fills_for_startup_sync:
|
|
|
+ closing_side = 'sell' if position_side == 'long' else 'buy'
|
|
|
+ relevant_fills = sorted(
|
|
|
+ [f for f in recent_fills_for_startup_sync if f.get('side') == closing_side and f.get('symbol') == symbol],
|
|
|
+ key=lambda f: f.get('timestamp'), reverse=True
|
|
|
+ )
|
|
|
+ if relevant_fills:
|
|
|
+ last_closing_fill = relevant_fills[0]
|
|
|
+ exit_price_for_calc = float(last_closing_fill.get('price', 0))
|
|
|
+ fill_ts_val = last_closing_fill.get('timestamp')
|
|
|
+ fill_timestamp_str = datetime.fromtimestamp(fill_ts_val/1000, tz=timezone.utc).isoformat() if fill_ts_val else "N/A"
|
|
|
+ price_source_info = f"last exchange fill ({formatter.format_price(exit_price_for_calc, symbol)} @ {fill_timestamp_str})"
|
|
|
+ logger.info(f"STARTUP SYNC: Using exit price {price_source_info} for {symbol} lifecycle {lc_id}.")
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"STARTUP SYNC: Error fetching recent fills for {symbol} to determine exit price: {e}")
|
|
|
+
|
|
|
+ if not exit_price_for_calc or exit_price_for_calc <= 0:
|
|
|
+ mark_price_from_lc = lc.get('mark_price')
|
|
|
+ if mark_price_from_lc and float(mark_price_from_lc) > 0:
|
|
|
+ exit_price_for_calc = float(mark_price_from_lc)
|
|
|
+ price_source_info = "lifecycle mark_price"
|
|
|
+ else:
|
|
|
+ exit_price_for_calc = entry_price
|
|
|
+ price_source_info = "lifecycle entry_price (0 PNL)"
|
|
|
+
|
|
|
+ realized_pnl = 0
|
|
|
+ if position_side == 'long':
|
|
|
+ realized_pnl = position_size_for_pnl * (exit_price_for_calc - entry_price)
|
|
|
+ elif position_side == 'short':
|
|
|
+ realized_pnl = position_size_for_pnl * (entry_price - exit_price_for_calc)
|
|
|
+
|
|
|
+ success_close = stats.update_trade_position_closed(
|
|
|
+ lifecycle_id=lc_id,
|
|
|
+ exit_price=exit_price_for_calc,
|
|
|
+ realized_pnl=realized_pnl,
|
|
|
+ exchange_fill_id=f"startup_sync_flat_{int(datetime.now().timestamp())}"
|
|
|
+ )
|
|
|
+
|
|
|
+ if success_close:
|
|
|
+ closed_due_to_discrepancy_startup += 1
|
|
|
+ logger.info(f"✅ STARTUP (Discrepancy): Successfully closed bot lifecycle {lc_id} for {symbol}.")
|
|
|
+ if self.notification_manager:
|
|
|
+ pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
|
|
|
+ notification_text = (
|
|
|
+ f"🔄 <b>Position Auto-Closed (Startup Sync)</b>\n\n"
|
|
|
+ f"Token: {token_for_log_discrepancy}\n"
|
|
|
+ f"Lifecycle ID: {lc_id[:8]}...\n"
|
|
|
+ f"Reason: Bot startup - found open lifecycle, but no corresponding position on exchange.\n"
|
|
|
+ f"Assumed Exit Price: {formatter.format_price(exit_price_for_calc, symbol)} (Source: {price_source_info})\n"
|
|
|
+ f"{pnl_emoji} Realized P&L: {formatter.format_price_with_symbol(realized_pnl)}\n"
|
|
|
+ f"Time: {datetime.now().strftime('%H:%M:%S')}"
|
|
|
+ )
|
|
|
+ await self.notification_manager.send_generic_notification(notification_text)
|
|
|
+ else:
|
|
|
+ logger.error(f"❌ STARTUP (Discrepancy): Failed to close bot lifecycle {lc_id} for {symbol}.")
|
|
|
+
|
|
|
+ if closed_due_to_discrepancy_startup > 0:
|
|
|
+ logger.info(f"🎉 STARTUP: Auto-closed {closed_due_to_discrepancy_startup} lifecycle(s) due to discrepancy (Bot had pos, Exchange did not).")
|
|
|
+ else:
|
|
|
+ logger.info("✅ STARTUP: No discrepancies found where bot had position and exchange did not.")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Error in startup auto-sync: {e}", exc_info=True)
|