Explorar o código

Implement auto-sync for orphaned positions in trading bot - Enhanced InfoCommands and MarketMonitor to automatically detect and sync orphaned positions without trade lifecycle records. This update improves position management accuracy and user notifications, ensuring all positions are tracked effectively. Added logging for better traceability of auto-sync actions.

Carles Sentis hai 4 días
pai
achega
ab63095d50
Modificáronse 2 ficheiros con 150 adicións e 5 borrados
  1. 62 4
      src/commands/info_commands.py
  2. 88 1
      src/monitoring/market_monitor.py

+ 62 - 4
src/commands/info_commands.py

@@ -112,10 +112,60 @@ class InfoCommands:
             await context.bot.send_message(chat_id=chat_id, text="❌ Trading statistics not available.")
             return
         
-        # Get open positions from unified trades table
+        # 🆕 AUTO-SYNC: Check for positions on exchange that don't have trade lifecycle records
+        exchange_positions = self.trading_engine.get_positions() or []
+        synced_positions = []
+        
+        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('entryPx', 0))
+                    position_side = 'long' if contracts > 0 else 'short'
+                    order_side = 'buy' if contracts > 0 else 'sell'
+                    
+                    logger.info(f"🔄 Auto-syncing orphaned position: {symbol} {position_side} {abs(contracts)} @ ${entry_price}")
+                    
+                    # Create trade lifecycle for external position
+                    lifecycle_id = stats.create_trade_lifecycle(
+                        symbol=symbol,
+                        side=order_side,
+                        entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
+                        trade_type='external'
+                    )
+                    
+                    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())}"
+                        )
+                        
+                        if success:
+                            synced_positions.append(symbol)
+                            logger.info(f"✅ Successfully synced orphaned position for {symbol}")
+                        else:
+                            logger.error(f"❌ Failed to sync orphaned position for {symbol}")
+                    else:
+                        logger.error(f"❌ Failed to create lifecycle for orphaned position {symbol}")
+        
+        if synced_positions:
+            sync_msg = f"🔄 <b>Auto-synced {len(synced_positions)} orphaned position(s):</b> {', '.join([s.split('/')[0] for s in synced_positions])}\n\n"
+        else:
+            sync_msg = ""
+        
+        # Get open positions from unified trades table (now including any newly synced ones)
         open_positions = stats.get_open_positions()
         
-        positions_text = "📈 <b>Open Positions</b>\n\n"
+        positions_text = f"📈 <b>Open Positions</b>\n\n{sync_msg}"
         
         if open_positions:
             total_unrealized = 0
@@ -128,6 +178,7 @@ class InfoCommands:
                 entry_price = position_trade['entry_price']
                 current_amount = position_trade['current_position_size']
                 unrealized_pnl = position_trade.get('unrealized_pnl', 0)
+                trade_type = position_trade.get('trade_type', 'manual')
                 
                 # Get current market price
                 mark_price = entry_price  # Fallback
@@ -165,7 +216,14 @@ class InfoCommands:
                 entry_price_str = formatter.format_price_with_symbol(entry_price, token)
                 mark_price_str = formatter.format_price_with_symbol(mark_price, token)
                 
-                positions_text += f"{pos_emoji} <b>{token} ({direction})</b>\n"
+                # Trade type indicator
+                type_indicator = ""
+                if trade_type == 'external':
+                    type_indicator = " 🔄"  # External/synced position
+                elif trade_type == 'bot':
+                    type_indicator = " 🤖"  # Bot-created position
+                
+                positions_text += f"{pos_emoji} <b>{token} ({direction}){type_indicator}</b>\n"
                 positions_text += f"   📏 Size: {abs(current_amount):.6f} {token}\n"
                 positions_text += f"   💰 Entry: {entry_price_str}\n"
                 positions_text += f"   📊 Mark: {mark_price_str}\n"
