Bläddra i källkod

Enhance CopyTradingMonitor with detailed logging and position sizing improvements

- Added comprehensive logging of configuration parameters during initialization for better visibility.
- Improved position size calculation to handle leverage and current price, ensuring accurate margin allocation.
- Updated trade execution logic to utilize calculated position values, enhancing trade accuracy.
- Introduced a test method for balance fetching and position sizing, aiding in debugging and validation of trading logic.
Carles Sentis 4 dagar sedan
förälder
incheckning
384b4ed128
2 ändrade filer med 276 tillägg och 68 borttagningar
  1. 275 67
      src/monitoring/copy_trading_monitor.py
  2. 1 1
      trading_bot.py

+ 275 - 67
src/monitoring/copy_trading_monitor.py

@@ -80,6 +80,15 @@ class CopyTradingMonitor:
         self.info_url = "https://api.hyperliquid.xyz/info"
         
         self.logger.info(f"Copy Trading Monitor initialized - Target: {self.target_address}")
+        self.logger.info(f"📊 Configuration:")
+        self.logger.info(f"  - Enabled: {self.enabled}")
+        self.logger.info(f"  - Target Address: {self.target_address}")
+        self.logger.info(f"  - Portfolio %: {self.portfolio_percentage:.1%}")
+        self.logger.info(f"  - Copy Mode: {self.copy_mode}")
+        self.logger.info(f"  - Max Leverage: {self.max_leverage}x")
+        self.logger.info(f"  - Min Position Size: ${self.min_position_size:.2f}")
+        self.logger.info(f"  - Execution Delay: {self.execution_delay}s")
+        self.logger.info(f"  - Notifications: {self.notifications_enabled}")
         
         # Load previous session info if available
         session_info = self.state_manager.get_session_info()
@@ -384,23 +393,42 @@ class CopyTradingMonitor:
                 self.logger.debug(f"Skipping already copied trade: {trade.original_trade_hash}")
                 return
             
-            # Calculate our position size
-            our_size = await self.calculate_position_size(trade)
-            
-            if our_size < self.min_position_size:
-                self.logger.info(f"Skipping {trade.coin} trade - size too small: ${our_size:.2f}")
-                # Still mark as copied to avoid retrying
+            # Get current price for the asset
+            symbol = f"{trade.coin}/USDC:USDC"
+            try:
+                market_data = await asyncio.to_thread(self.client.get_market_data, symbol)
+                if not market_data or not market_data.get('ticker'):
+                    self.logger.error(f"❌ Could not get market data for {trade.coin}")
+                    await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
+                    return
+                    
+                current_price = float(market_data['ticker'].get('last', 0))
+                if current_price <= 0:
+                    self.logger.error(f"❌ Invalid price for {trade.coin}: {current_price}")
+                    await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
+                    return
+            except Exception as e:
+                self.logger.error(f"❌ Error getting price for {trade.coin}: {e}")
                 await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
                 return
             
             # Apply leverage limit
             leverage = min(trade.leverage, self.max_leverage)
             
+            # Calculate our position size with proper leverage handling
+            position_calc = await self.calculate_position_size(trade, current_price, leverage)
+            
+            if position_calc['margin_to_use'] < self.min_position_size:
+                self.logger.info(f"Skipping {trade.coin} trade - margin too small: ${position_calc['margin_to_use']:.2f} (min: ${self.min_position_size:.2f})")
+                # Still mark as copied to avoid retrying
+                await self.state_manager.add_copied_trade_async(trade.original_trade_hash)
+                return
+            
             # Add execution delay
             await asyncio.sleep(self.execution_delay)
             
             # Execute the trade
-            success = await self._execute_hyperliquid_trade(trade, our_size, leverage)
+            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)
@@ -412,8 +440,10 @@ class CopyTradingMonitor:
                     f"🔄 Copy Trade {status}\n"
                     f"Action: {trade.action}\n"
                     f"Asset: {trade.coin}\n"
-                    f"Size: ${our_size:.2f}\n"
-                    f"Leverage: {leverage}x\n"
+                    f"💳 Margin Used: ${position_calc['margin_to_use']:.2f}\n"
+                    f"🏦 Position Value: ${position_calc['position_value']:.2f}\n"
+                    f"🪙 Token Amount: {position_calc['token_amount']:.6f}\n"
+                    f"⚖️ Leverage: {leverage}x\n"
                     f"Target: {trade.target_trader_address[:10]}...\n"
                     f"Trade ID: {trade.original_trade_hash[:16]}..."
                 )
@@ -433,53 +463,130 @@ class CopyTradingMonitor:
                     f"Trade ID: {trade.original_trade_hash[:16]}..."
                 )
     
