Bladeren bron

Update BOT_VERSION to 2.2.127 and enhance MarketMonitor with improved discrepancy handling during startup.

- Incremented BOT_VERSION for the upcoming release.
- Enhanced the MarketMonitor to log discrepancies when the bot has open positions that are not reflected on the exchange, including logic to auto-close these positions and calculate realized P&L.
- Improved logging for better visibility into the startup synchronization process and discrepancies found.
Carles Sentis 3 dagen geleden
bovenliggende
commit
36e9706ac4
2 gewijzigde bestanden met toevoegingen van 94 en 2 verwijderingen
  1. 93 1
      src/monitoring/market_monitor.py
  2. 1 1
      trading_bot.py

+ 93 - 1
src/monitoring/market_monitor.py

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

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.2.126"
+BOT_VERSION = "2.2.127"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))