Jelajahi Sumber

Enhance CopyTradingMonitor with stop loss and take profit monitoring features

- Added functionality to monitor and copy stop loss and take profit orders from the target trader, improving risk management.
- Introduced methods to fetch target trader's open orders and handle stop loss and take profit order placements.
- Updated trade execution logic to utilize limit orders at the target trader's entry price for better accuracy in opening positions.
- Enhanced logging for better tracking of order execution and monitoring actions.
Carles Sentis 4 hari lalu
induk
melakukan
c446da6d21
2 mengubah file dengan 292 tambahan dan 9 penghapusan
  1. 291 8
      src/monitoring/copy_trading_monitor.py
  2. 1 1
      trading_bot.py

+ 291 - 8
src/monitoring/copy_trading_monitor.py

@@ -42,6 +42,7 @@ class CopyTrade:
     original_trade_hash: str
     target_trader_address: str
     timestamp: int
+    entry_price: Optional[float] = None  # Target trader's entry price for better copying
 
 
 class CopyTradingMonitor:
@@ -246,6 +247,12 @@ class CopyTradingMonitor:
             except Exception as e:
                 self.logger.error(f"Error updating our positions: {e}")
             
+            # Monitor for stop losses and take profits from target trader
+            try:
+                await self._monitor_stop_losses_and_take_profits()
+            except Exception as e:
+                self.logger.error(f"Error monitoring stop losses and take profits: {e}")
+            
             # Update last check timestamp (async version)
             await self.state_manager.update_last_check_async()
             
@@ -343,7 +350,8 @@ class CopyTradingMonitor:
                         leverage=new_pos.leverage,
                         original_trade_hash=trade_id,
                         target_trader_address=self.target_address,
-                        timestamp=new_pos.timestamp
+                        timestamp=new_pos.timestamp,
+                        entry_price=new_pos.entry_price
                     ))
                     
             # Check for position reductions
@@ -361,7 +369,8 @@ class CopyTradingMonitor:
                         leverage=new_pos.leverage,
                         original_trade_hash=trade_id,
                         target_trader_address=self.target_address,
-                        timestamp=new_pos.timestamp
+                        timestamp=new_pos.timestamp,
+                        entry_price=new_pos.entry_price
                     ))
                     self.logger.info(f"📉 Detected position decrease: {action} {size_decrease} {coin}")
             
@@ -385,7 +394,8 @@ class CopyTradingMonitor:
                         leverage=tracked_pos['leverage'],
                         original_trade_hash=trade_id,
                         target_trader_address=self.target_address,
-                        timestamp=int(time.time() * 1000)
+                        timestamp=int(time.time() * 1000),
+                        entry_price=tracked_pos.get('entry_price')
                     ))
                     self.logger.info(f"❌ Detected position closure: {action} {tracked_pos['size']} {coin}")
                 
@@ -439,7 +449,12 @@ class CopyTradingMonitor:
             await asyncio.sleep(self.execution_delay)
             
             # Execute the trade
-            success = await self._execute_hyperliquid_trade(trade, position_calc, leverage)
+            if trade.entry_price and 'open' in trade.action:
+                # Use limit order at target's entry price for opening positions
+                success = await self._execute_limit_order_at_target_price(trade, position_calc, leverage, trade.entry_price)
+            else:
+                # Use market order for closes, reduces, or when entry_price not available
+                success = await self._execute_hyperliquid_trade(trade, position_calc, leverage)
             
             # Mark trade as copied (whether successful or not to avoid retrying)
             await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
@@ -628,8 +643,277 @@ class CopyTradingMonitor:
             self.logger.error(f"Error getting target account balance: {e}")
             return 0.0
     
