hyperliquid_client.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import logging
  2. from typing import Optional, Dict, Any, List
  3. from hyperliquid import HyperliquidSync
  4. from config import Config
  5. # Use existing logger setup (will be configured by main application)
  6. logger = logging.getLogger(__name__)
  7. class HyperliquidClient:
  8. """Wrapper class for Hyperliquid API client with enhanced functionality."""
  9. def __init__(self, use_testnet: bool = None):
  10. """
  11. Initialize the Hyperliquid client with CCXT-style configuration.
  12. Args:
  13. use_testnet: Whether to use testnet (default: from Config.HYPERLIQUID_TESTNET)
  14. """
  15. # Use config value if not explicitly provided
  16. if use_testnet is None:
  17. use_testnet = Config.HYPERLIQUID_TESTNET
  18. self.use_testnet = use_testnet
  19. # Get CCXT-style configuration
  20. self.config = Config.get_hyperliquid_config()
  21. # Override testnet setting if provided
  22. if use_testnet is not None:
  23. self.config['testnet'] = use_testnet
  24. self.config['sandbox'] = use_testnet
  25. # Ensure proper CCXT format
  26. # Hyperliquid CCXT expects: apiKey=API_generator_key, walletAddress=wallet_address
  27. if not self.config.get('apiKey') and Config.HYPERLIQUID_SECRET_KEY:
  28. self.config['apiKey'] = Config.HYPERLIQUID_SECRET_KEY # API generator key
  29. if not self.config.get('walletAddress') and Config.HYPERLIQUID_WALLET_ADDRESS:
  30. self.config['walletAddress'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address
  31. if not self.config.get('secret') and Config.HYPERLIQUID_WALLET_ADDRESS:
  32. self.config['secret'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address as secret too
  33. # Initialize clients
  34. self.sync_client = None
  35. self.async_client = None
  36. if self.config.get('privateKey') or self.config.get('apiKey'):
  37. try:
  38. # Log configuration (safely)
  39. logger.info(f"🔧 Initializing Hyperliquid client with config: {self._safe_config_log()}")
  40. # Initialize with Hyperliquid-specific CCXT format
  41. ccxt_config = {
  42. 'apiKey': self.config.get('apiKey'),
  43. 'privateKey': self.config.get('apiKey'), # Same as apiKey for Hyperliquid
  44. 'testnet': self.config.get('testnet', False),
  45. 'sandbox': self.config.get('sandbox', False),
  46. }
  47. # Add walletAddress - this is required by CCXT Hyperliquid
  48. if self.config.get('walletAddress'):
  49. ccxt_config['walletAddress'] = self.config['walletAddress']
  50. # Add secret if available
  51. if self.config.get('secret'):
  52. ccxt_config['secret'] = self.config['secret']
  53. logger.info(f"📋 Using CCXT config structure: {self._safe_ccxt_config_log(ccxt_config)}")
  54. # Initialize with the proper CCXT format
  55. self.sync_client = HyperliquidSync(ccxt_config)
  56. logger.info(f"✅ Hyperliquid client initialized successfully")
  57. logger.info(f"🌐 Network: {'Testnet' if use_testnet else '🚨 MAINNET 🚨'}")
  58. # Test the connection
  59. self._test_connection()
  60. except Exception as e:
  61. logger.error(f"❌ Failed to initialize Hyperliquid client: {e}")
  62. logger.error(f"💡 Config used: {self._safe_config_log()}")
  63. raise
  64. else:
  65. logger.warning("⚠️ No private key provided - client will have limited functionality")
  66. def _safe_config_log(self) -> Dict[str, Any]:
  67. """Return config with sensitive data masked for logging."""
  68. safe_config = self.config.copy()
  69. if 'apiKey' in safe_config and safe_config['apiKey']:
  70. safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..."
  71. if 'walletAddress' in safe_config and safe_config['walletAddress']:
  72. safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..."
  73. if 'secret' in safe_config and safe_config['secret']:
  74. safe_config['secret'] = f"{safe_config['secret'][:8]}..."
  75. return safe_config
  76. def _safe_ccxt_config_log(self, config: dict) -> dict:
  77. """Return CCXT config with sensitive data masked for logging."""
  78. safe_config = config.copy()
  79. if 'apiKey' in safe_config and safe_config['apiKey']:
  80. safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..."
  81. if 'walletAddress' in safe_config and safe_config['walletAddress']:
  82. safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..."
  83. if 'secret' in safe_config and safe_config['secret']:
  84. safe_config['secret'] = f"{safe_config['secret'][:8]}..."
  85. return safe_config
  86. def _test_connection(self):
  87. """Test the connection to verify credentials."""
  88. try:
  89. # Try to fetch balance to test authentication
  90. # Use the same logic as get_balance for consistency
  91. params = {}
  92. if Config.HYPERLIQUID_WALLET_ADDRESS:
  93. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  94. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  95. balance = self.sync_client.fetch_balance(params=params)
  96. logger.info(f"🔗 Connection test successful")
  97. except Exception as e:
  98. logger.warning(f"⚠️ Connection test failed: {e}")
  99. logger.warning("💡 This might be normal if you have no positions/balance")
  100. def get_balance(self) -> Optional[Dict[str, Any]]:
  101. """Get account balance."""
  102. try:
  103. if not self.sync_client:
  104. logger.error("❌ Client not initialized")
  105. return None
  106. # For Hyperliquid, we need to pass the wallet address/user parameter
  107. # The user parameter should be the wallet address derived from private key
  108. params = {}
  109. # If we have a wallet address, use it as the user parameter
  110. if Config.HYPERLIQUID_WALLET_ADDRESS:
  111. # Extract the wallet address
  112. # For CCXT Hyperliquid, the user parameter should be the wallet address
  113. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  114. if wallet_address.startswith('0x'):
  115. # Use the address as the user parameter
  116. params['user'] = wallet_address
  117. else:
  118. # Add 0x prefix if missing
  119. params['user'] = f"0x{wallet_address}"
  120. logger.debug(f"🔍 Fetching balance with params: {params}")
  121. balance = self.sync_client.fetch_balance(params=params)
  122. logger.info("✅ Successfully fetched balance")
  123. return balance
  124. except Exception as e:
  125. logger.error(f"❌ Error fetching balance: {e}")
  126. logger.debug(f"💡 Attempted with params: {params}")
  127. return None
  128. def get_balance_alternative(self) -> Optional[Dict[str, Any]]:
  129. """Alternative balance fetching method trying different approaches."""
  130. try:
  131. if not self.sync_client:
  132. logger.error("❌ Client not initialized")
  133. return None
  134. # Try different approaches for balance fetching
  135. approaches = [
  136. # Approach 1: No params (original)
  137. {},
  138. # Approach 2: Wallet address as user
  139. {'user': Config.HYPERLIQUID_WALLET_ADDRESS},
  140. # Approach 3: Wallet address with 0x prefix
  141. {'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},
  142. # Approach 4: Empty user
  143. {'user': ''},
  144. ]
  145. for i, params in enumerate(approaches, 1):
  146. try:
  147. logger.info(f"🔍 Trying approach {i}: {params}")
  148. balance = self.sync_client.fetch_balance(params=params)
  149. logger.info(f"✅ Approach {i} successful!")
  150. return balance
  151. except Exception as e:
  152. logger.warning(f"⚠️ Approach {i} failed: {e}")
  153. continue
  154. logger.error("❌ All approaches failed")
  155. return None
  156. except Exception as e:
  157. logger.error(f"❌ Error in alternative balance fetch: {e}")
  158. return None
  159. def get_positions(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
  160. """Get current positions."""
  161. try:
  162. if not self.sync_client:
  163. logger.error("❌ Client not initialized")
  164. return None
  165. # Add user parameter for Hyperliquid CCXT compatibility
  166. params = {}
  167. if Config.HYPERLIQUID_WALLET_ADDRESS:
  168. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  169. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  170. logger.debug(f"🔍 Fetching positions with params: {params}")
  171. positions = self.sync_client.fetch_positions([symbol] if symbol else None, params=params)
  172. logger.info(f"✅ Successfully fetched positions for {symbol or 'all symbols'}")
  173. return positions
  174. except Exception as e:
  175. logger.error(f"❌ Error fetching positions: {e}")
  176. logger.debug(f"💡 Attempted with params: {params}")
  177. return None
  178. def get_market_data(self, symbol: str) -> Optional[Dict[str, Any]]:
  179. """Get market data for a symbol."""
  180. try:
  181. if not self.sync_client:
  182. logger.error("❌ Client not initialized")
  183. return None
  184. ticker = self.sync_client.fetch_ticker(symbol)
  185. orderbook = self.sync_client.fetch_order_book(symbol)
  186. market_data = {
  187. 'ticker': ticker,
  188. 'orderbook': orderbook,
  189. 'symbol': symbol
  190. }
  191. logger.info(f"✅ Successfully fetched market data for {symbol}")
  192. return market_data
  193. except Exception as e:
  194. logger.error(f"❌ Error fetching market data for {symbol}: {e}")
  195. return None
  196. def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
  197. """
  198. Place a limit order with CCXT-style parameters.
  199. Args:
  200. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  201. side: 'buy' or 'sell'
  202. amount: Order amount
  203. price: Order price
  204. params: Additional parameters for CCXT compatibility
  205. """
  206. try:
  207. if not self.sync_client:
  208. logger.error("❌ Client not initialized")
  209. return None
  210. # CCXT-style order creation
  211. order_params = params or {}
  212. order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params)
  213. logger.info(f"✅ Successfully placed {side} limit order for {amount} {symbol} at ${price}")
  214. logger.debug(f"📄 Order details: {order}")
  215. return order
  216. except Exception as e:
  217. logger.error(f"❌ Error placing limit order: {e}")
  218. return None
  219. def place_market_order(self, symbol: str, side: str, amount: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
  220. """
  221. Place a market order with CCXT-style parameters.
  222. Args:
  223. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  224. side: 'buy' or 'sell'
  225. amount: Order amount
  226. params: Additional parameters for CCXT compatibility
  227. """
  228. try:
  229. if not self.sync_client:
  230. logger.error("❌ Client not initialized")
  231. return None
  232. # Get current market price for slippage calculation
  233. ticker = self.sync_client.fetch_ticker(symbol)
  234. if not ticker:
  235. logger.error(f"❌ Could not fetch ticker for {symbol}")
  236. return None
  237. current_price = ticker.get('last')
  238. if not current_price:
  239. logger.error(f"❌ Could not get current price for {symbol}")
  240. return None
  241. # Apply slippage tolerance (default 0.5% for market orders)
  242. slippage_percent = 0.5 # 0.5% slippage
  243. if side == 'buy':
  244. # For buy orders, increase price to ensure fill
  245. slippage_price = current_price * (1 + slippage_percent / 100)
  246. else:
  247. # For sell orders, decrease price to ensure fill
  248. slippage_price = current_price * (1 - slippage_percent / 100)
  249. logger.info(f"🔄 Market order: {side} {amount} {symbol} @ current ${current_price:.2f} (slippage price: ${slippage_price:.2f})")
  250. # CCXT-style order creation with price for slippage
  251. order_params = params or {}
  252. order_params['price'] = slippage_price # Add price for slippage calculation
  253. order = self.sync_client.create_market_order(symbol, side, amount, price=slippage_price, params=order_params)
  254. logger.info(f"✅ Successfully placed {side} market order for {amount} {symbol}")
  255. logger.debug(f"📄 Order details: {order}")
  256. return order
  257. except Exception as e:
  258. logger.error(f"❌ Error placing market order: {e}")
  259. return None
  260. def get_open_orders(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
  261. """Get open orders."""
  262. try:
  263. if not self.sync_client:
  264. logger.error("❌ Client not initialized")
  265. return None
  266. # Add user parameter for Hyperliquid CCXT compatibility
  267. params = {}
  268. if Config.HYPERLIQUID_WALLET_ADDRESS:
  269. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  270. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  271. logger.debug(f"🔍 Fetching open orders with params: {params}")
  272. orders = self.sync_client.fetch_open_orders(symbol, params=params)
  273. logger.info(f"✅ Successfully fetched open orders for {symbol or 'all symbols'}")
  274. return orders
  275. except Exception as e:
  276. logger.error(f"❌ Error fetching open orders: {e}")
  277. logger.debug(f"💡 Attempted with params: {params}")
  278. return None
  279. def cancel_order(self, order_id: str, symbol: str, params: Optional[Dict] = None) -> bool:
  280. """Cancel an order with CCXT-style parameters."""
  281. try:
  282. if not self.sync_client:
  283. logger.error("❌ Client not initialized")
  284. return False
  285. cancel_params = params or {}
  286. result = self.sync_client.cancel_order(order_id, symbol, params=cancel_params)
  287. logger.info(f"✅ Successfully cancelled order {order_id}")
  288. return True
  289. except Exception as e:
  290. logger.error(f"❌ Error cancelling order {order_id}: {e}")
  291. return False
  292. def get_recent_trades(self, symbol: str, limit: int = 10) -> Optional[List[Dict[str, Any]]]:
  293. """Get recent trades for a symbol."""
  294. try:
  295. if not self.sync_client:
  296. logger.error("❌ Client not initialized")
  297. return None
  298. trades = self.sync_client.fetch_trades(symbol, limit=limit)
  299. logger.info(f"✅ Successfully fetched {len(trades)} recent trades for {symbol}")
  300. return trades
  301. except Exception as e:
  302. logger.error(f"❌ Error fetching recent trades for {symbol}: {e}")
  303. return None
  304. def get_trading_fee(self, symbol: str) -> Optional[Dict[str, Any]]:
  305. """Get trading fee for a symbol."""
  306. try:
  307. if not self.sync_client:
  308. logger.error("❌ Client not initialized")
  309. return None
  310. fee = self.sync_client.fetch_trading_fee(symbol)
  311. logger.info(f"✅ Successfully fetched trading fee for {symbol}")
  312. return fee
  313. except Exception as e:
  314. logger.error(f"❌ Error fetching trading fee for {symbol}: {e}")
  315. return None
  316. def get_markets(self) -> Optional[Dict[str, Any]]:
  317. """Get available markets/symbols."""
  318. try:
  319. if not self.sync_client:
  320. logger.error("❌ Client not initialized")
  321. return None
  322. markets = self.sync_client.load_markets()
  323. logger.info(f"✅ Successfully loaded {len(markets)} markets")
  324. return markets
  325. except Exception as e:
  326. logger.error(f"❌ Error loading markets: {e}")
  327. return None
  328. def place_stop_loss_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
  329. """
  330. Place a stop loss order (implemented as a limit order).
  331. Args:
  332. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  333. side: 'buy' or 'sell'
  334. amount: Order amount
  335. price: Stop loss price
  336. params: Additional parameters for CCXT compatibility
  337. """
  338. try:
  339. if not self.sync_client:
  340. logger.error("❌ Client not initialized")
  341. return None
  342. # Stop loss orders are implemented as limit orders
  343. # They will be filled when the market price reaches the stop price
  344. order_params = params or {}
  345. # Add order type information for clarity in logs
  346. logger.info(f"🛑 Placing stop loss order: {side} {amount} {symbol} @ ${price}")
  347. order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params)
  348. logger.info(f"✅ Successfully placed stop loss order for {amount} {symbol} at ${price}")
  349. logger.debug(f"📄 Stop loss order details: {order}")
  350. return order
  351. except Exception as e:
  352. logger.error(f"❌ Error placing stop loss order: {e}")
  353. return None
  354. def place_take_profit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
  355. """
  356. Place a take profit order (implemented as a limit order).
  357. Args:
  358. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  359. side: 'buy' or 'sell'
  360. amount: Order amount
  361. price: Take profit price
  362. params: Additional parameters for CCXT compatibility
  363. """
  364. try:
  365. if not self.sync_client:
  366. logger.error("❌ Client not initialized")
  367. return None
  368. # Take profit orders are implemented as limit orders
  369. # They will be filled when the market price reaches the target price
  370. order_params = params or {}
  371. # Add order type information for clarity in logs
  372. logger.info(f"🎯 Placing take profit order: {side} {amount} {symbol} @ ${price}")
  373. order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params)
  374. logger.info(f"✅ Successfully placed take profit order for {amount} {symbol} at ${price}")
  375. logger.debug(f"📄 Take profit order details: {order}")
  376. return order
  377. except Exception as e:
  378. logger.error(f"❌ Error placing take profit order: {e}")
  379. return None
  380. def get_recent_fills(self, limit: int = 100) -> Optional[List[Dict[str, Any]]]:
  381. """
  382. Get recent fills/trades for the account.
  383. Args:
  384. limit: Maximum number of fills to return
  385. Returns:
  386. List of recent fills/trades or None if error
  387. """
  388. try:
  389. if not self.sync_client:
  390. logger.error("❌ Client not initialized")
  391. return None
  392. # Add user parameter for Hyperliquid CCXT compatibility
  393. params = {}
  394. if Config.HYPERLIQUID_WALLET_ADDRESS:
  395. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  396. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  397. # Fetch recent trades/fills for the account
  398. # Use fetch_my_trades to get account-specific trades
  399. logger.debug(f"🔍 Fetching recent fills with params: {params}")
  400. # Get recent fills across all symbols
  401. # We'll fetch trades for all symbols and merge them
  402. try:
  403. # Option 1: Try fetch_my_trades if available
  404. fills = self.sync_client.fetch_my_trades(None, limit=limit, params=params)
  405. logger.info(f"✅ Successfully fetched {len(fills)} recent fills")
  406. return fills
  407. except AttributeError:
  408. # Option 2: If fetch_my_trades not available, try alternative approach
  409. logger.debug("fetch_my_trades not available, trying alternative approach")
  410. # Get positions to determine active symbols
  411. positions = self.get_positions()
  412. if not positions:
  413. logger.info("No positions found, no recent fills to fetch")
  414. return []
  415. # Get symbols from positions
  416. symbols = list(set([pos.get('symbol') for pos in positions if pos.get('symbol')]))
  417. all_fills = []
  418. for symbol in symbols[:5]: # Limit to 5 symbols to avoid too many requests
  419. try:
  420. symbol_trades = self.sync_client.fetch_my_trades(symbol, limit=limit//len(symbols), params=params)
  421. if symbol_trades:
  422. all_fills.extend(symbol_trades)
  423. except Exception as e:
  424. logger.warning(f"Could not fetch trades for {symbol}: {e}")
  425. continue
  426. # Sort by timestamp (newest first)
  427. all_fills.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
  428. # Return only the requested limit
  429. result = all_fills[:limit]
  430. logger.info(f"✅ Successfully fetched {len(result)} recent fills from {len(symbols)} symbols")
  431. return result
  432. except Exception as e:
  433. logger.error(f"❌ Error fetching recent fills: {e}")
  434. logger.debug(f"💡 Attempted with params: {params}")
  435. return None