-    async def calculate_position_size(self, trade: CopyTrade) -> float:
-        """Calculate our position size based on the copy trading mode"""
+    async def calculate_position_size(self, trade: CopyTrade, current_price: float, leverage: float) -> Dict[str, float]:
+        """
+        Calculate our position size based on the copy trading mode.
+        Returns margin allocation and converts to actual token amount.
+        
+        Args:
+            trade: The copy trade to execute
+            current_price: Current price of the asset
+            leverage: Leverage to use for the trade
+            
+        Returns:
+            Dict with 'margin_to_use', 'position_value', 'token_amount'
+        """
         try:
             # Get our current account balance
             our_balance = await self.get_our_account_balance()
+            self.logger.info(f"📊 Our account balance: ${our_balance:.2f}")
+            self.logger.info(f"🎯 Portfolio percentage: {self.portfolio_percentage:.1%}")
+            self.logger.info(f"📈 Copy mode: {self.copy_mode}")
+            self.logger.info(f"💰 Current {trade.coin} price: ${current_price:.4f}")
+            self.logger.info(f"⚖️ Leverage to use: {leverage:.1f}x")
+            
+            # Calculate the MARGIN we want to allocate (risk-based)
+            margin_to_use = 0.0
             
             if self.copy_mode == 'FIXED':
-                # Fixed percentage of our account
-                return our_balance * self.portfolio_percentage
+                # Fixed percentage of our account as margin
+                margin_to_use = our_balance * self.portfolio_percentage
+                self.logger.info(f"🔢 FIXED mode - margin to allocate: ${margin_to_use:.2f}")
                 
             elif self.copy_mode == 'PROPORTIONAL':
                 # Get target trader's account balance
                 target_balance = await self.get_target_account_balance()
+                self.logger.info(f"🎯 Target balance: ${target_balance:.2f}")
                 
                 if target_balance <= 0:
-                    return our_balance * self.portfolio_percentage
-                
-                # Calculate target trader's position percentage
-                target_pos = self.target_positions.get(trade.coin)
-                if not target_pos:
-                    return our_balance * self.portfolio_percentage
-                
-                target_position_percentage = target_pos.margin_used / target_balance
-                
-                # Apply same percentage to our account
-                our_position_size = our_balance * target_position_percentage
-                
-                # Cap at our portfolio percentage limit
-                max_size = our_balance * self.portfolio_percentage
-                return min(our_position_size, max_size)
-                
+                    margin_to_use = our_balance * self.portfolio_percentage
+                    self.logger.info(f"🔢 PROPORTIONAL mode (fallback) - margin: ${margin_to_use:.2f}")
+                else:
+                    # Calculate target trader's margin percentage
+                    target_pos = self.target_positions.get(trade.coin)
+                    if not target_pos:
+                        margin_to_use = our_balance * self.portfolio_percentage
+                        self.logger.info(f"🔢 PROPORTIONAL mode (no target pos) - margin: ${margin_to_use:.2f}")
+                    else:
+                        target_margin_percentage = target_pos.margin_used / target_balance
+                        self.logger.info(f"📊 Target margin percentage: {target_margin_percentage:.1%}")
+                        
+                        # Apply same margin percentage to our account
+                        our_proportional_margin = our_balance * target_margin_percentage
+                        
+                        # Cap at our portfolio percentage limit
+                        max_margin = our_balance * self.portfolio_percentage
+                        margin_to_use = min(our_proportional_margin, max_margin)
+                        
+                        self.logger.info(f"🔢 PROPORTIONAL mode - uncapped: ${our_proportional_margin:.2f}, max: ${max_margin:.2f}, final: ${margin_to_use:.2f}")
+                        
             else:
-                return our_balance * self.portfolio_percentage
+                margin_to_use = our_balance * self.portfolio_percentage
+                self.logger.info(f"🔢 Unknown mode (fallback) - margin: ${margin_to_use:.2f}")
+            
+            # Calculate position value with leverage
+            position_value = margin_to_use * leverage
+            
+            # Calculate token amount based on current price
+            token_amount = position_value / current_price
+            
+            result = {
+                'margin_to_use': margin_to_use,
+                'position_value': position_value,
+                'token_amount': token_amount
+            }
+            
+            self.logger.info(f"📊 Position calculation:")
+            self.logger.info(f"  💳 Margin to use: ${margin_to_use:.2f}")
+            self.logger.info(f"  🏦 Position value (with {leverage:.1f}x): ${position_value:.2f}")
+            self.logger.info(f"  🪙 Token amount: {token_amount:.6f} {trade.coin}")
+            
+            return result
                 
         except Exception as e:
             self.logger.error(f"Error calculating position size: {e}")
             # Fallback to fixed percentage
             our_balance = await self.get_our_account_balance()