+    async def _execute_limit_order_at_target_price(self, trade: CopyTrade, position_calc: Dict[str, float], leverage: float, target_entry_price: float) -> bool:
+        """Execute trade using limit order at target trader's entry price for better accuracy"""
+        try:
+            # Determine if this is a buy or sell order
+            is_buy = 'long' in trade.action or ('close' in trade.action and 'short' in trade.action)
+            side = 'buy' if is_buy else 'sell'
+            
+            # Extract values from position calculation
+            token_amount = position_calc['token_amount']
+            margin_used = position_calc['margin_to_use']
+            position_value = position_calc['position_value']
+            
+            symbol = f"{trade.coin}/USDC:USDC"
+            
+            self.logger.info(f"📍 Executing {trade.action} for {trade.coin} with LIMIT order:")
+            self.logger.info(f"  📊 Side: {side}")
+            self.logger.info(f"  🎯 Target Entry Price: ${target_entry_price:.4f}")
+            self.logger.info(f"  🪙 Token Amount: {token_amount:.6f} {trade.coin}")
+            self.logger.info(f"  💳 Margin: ${margin_used:.2f}")
+            self.logger.info(f"  🏦 Position Value: ${position_value:.2f}")
+            self.logger.info(f"  ⚖️ Leverage: {leverage}x")
+            
+            # Set leverage before placing the order
+            self.logger.info(f"⚖️ Setting leverage to {leverage}x for {trade.coin}")
+            leverage_result, leverage_error = await asyncio.to_thread(
+                self.client.set_leverage,
+                leverage=int(leverage),
+                symbol=symbol
+            )
+            if leverage_error:
+                self.logger.warning(f"⚠️ Failed to set leverage to {leverage}x for {trade.coin}: {leverage_error}")
+            else:
+                self.logger.info(f"✅ Successfully set leverage to {leverage}x for {trade.coin}")
+            
+            # Place limit order at target trader's entry price
+            result, error = await asyncio.to_thread(
+                self.client.place_limit_order,
+                symbol=symbol,
+                side=side,
+                amount=token_amount,
+                price=target_entry_price
+            )
+            
+            if error:
+                self.logger.error(f"❌ Limit order failed: {error}")
+                # Fallback to market order if limit order fails
+                self.logger.info("🔄 Falling back to market order...")
+                return await self._execute_hyperliquid_trade(trade, position_calc, leverage)
+            
+            if result:
+                self.logger.info(f"✅ Successfully placed LIMIT order at target price: ${target_entry_price:.4f}")
+                self.logger.info(f"  🪙 {token_amount:.6f} {trade.coin} (${position_value:.2f} value, ${margin_used:.2f} margin)")
+                return True
+            else:
+                self.logger.error(f"❌ Failed to place limit order - no result returned")
+                return False
+                
+        except Exception as e:
+            self.logger.error(f"❌ Error executing limit order at target price: {e}")
+            # Fallback to market order
+            self.logger.info("🔄 Falling back to market order due to error...")
+            return await self._execute_hyperliquid_trade(trade, position_calc, leverage)
+
+    async def _get_target_trader_open_orders(self) -> Optional[List[Dict[str, Any]]]:
+        """Attempt to get target trader's open orders (may not be available for external addresses)"""
+        try:
+            # Note: This likely won't work for external addresses due to API limitations
+            # But we can try anyway for completeness
+            timeout = aiohttp.ClientTimeout(total=5.0)
+            async with aiohttp.ClientSession(timeout=timeout) as session:
+                payload = {
+                    "type": "openOrders",
+                    "user": self.target_address
+                }
+                
+                async with session.post(self.info_url, json=payload) as response:
+                    if response.status == 200:
+                        data = await response.json()
+                        if data and isinstance(data, list):
+                            self.logger.info(f"📋 Found {len(data)} open orders for target trader")
+                            return data
+                        else:
+                            self.logger.debug("📋 No open orders found for target trader")
+                            return []
+                    else:
+                        self.logger.debug(f"❌ Failed to fetch target trader open orders: {response.status}")
+                        return None
+                        
+        except Exception as e:
+            self.logger.debug(f"❌ Error fetching target trader open orders: {e}")
+            return None
+
+    async def _monitor_stop_losses_and_take_profits(self):
+        """Monitor for stop loss and take profit orders from target trader"""
+        try:
+            # Get target trader's open orders (if available)
+            open_orders = await self._get_target_trader_open_orders()
+            
+            if not open_orders:
+                return
+            
+            # Look for stop loss and take profit orders
+            for order in open_orders:
+                order_type = order.get('type', '').lower()
+                symbol = order.get('coin', '')
+                trigger_price = order.get('triggerPx')
+                order_side = order.get('side', '').lower()
+                order_size = order.get('sz', 0)
+                
+                if not symbol or not trigger_price:
+                    continue
+                
+                if 'stop' in order_type or 'sl' in order_type:
+                    # Found a stop loss order
+                    self.logger.info(f"🛑 Target trader has stop loss for {symbol} at ${trigger_price}")
+                    await self._copy_stop_loss_order(symbol, trigger_price, order_side, order_size)
+                    
+                elif 'take' in order_type or 'tp' in order_type:
+                    # Found a take profit order
+                    self.logger.info(f"🎯 Target trader has take profit for {symbol} at ${trigger_price}")
+                    await self._copy_take_profit_order(symbol, trigger_price, order_side, order_size)
+                    
+        except Exception as e:
+            self.logger.error(f"❌ Error monitoring stop losses and take profits: {e}")
+
+    async def _copy_stop_loss_order(self, symbol: str, trigger_price: float, order_side: str, target_order_size: float):
+        """Copy a stop loss order from the target trader"""
+        try:
+            # Convert symbol format if needed (e.g., ETH -> ETH/USDC:USDC)
+            trading_symbol = f"{symbol}/USDC:USDC" if "/" not in symbol else symbol
+            
+            # Check if we have a position in this symbol
+            our_positions = await asyncio.to_thread(self.client.get_positions, symbol=trading_symbol)
+            if not our_positions:
+                self.logger.debug(f"⚠️ No position found for {symbol} - skipping stop loss copy")
+                return
+            
+            # Find our position
+            our_position = None
+            for pos in our_positions:
+                if pos.get('symbol') == trading_symbol and float(pos.get('contracts', 0)) != 0:
+                    our_position = pos
+                    break
+            
+            if not our_position:
+                self.logger.debug(f"⚠️ No open position found for {symbol} - skipping stop loss copy")
+                return
+            
+            # Calculate our order size based on position ratio
+            our_position_size = abs(float(our_position.get('contracts', 0)))
+            our_position_side = 'long' if float(our_position.get('contracts', 0)) > 0 else 'short'
+            
+            # Determine stop loss side (opposite of position)
+            stop_side = 'sell' if our_position_side == 'long' else 'buy'
+            
+            # Check if we already have a stop loss for this position
+            existing_orders = await asyncio.to_thread(self.client.get_open_orders, trading_symbol)
+            if existing_orders:
+                for existing_order in existing_orders:
+                    if ('stop' in existing_order.get('type', '').lower() or 
+                        'sl' in existing_order.get('type', '').lower()):
+                        self.logger.debug(f"📋 Stop loss already exists for {symbol} - skipping")
+                        return
+            
+            # Place the stop loss order
+            self.logger.info(f"🛑 Placing stop loss for {symbol}: {stop_side} {our_position_size:.6f} @ ${trigger_price}")
+            
+            result, error = await asyncio.to_thread(
+                self.client.place_stop_loss_order,
+                symbol=trading_symbol,
+                side=stop_side,
+                amount=our_position_size,
+                stop_price_arg=float(trigger_price)
+            )
+            
+            if error:
+                self.logger.error(f"❌ Failed to place stop loss for {symbol}: {error}")
+            else:
+                self.logger.info(f"✅ Successfully copied stop loss for {symbol}")
+                
+                # Send notification
+                if self.notifications_enabled:
+                    await self.notification_manager.send_generic_notification(
+                        f"🛑 Stop Loss Copied\n"
+                        f"Symbol: {symbol}\n"
+                        f"Trigger: ${trigger_price}\n"
+                        f"Size: {our_position_size:.6f}\n"
+                        f"Side: {stop_side}"
+                    )
+                    
+        except Exception as e:
+            self.logger.error(f"❌ Error copying stop loss for {symbol}: {e}")
+
+    async def _copy_take_profit_order(self, symbol: str, trigger_price: float, order_side: str, target_order_size: float):
+        """Copy a take profit order from the target trader"""
+        try:
+            # Convert symbol format if needed (e.g., ETH -> ETH/USDC:USDC)
+            trading_symbol = f"{symbol}/USDC:USDC" if "/" not in symbol else symbol
+            
+            # Check if we have a position in this symbol
+            our_positions = await asyncio.to_thread(self.client.get_positions, symbol=trading_symbol)
+            if not our_positions:
+                self.logger.debug(f"⚠️ No position found for {symbol} - skipping take profit copy")
+                return
+            
+            # Find our position
+            our_position = None
+            for pos in our_positions:
+                if pos.get('symbol') == trading_symbol and float(pos.get('contracts', 0)) != 0:
+                    our_position = pos
+                    break
+            
+            if not our_position:
+                self.logger.debug(f"⚠️ No open position found for {symbol} - skipping take profit copy")
+                return
+            
+            # Calculate our order size based on position ratio
+            our_position_size = abs(float(our_position.get('contracts', 0)))
+            our_position_side = 'long' if float(our_position.get('contracts', 0)) > 0 else 'short'
+            
+            # Determine take profit side (opposite of position)
+            tp_side = 'sell' if our_position_side == 'long' else 'buy'
+            
+            # Check if we already have a take profit for this position
+            existing_orders = await asyncio.to_thread(self.client.get_open_orders, trading_symbol)
+            if existing_orders:
+                for existing_order in existing_orders:
+                    if ('take' in existing_order.get('type', '').lower() or 
+                        'tp' in existing_order.get('type', '').lower() or
+                        existing_order.get('side') == tp_side):
+                        # Check if it's a limit order in profit direction
+                        current_price = float(our_position.get('markPrice', 0))
+                        order_price = float(existing_order.get('price', 0))
+                        
+                        # Skip if we already have a profitable exit order
+                        if ((our_position_side == 'long' and order_price > current_price) or
+                            (our_position_side == 'short' and order_price < current_price)):
+                            self.logger.debug(f"📋 Take profit already exists for {symbol} - skipping")
+                            return
+            
+            # Place the take profit order (using limit order)
+            self.logger.info(f"🎯 Placing take profit for {symbol}: {tp_side} {our_position_size:.6f} @ ${trigger_price}")
+            
+            result, error = await asyncio.to_thread(
+                self.client.place_take_profit_order,
+                symbol=trading_symbol,
+                side=tp_side,
+                amount=our_position_size,
+                take_profit_price_arg=float(trigger_price)
+            )
+            
+            if error:
+                self.logger.error(f"❌ Failed to place take profit for {symbol}: {error}")
+            else:
+                self.logger.info(f"✅ Successfully copied take profit for {symbol}")
+                
+                # Send notification
+                if self.notifications_enabled:
+                    await self.notification_manager.send_generic_notification(
+                        f"🎯 Take Profit Copied\n"
+                        f"Symbol: {symbol}\n"
+                        f"Target: ${trigger_price}\n"
+                        f"Size: {our_position_size:.6f}\n"
+                        f"Side: {tp_side}"
+                    )
+                    
+        except Exception as e:
+            self.logger.error(f"❌ Error copying take profit for {symbol}: {e}")
+
     async def _execute_hyperliquid_trade(self, trade: CopyTrade, position_calc: Dict[str, float], leverage: float) -> bool:
-        """Execute trade on Hyperliquid"""
+        """Execute trade on Hyperliquid using market orders (fallback method)"""
         try:
             # Determine if this is a buy or sell order
             is_buy = 'long' in trade.action or ('close' in trade.action and 'short' in trade.action)
@@ -640,7 +924,7 @@ class CopyTradingMonitor:
             margin_used = position_calc['margin_to_use']
             position_value = position_calc['position_value']
             
-            self.logger.info(f"🔄 Executing {trade.action} for {trade.coin}:")
+            self.logger.info(f"🔄 Executing {trade.action} for {trade.coin} with MARKET order:")
             self.logger.info(f"  📊 Side: {side}")
             self.logger.info(f"  🪙 Token Amount: {token_amount:.6f} {trade.coin}")
             self.logger.info(f"  💳 Margin: ${margin_used:.2f}")
@@ -661,7 +945,6 @@ class CopyTradingMonitor:
                 )
                 if leverage_error:
                     self.logger.warning(f"⚠️ Failed to set leverage to {leverage}x for {trade.coin}: {leverage_error}")
-                    # Continue anyway - leverage might already be set or order might still work
                 else:
                     self.logger.info(f"✅ Successfully set leverage to {leverage}x for {trade.coin}")
                 
@@ -777,7 +1060,7 @@ class CopyTradingMonitor:
         except Exception as e:
             self.logger.error(f"❌ Error executing Hyperliquid trade: {e}")
             return False
-    
+
     async def sync_positions(self):
         """Sync our current positions with tracking"""
         try:

+ 1 - 1
trading_bot.py

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