@@ -191,7 +249,7 @@ class InfoCommands:
             positions_text += f"💼 <b>Total Portfolio:</b>\n"
             positions_text += f"   💵 Total Value: ${total_position_value:,.2f}\n"
             positions_text += f"   {portfolio_emoji} Total P&L: ${total_unrealized:,.2f}\n\n"
-            positions_text += f"🆕 <b>Source:</b> Unified Trades Table (Phase 4)\n"
+            positions_text += f"🤖 <b>Legend:</b> 🤖 Bot-created • 🔄 External/synced\n"
             positions_text += f"💡 Use /sl [token] [price] or /tp [token] [price] to set risk management"
             
         else:

+ 88 - 1
src/monitoring/market_monitor.py

@@ -171,6 +171,10 @@ class MarketMonitor:
                 if loop_count % 10 == 0:
                     await self._cleanup_orphaned_stop_losses()
                     await self._cleanup_external_stop_loss_tracking()
+                    
+                    # 🆕 AUTO-SYNC: Check for orphaned positions every 10 heartbeats
+                    await self._auto_sync_orphaned_positions()
+                    
                     loop_count = 0  # Reset counter to prevent overflow
                 
                 await asyncio.sleep(Config.BOT_HEARTBEAT_SECONDS)
@@ -1515,4 +1519,87 @@ class MarketMonitor:
                 logger.info(f"🧹 Cleaned up {len(to_remove)} external stop loss orders")
                 
         except Exception as e:
-            logger.error(f"❌ Error cleaning up external stop loss tracking: {e}") 
+            logger.error(f"❌ Error cleaning up external stop loss tracking: {e}") 
+
+    async def _auto_sync_orphaned_positions(self):
+        """Automatically detect and sync orphaned positions (positions on exchange without trade lifecycle records)."""
+        try:
+            stats = self.trading_engine.get_stats()
+            if not stats:
+                return
+
+            # Get current 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('entryPx', 0))
+                        position_side = 'long' if contracts > 0 else 'short'
+                        order_side = 'buy' if contracts > 0 else 'sell'
+                        token = symbol.split('/')[0] if '/' in symbol else symbol
+                        
+                        logger.warning(f"🔄 AUTO-SYNC: Orphaned position detected - {symbol} {position_side} {abs(contracts)} @ ${entry_price}")
+                        
+                        # Create trade lifecycle for external position
+                        lifecycle_id = stats.create_trade_lifecycle(
+                            symbol=symbol,
+                            side=order_side,
+                            entry_order_id=f"external_sync_{int(datetime.now().timestamp())}",
+                            trade_type='external'
+                        )
+                        
+                        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())}"
+                            )
+                            
+                            if success:
+                                synced_count += 1
+                                logger.info(f"✅ AUTO-SYNC: Successfully synced orphaned position for {symbol}")
+                                
+                                # Send notification about auto-sync
+                                if self.notification_manager:
+                                    await self.notification_manager.send_generic_notification(
+                                        f"🔄 <b>Position Auto-Synced</b>\n\n"
+                                        f"Token: {token}\n"
+                                        f"Direction: {position_side.upper()}\n"
+                                        f"Size: {abs(contracts):.6f} {token}\n"
+                                        f"Entry Price: ${entry_price:,.4f}\n"
+                                        f"Reason: Position opened outside bot\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"
+                                    )
+                            else:
+                                logger.error(f"❌ AUTO-SYNC: Failed to sync orphaned position for {symbol}")
+                        else:
+                            logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for orphaned position {symbol}")
+
+            if synced_count > 0:
+                logger.info(f"🔄 AUTO-SYNC: Synced {synced_count} orphaned position(s) this cycle")
+
+        except Exception as e:
+            logger.error(f"❌ Error in auto-sync orphaned positions: {e}", exc_info=True)
+
+    async def _handle_orphaned_position(self, symbol, contracts):
+        """Handle the orphaned position."""
+        try:
+            # This method is now deprecated in favor of _auto_sync_orphaned_positions
+            # Keeping for backwards compatibility but not implementing
+            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)