import asyncio import logging from typing import Optional, Dict, Any, List from hyperliquid import HyperliquidSync, HyperliquidAsync from config import Config # Use existing logger setup (will be configured by main application) logger = logging.getLogger(__name__) class HyperliquidClient: """Wrapper class for Hyperliquid API client with enhanced functionality.""" def __init__(self, use_testnet: bool = None): """ Initialize the Hyperliquid client with CCXT-style configuration. Args: use_testnet: Whether to use testnet (default: from Config.HYPERLIQUID_TESTNET) """ # Use config value if not explicitly provided if use_testnet is None: use_testnet = Config.HYPERLIQUID_TESTNET self.use_testnet = use_testnet # Get CCXT-style configuration self.config = Config.get_hyperliquid_config() # Override testnet setting if provided if use_testnet is not None: self.config['testnet'] = use_testnet self.config['sandbox'] = use_testnet # Ensure proper CCXT format # Hyperliquid CCXT expects: apiKey=API_generator_key, walletAddress=wallet_address if not self.config.get('apiKey') and Config.HYPERLIQUID_SECRET_KEY: self.config['apiKey'] = Config.HYPERLIQUID_SECRET_KEY # API generator key if not self.config.get('walletAddress') and Config.HYPERLIQUID_WALLET_ADDRESS: self.config['walletAddress'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address if not self.config.get('secret') and Config.HYPERLIQUID_WALLET_ADDRESS: self.config['secret'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address as secret too # Initialize clients self.sync_client = None self.async_client = None if self.config.get('privateKey') or self.config.get('apiKey'): try: # Log configuration (safely) logger.info(f"🔧 Initializing Hyperliquid client with config: {self._safe_config_log()}") # Initialize with Hyperliquid-specific CCXT format ccxt_config = { 'apiKey': self.config.get('apiKey'), 'privateKey': self.config.get('apiKey'), # Same as apiKey for Hyperliquid 'testnet': self.config.get('testnet', False), 'sandbox': self.config.get('sandbox', False), } # Add walletAddress - this is required by CCXT Hyperliquid if self.config.get('walletAddress'): ccxt_config['walletAddress'] = self.config['walletAddress'] # Add secret if available if self.config.get('secret'): ccxt_config['secret'] = self.config['secret'] logger.info(f"📋 Using CCXT config structure: {self._safe_ccxt_config_log(ccxt_config)}") # Initialize with the proper CCXT format self.sync_client = HyperliquidSync(ccxt_config) logger.info(f"✅ Hyperliquid client initialized successfully") logger.info(f"🌐 Network: {'Testnet' if use_testnet else '🚨 MAINNET 🚨'}") # Test the connection self._test_connection() except Exception as e: logger.error(f"❌ Failed to initialize Hyperliquid client: {e}") logger.error(f"💡 Config used: {self._safe_config_log()}") raise else: logger.warning("⚠️ No private key provided - client will have limited functionality") def _safe_config_log(self) -> Dict[str, Any]: """Return config with sensitive data masked for logging.""" safe_config = self.config.copy() if 'apiKey' in safe_config and safe_config['apiKey']: safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..." if 'walletAddress' in safe_config and safe_config['walletAddress']: safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..." if 'secret' in safe_config and safe_config['secret']: safe_config['secret'] = f"{safe_config['secret'][:8]}..." return safe_config def _safe_ccxt_config_log(self, config: dict) -> dict: """Return CCXT config with sensitive data masked for logging.""" safe_config = config.copy() if 'apiKey' in safe_config and safe_config['apiKey']: safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..." if 'walletAddress' in safe_config and safe_config['walletAddress']: safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..." if 'secret' in safe_config and safe_config['secret']: safe_config['secret'] = f"{safe_config['secret'][:8]}..." return safe_config def _test_connection(self): """Test the connection to verify credentials.""" try: # Try to fetch balance to test authentication # Use the same logic as get_balance for consistency params = {} if Config.HYPERLIQUID_WALLET_ADDRESS: wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address balance = self.sync_client.fetch_balance(params=params) logger.info(f"🔗 Connection test successful") except Exception as e: logger.warning(f"⚠️ Connection test failed: {e}") logger.warning("💡 This might be normal if you have no positions/balance") def get_balance(self) -> Optional[Dict[str, Any]]: """Get account balance.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None # For Hyperliquid, we need to pass the wallet address/user parameter # The user parameter should be the wallet address derived from private key params = {} # If we have a wallet address, use it as the user parameter if Config.HYPERLIQUID_WALLET_ADDRESS: # Extract the wallet address # For CCXT Hyperliquid, the user parameter should be the wallet address wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS if wallet_address.startswith('0x'): # Use the address as the user parameter params['user'] = wallet_address else: # Add 0x prefix if missing params['user'] = f"0x{wallet_address}" logger.debug(f"🔍 Fetching balance with params: {params}") balance = self.sync_client.fetch_balance(params=params) logger.info("✅ Successfully fetched balance") return balance except Exception as e: logger.error(f"❌ Error fetching balance: {e}") logger.debug(f"💡 Attempted with params: {params}") return None def get_balance_alternative(self) -> Optional[Dict[str, Any]]: """Alternative balance fetching method trying different approaches.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Try different approaches for balance fetching approaches = [ # Approach 1: No params (original) {}, # Approach 2: Wallet address as user {'user': Config.HYPERLIQUID_WALLET_ADDRESS}, # Approach 3: Wallet address with 0x prefix {'user': f"0x{Config.HYPERLIQUID_WALLET_ADDRESS}" if Config.HYPERLIQUID_WALLET_ADDRESS and not Config.HYPERLIQUID_WALLET_ADDRESS.startswith('0x') else Config.HYPERLIQUID_WALLET_ADDRESS}, # Approach 4: Empty user {'user': ''}, ] for i, params in enumerate(approaches, 1): try: logger.info(f"🔍 Trying approach {i}: {params}") balance = self.sync_client.fetch_balance(params=params) logger.info(f"✅ Approach {i} successful!") return balance except Exception as e: logger.warning(f"⚠️ Approach {i} failed: {e}") continue logger.error("❌ All approaches failed") return None except Exception as e: logger.error(f"❌ Error in alternative balance fetch: {e}") return None def get_positions(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: """Get current positions.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Add user parameter for Hyperliquid CCXT compatibility params = {} if Config.HYPERLIQUID_WALLET_ADDRESS: wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address logger.debug(f"🔍 Fetching positions with params: {params}") positions = self.sync_client.fetch_positions([symbol] if symbol else None, params=params) logger.info(f"✅ Successfully fetched positions for {symbol or 'all symbols'}") return positions except Exception as e: logger.error(f"❌ Error fetching positions: {e}") logger.debug(f"💡 Attempted with params: {params}") return None def get_market_data(self, symbol: str) -> Optional[Dict[str, Any]]: """Get market data for a symbol.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None ticker = self.sync_client.fetch_ticker(symbol) orderbook = self.sync_client.fetch_order_book(symbol) market_data = { 'ticker': ticker, 'orderbook': orderbook, 'symbol': symbol } logger.info(f"✅ Successfully fetched market data for {symbol}") return market_data except Exception as e: logger.error(f"❌ Error fetching market data for {symbol}: {e}") return None def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """ Place a limit order with CCXT-style parameters. Args: symbol: Trading symbol (e.g., 'BTC/USDC:USDC') side: 'buy' or 'sell' amount: Order amount price: Order price params: Additional parameters for CCXT compatibility """ try: if not self.sync_client: logger.error("❌ Client not initialized") return None # CCXT-style order creation order_params = params or {} order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params) logger.info(f"✅ Successfully placed {side} limit order for {amount} {symbol} at ${price}") logger.debug(f"📄 Order details: {order}") return order except Exception as e: logger.error(f"❌ Error placing limit order: {e}") return None def place_market_order(self, symbol: str, side: str, amount: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """ Place a market order with CCXT-style parameters. Args: symbol: Trading symbol (e.g., 'BTC/USDC:USDC') side: 'buy' or 'sell' amount: Order amount params: Additional parameters for CCXT compatibility """ try: if not self.sync_client: logger.error("❌ Client not initialized") return None # CCXT-style order creation order_params = params or {} order = self.sync_client.create_market_order(symbol, side, amount, params=order_params) logger.info(f"✅ Successfully placed {side} market order for {amount} {symbol}") logger.debug(f"📄 Order details: {order}") return order except Exception as e: logger.error(f"❌ Error placing market order: {e}") return None def get_open_orders(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: """Get open orders.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Add user parameter for Hyperliquid CCXT compatibility params = {} if Config.HYPERLIQUID_WALLET_ADDRESS: wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address logger.debug(f"🔍 Fetching open orders with params: {params}") orders = self.sync_client.fetch_open_orders(symbol, params=params) logger.info(f"✅ Successfully fetched open orders for {symbol or 'all symbols'}") return orders except Exception as e: logger.error(f"❌ Error fetching open orders: {e}") logger.debug(f"💡 Attempted with params: {params}") return None def cancel_order(self, order_id: str, symbol: str, params: Optional[Dict] = None) -> bool: """Cancel an order with CCXT-style parameters.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return False cancel_params = params or {} result = self.sync_client.cancel_order(order_id, symbol, params=cancel_params) logger.info(f"✅ Successfully cancelled order {order_id}") return True except Exception as e: logger.error(f"❌ Error cancelling order {order_id}: {e}") return False def get_recent_trades(self, symbol: str, limit: int = 10) -> Optional[List[Dict[str, Any]]]: """Get recent trades for a symbol.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None trades = self.sync_client.fetch_trades(symbol, limit=limit) logger.info(f"✅ Successfully fetched {len(trades)} recent trades for {symbol}") return trades except Exception as e: logger.error(f"❌ Error fetching recent trades for {symbol}: {e}") return None def get_trading_fee(self, symbol: str) -> Optional[Dict[str, Any]]: """Get trading fee for a symbol.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None fee = self.sync_client.fetch_trading_fee(symbol) logger.info(f"✅ Successfully fetched trading fee for {symbol}") return fee except Exception as e: logger.error(f"❌ Error fetching trading fee for {symbol}: {e}") return None def get_markets(self) -> Optional[Dict[str, Any]]: """Get available markets/symbols.""" try: if not self.sync_client: logger.error("❌ Client not initialized") return None markets = self.sync_client.load_markets() logger.info(f"✅ Successfully loaded {len(markets)} markets") return markets except Exception as e: logger.error(f"❌ Error loading markets: {e}") return None def place_stop_loss_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """ Place a stop loss order (implemented as a limit order). Args: symbol: Trading symbol (e.g., 'BTC/USDC:USDC') side: 'buy' or 'sell' amount: Order amount price: Stop loss price params: Additional parameters for CCXT compatibility """ try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Stop loss orders are implemented as limit orders # They will be filled when the market price reaches the stop price order_params = params or {} # Add order type information for clarity in logs logger.info(f"🛑 Placing stop loss order: {side} {amount} {symbol} @ ${price}") order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params) logger.info(f"✅ Successfully placed stop loss order for {amount} {symbol} at ${price}") logger.debug(f"📄 Stop loss order details: {order}") return order except Exception as e: logger.error(f"❌ Error placing stop loss order: {e}") return None def place_take_profit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """ Place a take profit order (implemented as a limit order). Args: symbol: Trading symbol (e.g., 'BTC/USDC:USDC') side: 'buy' or 'sell' amount: Order amount price: Take profit price params: Additional parameters for CCXT compatibility """ try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Take profit orders are implemented as limit orders # They will be filled when the market price reaches the target price order_params = params or {} # Add order type information for clarity in logs logger.info(f"🎯 Placing take profit order: {side} {amount} {symbol} @ ${price}") order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params) logger.info(f"✅ Successfully placed take profit order for {amount} {symbol} at ${price}") logger.debug(f"📄 Take profit order details: {order}") return order except Exception as e: logger.error(f"❌ Error placing take profit order: {e}") return None def get_recent_fills(self, limit: int = 100) -> Optional[List[Dict[str, Any]]]: """ Get recent fills/trades for the account. Args: limit: Maximum number of fills to return Returns: List of recent fills/trades or None if error """ try: if not self.sync_client: logger.error("❌ Client not initialized") return None # Add user parameter for Hyperliquid CCXT compatibility params = {} if Config.HYPERLIQUID_WALLET_ADDRESS: wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address # Fetch recent trades/fills for the account # Use fetch_my_trades to get account-specific trades logger.debug(f"🔍 Fetching recent fills with params: {params}") # Get recent fills across all symbols # We'll fetch trades for all symbols and merge them try: # Option 1: Try fetch_my_trades if available fills = self.sync_client.fetch_my_trades(None, limit=limit, params=params) logger.info(f"✅ Successfully fetched {len(fills)} recent fills") return fills except AttributeError: # Option 2: If fetch_my_trades not available, try alternative approach logger.debug("fetch_my_trades not available, trying alternative approach") # Get positions to determine active symbols positions = self.get_positions() if not positions: logger.info("No positions found, no recent fills to fetch") return [] # Get symbols from positions symbols = list(set([pos.get('symbol') for pos in positions if pos.get('symbol')])) all_fills = [] for symbol in symbols[:5]: # Limit to 5 symbols to avoid too many requests try: symbol_trades = self.sync_client.fetch_my_trades(symbol, limit=limit//len(symbols), params=params) if symbol_trades: all_fills.extend(symbol_trades) except Exception as e: logger.warning(f"Could not fetch trades for {symbol}: {e}") continue # Sort by timestamp (newest first) all_fills.sort(key=lambda x: x.get('timestamp', ''), reverse=True) # Return only the requested limit result = all_fills[:limit] logger.info(f"✅ Successfully fetched {len(result)} recent fills from {len(symbols)} symbols") return result except Exception as e: logger.error(f"❌ Error fetching recent fills: {e}") logger.debug(f"💡 Attempted with params: {params}") return None