hyperliquid_client.py 22 KB

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