-            return our_balance * self.portfolio_percentage
+            fallback_margin = our_balance * self.portfolio_percentage
+            fallback_value = fallback_margin * leverage
+            fallback_tokens = fallback_value / current_price
+            
+            result = {
+                'margin_to_use': fallback_margin,
+                'position_value': fallback_value,
+                'token_amount': fallback_tokens
+            }
+            
+            self.logger.info(f"🔢 Error fallback - margin: ${fallback_margin:.2f}, tokens: {fallback_tokens:.6f}")
+            return result
     
     async def get_our_account_balance(self) -> float:
         """Get our account balance"""
         try:
-            balance_info = self.client.get_balance()
+            balance_info = await asyncio.to_thread(self.client.get_balance)
+            self.logger.info(f"🔍 Raw balance info: {balance_info}")
+            
             if balance_info:
-                return float(balance_info.get('accountValue', 0))
+                # Use the same approach as the /balance command
+                usdc_total = 0.0
+                usdc_free = 0.0
+                usdc_used = 0.0
+                
+                if 'USDC' in balance_info.get('total', {}):
+                    usdc_total = float(balance_info['total']['USDC'])
+                    usdc_free = float(balance_info.get('free', {}).get('USDC', 0))
+                    usdc_used = float(balance_info.get('used', {}).get('USDC', 0))
+                
+                self.logger.info(f"💰 USDC Balance - Total: ${usdc_total:.2f}, Free: ${usdc_free:.2f}, Used: ${usdc_used:.2f}")
+                
+                if usdc_total > 0:
+                    self.logger.info(f"📊 Using total USDC balance: ${usdc_total:.2f}")
+                    return usdc_total
+                else:
+                    self.logger.warning(f"⚠️ No USDC balance found - raw response: {balance_info}")
+                    return 0.0
             else:
+                self.logger.warning("⚠️ No balance info returned")
                 return 0.0
         except Exception as e:
             self.logger.error(f"Error getting our account balance: {e}")
@@ -510,58 +617,105 @@ class CopyTradingMonitor:
             self.logger.error(f"Error getting target account balance: {e}")
             return 0.0
     
-    async def _execute_hyperliquid_trade(self, trade: CopyTrade, size: float, leverage: float) -> bool:
+    async def _execute_hyperliquid_trade(self, trade: CopyTrade, position_calc: Dict[str, float], leverage: float) -> bool:
         """Execute trade on Hyperliquid"""
         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)
-            
-            # For position opening/closing
-            if 'open' in trade.action:
-                # Open new position
-                result = await self.client.place_order(
-                    symbol=trade.coin,
-                    side='buy' if is_buy else 'sell',
-                    size=size,
-                    leverage=leverage,
-                    order_type='market'
+            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']
+            
+            self.logger.info(f"🔄 Executing {trade.action} for {trade.coin}:")
+            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}")
+            self.logger.info(f"  🏦 Position Value: ${position_value:.2f}")
+            self.logger.info(f"  ⚖️ Leverage: {leverage}x")
+            
+            # For position opening/closing/modifying - all use market orders
+            if 'open' in trade.action or 'add' in trade.action:
+                # Open new position or add to existing position
+                symbol = f"{trade.coin}/USDC:USDC"
+                result, error = await asyncio.to_thread(
+                    self.client.place_market_order,
+                    symbol=symbol,
+                    side=side,
+                    amount=token_amount
                 )
+                if error:
+                    self.logger.error(f"❌ Market order failed: {error}")
+                    return False
+                    
             elif 'close' in trade.action:
-                # Close existing position
-                result = await self.client.close_position(
-                    symbol=trade.coin,
-                    size=size
-                )
-            elif 'add' in trade.action:
-                # Add to existing position
-                result = await self.client.place_order(
-                    symbol=trade.coin,
-                    side='buy' if is_buy else 'sell',
-                    size=size,
-                    leverage=leverage,
-                    order_type='market'
+                # Close existing position - we need to place an opposite market order
+                # Get current position to determine the exact size to close
+                symbol = f"{trade.coin}/USDC:USDC"
+                positions = await asyncio.to_thread(self.client.get_positions, symbol=symbol)
+                if not positions:
+                    self.logger.warning(f"⚠️ No position found for {trade.coin} to close")
+                    return False
+                    
+                # Find the position to close
+                position_to_close = None
+                for pos in positions:
+                    if pos.get('symbol') == symbol and float(pos.get('contracts', 0)) != 0:
+                        position_to_close = pos
+                        break
+                
+                if not position_to_close:
+                    self.logger.warning(f"⚠️ No open position found for {trade.coin}")
+                    return False
+                
+                # Determine the opposite side to close the position
+                current_side = 'long' if float(position_to_close.get('contracts', 0)) > 0 else 'short'
+                close_side = 'sell' if current_side == 'long' else 'buy'
+                close_size = abs(float(position_to_close.get('contracts', 0)))
+                
+                self.logger.info(f"📉 Closing {current_side} position: {close_side} {close_size} {trade.coin}")
+                
+                result, error = await asyncio.to_thread(
+                    self.client.place_market_order,
+                    symbol=symbol,
+                    side=close_side,
+                    amount=close_size
                 )
+                if error:
+                    self.logger.error(f"❌ Close order failed: {error}")
+                    return False
+                    
             elif 'reduce' in trade.action:
                 # Reduce existing position
-                result = await self.client.place_order(
-                    symbol=trade.coin,
-                    side='sell' if 'long' in trade.action else 'buy',
-                    size=size,
-                    order_type='market'
+                reduce_side = 'sell' if 'long' in trade.action else 'buy'
+                symbol = f"{trade.coin}/USDC:USDC"
+                result, error = await asyncio.to_thread(
+                    self.client.place_market_order,
+                    symbol=symbol,
+                    side=reduce_side,
+                    amount=token_amount
                 )
+                if error:
+                    self.logger.error(f"❌ Reduce order failed: {error}")
+                    return False
+                    
             else:
-                self.logger.error(f"Unknown trade action: {trade.action}")
+                self.logger.error(f"Unknown trade action: {trade.action}")
                 return False
             
-            if result and result.get('success'):
-                self.logger.info(f"Successfully executed copy trade: {trade.action} {size} {trade.coin}")
+            # Check if result indicates success
+            if result:
+                self.logger.info(f"✅ Successfully executed copy trade: {trade.action}")
+                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 execute copy trade: {result}")
+                self.logger.error(f"❌ Failed to execute copy trade - no result returned")
                 return False
                 
         except Exception as e:
-            self.logger.error(f"Error executing Hyperliquid trade: {e}")
+            self.logger.error(f"Error executing Hyperliquid trade: {e}")
             return False
     
     async def sync_positions(self):
@@ -603,6 +757,60 @@ class CopyTradingMonitor:
                 f"\n\n💾 State saved - can resume later"
             )
     
+    async def test_balance_fetching(self) -> Dict[str, Any]:
+        """Test balance fetching and position sizing for debugging purposes"""
+        try:
+            our_balance = await self.get_our_account_balance()
+            target_balance = await self.get_target_account_balance()
+            
+            # Test with a mock trade for SOL
+            test_price = 150.0  # Mock SOL price
+            test_leverage = min(10.0, self.max_leverage)  # Test leverage
+            
+            mock_trade = CopyTrade(
+                coin="SOL",
+                action="open_long",
+                size=100,
+                leverage=test_leverage,
+                original_trade_hash="test_hash",
+                target_trader_address=self.target_address or "test_address",
+                timestamp=int(time.time() * 1000)
+            )
+            
+            # Calculate position with leverage
+            position_calc = await self.calculate_position_size(mock_trade, test_price, test_leverage)
+            
+            result = {
+                'our_balance': our_balance,
+                'target_balance': target_balance,
+                'portfolio_percentage': self.portfolio_percentage,
+                'test_price': test_price,
+                'test_leverage': test_leverage,
+                'margin_to_use': position_calc['margin_to_use'],
+                'position_value': position_calc['position_value'],
+                'token_amount': position_calc['token_amount'],
+                'min_position_size': self.min_position_size,
+                'would_execute': position_calc['margin_to_use'] >= self.min_position_size,
+                'config_enabled': self.enabled,
+                'state_enabled': self.state_manager.is_enabled()
+            }
+            
+            self.logger.info(f"🧪 Balance & Leverage test results:")
+            self.logger.info(f"  💰 Our balance: ${our_balance:.2f}")
+            self.logger.info(f"  🎯 Target balance: ${target_balance:.2f}")
+            self.logger.info(f"  📊 Portfolio allocation: {self.portfolio_percentage:.1%}")
+            self.logger.info(f"  ⚖️ Test leverage: {test_leverage:.1f}x")
+            self.logger.info(f"  💳 Margin to use: ${position_calc['margin_to_use']:.2f}")
+            self.logger.info(f"  🏦 Position value: ${position_calc['position_value']:.2f}")
+            self.logger.info(f"  🪙 Token amount: {position_calc['token_amount']:.6f} SOL")
+            self.logger.info(f"  ✅ Would execute: {position_calc['margin_to_use'] >= self.min_position_size}")
+            
+            return result
+            
+        except Exception as e:
+            self.logger.error(f"❌ Error in balance test: {e}")
+            return {'error': str(e)}
+    
     def get_status(self) -> Dict[str, Any]:
         """Get current copy trading status"""
         session_info = self.state_manager.get_session_info()

+ 1 - 1
trading_bot.py

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