hyperliquid_client.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. import logging
  2. from typing import Optional, Dict, Any, List, Tuple
  3. from hyperliquid import HyperliquidSync
  4. from src.config.config import Config
  5. import re
  6. import json
  7. # Use existing logger setup (will be configured by main application)
  8. logger = logging.getLogger(__name__)
  9. class HyperliquidClient:
  10. """Wrapper class for Hyperliquid API client with enhanced functionality."""
  11. def __init__(self, use_testnet: bool = None):
  12. """
  13. Initialize the Hyperliquid client with CCXT-style configuration.
  14. Args:
  15. use_testnet: Whether to use testnet (default: from Config.HYPERLIQUID_TESTNET)
  16. """
  17. # Use config value if not explicitly provided
  18. if use_testnet is None:
  19. use_testnet = Config.HYPERLIQUID_TESTNET
  20. self.use_testnet = use_testnet
  21. # Get CCXT-style configuration
  22. self.config = Config.get_hyperliquid_config()
  23. # Override testnet setting if provided
  24. if use_testnet is not None:
  25. self.config['testnet'] = use_testnet
  26. self.config['sandbox'] = use_testnet
  27. # Ensure proper CCXT format
  28. # Hyperliquid CCXT expects: apiKey=API_generator_key, walletAddress=wallet_address
  29. if not self.config.get('apiKey') and Config.HYPERLIQUID_SECRET_KEY:
  30. self.config['apiKey'] = Config.HYPERLIQUID_SECRET_KEY # API generator key
  31. if not self.config.get('walletAddress') and Config.HYPERLIQUID_WALLET_ADDRESS:
  32. self.config['walletAddress'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address
  33. if not self.config.get('secret') and Config.HYPERLIQUID_WALLET_ADDRESS:
  34. self.config['secret'] = Config.HYPERLIQUID_WALLET_ADDRESS # Wallet address as secret too
  35. # Initialize clients
  36. self.sync_client = None
  37. self.async_client = None
  38. if self.config.get('privateKey') or self.config.get('apiKey'):
  39. try:
  40. # Log configuration (safely)
  41. logger.info(f"🔧 Initializing Hyperliquid client with config: {self._safe_config_log()}")
  42. # Initialize with Hyperliquid-specific CCXT format
  43. ccxt_config = {
  44. 'apiKey': self.config.get('apiKey'),
  45. 'privateKey': self.config.get('apiKey'), # Same as apiKey for Hyperliquid
  46. 'testnet': self.config.get('testnet', False),
  47. 'sandbox': self.config.get('sandbox', False),
  48. }
  49. # Add walletAddress - this is required by CCXT Hyperliquid
  50. if self.config.get('walletAddress'):
  51. ccxt_config['walletAddress'] = self.config['walletAddress']
  52. # Add secret if available
  53. if self.config.get('secret'):
  54. ccxt_config['secret'] = self.config['secret']
  55. logger.info(f"📋 Using CCXT config structure: {self._safe_ccxt_config_log(ccxt_config)}")
  56. # Initialize with the proper CCXT format
  57. self.sync_client = HyperliquidSync(ccxt_config)
  58. logger.info(f"✅ Hyperliquid client initialized successfully")
  59. logger.info(f"🌐 Network: {'Testnet' if use_testnet else '🚨 MAINNET 🚨'}")
  60. # Test the connection
  61. self._test_connection()
  62. except Exception as e:
  63. logger.error(f"❌ Failed to initialize Hyperliquid client: {e}")
  64. logger.error(f"💡 Config used: {self._safe_config_log()}")
  65. raise
  66. else:
  67. logger.warning("⚠️ No private key provided - client will have limited functionality")
  68. def _safe_config_log(self) -> Dict[str, Any]:
  69. """Return config with sensitive data masked for logging."""
  70. safe_config = self.config.copy()
  71. if 'apiKey' in safe_config and safe_config['apiKey']:
  72. safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..."
  73. if 'walletAddress' in safe_config and safe_config['walletAddress']:
  74. safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..."
  75. if 'secret' in safe_config and safe_config['secret']:
  76. safe_config['secret'] = f"{safe_config['secret'][:8]}..."
  77. return safe_config
  78. def _safe_ccxt_config_log(self, config: dict) -> dict:
  79. """Return CCXT config with sensitive data masked for logging."""
  80. safe_config = config.copy()
  81. if 'apiKey' in safe_config and safe_config['apiKey']:
  82. safe_config['apiKey'] = f"{safe_config['apiKey'][:8]}..."
  83. if 'privateKey' in safe_config and safe_config['privateKey']:
  84. safe_config['privateKey'] = f"{safe_config['privateKey'][:8]}..."
  85. if 'walletAddress' in safe_config and safe_config['walletAddress']:
  86. safe_config['walletAddress'] = f"{safe_config['walletAddress'][:8]}..."
  87. if 'secret' in safe_config and safe_config['secret']:
  88. safe_config['secret'] = f"{safe_config['secret'][:8]}..."
  89. return safe_config
  90. def _test_connection(self):
  91. """Test the connection to verify credentials."""
  92. try:
  93. # Try to fetch balance to test authentication
  94. # Use the same logic as get_balance for consistency
  95. params = {}
  96. if Config.HYPERLIQUID_WALLET_ADDRESS:
  97. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  98. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  99. balance = self.sync_client.fetch_balance(params=params)
  100. logger.info(f"🔗 Connection test successful")
  101. except Exception as e:
  102. logger.warning(f"⚠️ Connection test failed: {e}")
  103. logger.warning("💡 This might be normal if you have no positions/balance")
  104. def _extract_error_message(self, exception_obj: Exception) -> str:
  105. """Extracts a more specific error message from a Hyperliquid exception."""
  106. error_str = str(exception_obj)
  107. # Attempt to parse the JSON-like structure in the error string
  108. # Example: hyperliquid {"status":"ok","response":{"type":"order","data":{"statuses":[{"error":"Insufficient margin..."}]}}}
  109. try:
  110. # Look for the start of the JSON part
  111. json_match = re.search(r'{\s*"status":.*}', error_str)
  112. if json_match:
  113. json_str = json_match.group(0)
  114. error_data = json.loads(json_str)
  115. if isinstance(error_data, dict):
  116. response = error_data.get('response')
  117. if isinstance(response, dict):
  118. data = response.get('data')
  119. if isinstance(data, dict):
  120. statuses = data.get('statuses')
  121. if isinstance(statuses, list) and statuses:
  122. first_status = statuses[0]
  123. if isinstance(first_status, dict) and 'error' in first_status:
  124. return str(first_status['error']) # Return the specific error
  125. except (json.JSONDecodeError, AttributeError, TypeError, IndexError) as parse_error:
  126. logger.debug(f"Could not parse detailed Hyperliquid error from string '{error_str}': {parse_error}")
  127. # Fallback: Check for common CCXT error types if the above fails or if it's a CCXT error
  128. # (ccxt.base.errors.InsufficientFunds, ccxt.base.errors.ExchangeError etc.)
  129. # These often have a message attribute or a more direct string representation.
  130. if hasattr(exception_obj, 'message') and isinstance(exception_obj.message, str) and exception_obj.message:
  131. return exception_obj.message
  132. # Generic fallback to the first 150 chars of the exception string
  133. # Avoid returning the full "hyperliquid {..." string if parsing failed.
  134. prefix_to_remove = "hyperliquid "
  135. if error_str.startswith(prefix_to_remove):
  136. return error_str[len(prefix_to_remove):].split(',')[0][:150] # Get a cleaner part
  137. return error_str[:150]
  138. def get_balance(self) -> Optional[Dict[str, Any]]:
  139. """Get account balance."""
  140. try:
  141. if not self.sync_client:
  142. logger.error("❌ Client not initialized")
  143. return None
  144. # For Hyperliquid, we need to pass the wallet address/user parameter
  145. # The user parameter should be the wallet address derived from private key
  146. params = {}
  147. # If we have a wallet address, use it as the user parameter
  148. if Config.HYPERLIQUID_WALLET_ADDRESS:
  149. # Extract the wallet address
  150. # For CCXT Hyperliquid, the user parameter should be the wallet address
  151. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  152. if wallet_address.startswith('0x'):
  153. # Use the address as the user parameter
  154. params['user'] = wallet_address
  155. else:
  156. # Add 0x prefix if missing
  157. params['user'] = f"0x{wallet_address}"
  158. logger.debug(f"🔍 Fetching balance with params: {params}")
  159. balance = self.sync_client.fetch_balance(params=params)
  160. logger.info("✅ Successfully fetched balance")
  161. return balance
  162. except Exception as e:
  163. error_message = self._extract_error_message(e)
  164. logger.error(f"❌ Error fetching balance: {error_message} (Full exception: {e})")
  165. logger.debug(f"💡 Attempted with params: {params}")
  166. return None
  167. def get_balance_alternative(self) -> Optional[Dict[str, Any]]:
  168. """Alternative balance fetching method trying different approaches."""
  169. try:
  170. if not self.sync_client:
  171. logger.error("❌ Client not initialized")
  172. return None
  173. # Try different approaches for balance fetching
  174. approaches = [
  175. # Approach 1: No params (original)
  176. {},
  177. # Approach 2: Wallet address as user
  178. {'user': Config.HYPERLIQUID_WALLET_ADDRESS},
  179. # Approach 3: Wallet address with 0x prefix
  180. {'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},
  181. # Approach 4: Empty user
  182. {'user': ''},
  183. ]
  184. for i, params in enumerate(approaches, 1):
  185. try:
  186. logger.info(f"🔍 Trying approach {i}: {params}")
  187. balance = self.sync_client.fetch_balance(params=params)
  188. logger.info(f"✅ Approach {i} successful!")
  189. return balance
  190. except Exception as e:
  191. logger.warning(f"⚠️ Approach {i} failed: {e}")
  192. continue
  193. logger.error("❌ All approaches failed")
  194. return None
  195. except Exception as e:
  196. error_message = self._extract_error_message(e)
  197. logger.error(f"❌ Error in alternative balance fetch: {error_message} (Full exception: {e})")
  198. return None
  199. def get_positions(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
  200. """Get current positions."""
  201. try:
  202. if not self.sync_client:
  203. logger.error("❌ Client not initialized")
  204. return None
  205. # Add user parameter for Hyperliquid CCXT compatibility
  206. params = {}
  207. if Config.HYPERLIQUID_WALLET_ADDRESS:
  208. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  209. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  210. logger.debug(f"🔍 Fetching positions with params: {params}")
  211. positions = self.sync_client.fetch_positions([symbol] if symbol else None, params=params)
  212. logger.info(f"✅ Successfully fetched positions for {symbol or 'all symbols'}")
  213. return positions
  214. except Exception as e:
  215. error_message = self._extract_error_message(e)
  216. logger.error(f"❌ Error fetching positions: {error_message} (Full exception: {e})")
  217. logger.debug(f"💡 Attempted with params: {params}")
  218. return None
  219. def get_market_data(self, symbol: str) -> Optional[Dict[str, Any]]:
  220. """Get market data for a symbol, including OHLCV for high/low."""
  221. try:
  222. if not self.sync_client:
  223. logger.error("❌ Client not initialized")
  224. return None
  225. ticker = self.sync_client.fetch_ticker(symbol)
  226. orderbook = self.sync_client.fetch_order_book(symbol)
  227. # Fetch last 24h OHLCV data to get accurate high/low
  228. ohlcv = self.sync_client.fetch_ohlcv(symbol, '1d', limit=1)
  229. if ohlcv:
  230. # CCXT OHLCV format: [timestamp, open, high, low, close, volume]
  231. last_day_candle = ohlcv[0]
  232. ticker['high'] = last_day_candle[2]
  233. ticker['low'] = last_day_candle[3]
  234. market_data = {
  235. 'ticker': ticker,
  236. 'orderbook': orderbook,
  237. 'symbol': symbol
  238. }
  239. logger.info(f"✅ Successfully fetched market data for {symbol}")
  240. return market_data
  241. except Exception as e:
  242. error_message = self._extract_error_message(e)
  243. logger.error(f"❌ Error fetching market data for {symbol}: {error_message} (Full exception: {e})")
  244. return None
  245. def get_candle_data(self, symbol: str, timeframe: str = '1h', limit: int = 100, since: Optional[int] = None) -> Optional[List[List]]:
  246. """
  247. Get OHLCV candle data for a symbol.
  248. Args:
  249. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  250. timeframe: Timeframe for candles ('1m', '5m', '15m', '1h', '4h', '1d', etc.)
  251. limit: Maximum number of candles to return (default: 100)
  252. since: Timestamp in milliseconds to start from (optional)
  253. Returns:
  254. List of OHLCV candles in format: [[timestamp, open, high, low, close, volume], ...]
  255. Returns None if error occurs
  256. """
  257. try:
  258. if not self.sync_client:
  259. logger.error("❌ Client not initialized")
  260. return None
  261. logger.debug(f"🕯️ Fetching {limit} candles for {symbol} ({timeframe})")
  262. # Fetch OHLCV data
  263. params = {}
  264. if since is not None:
  265. candles = self.sync_client.fetch_ohlcv(symbol, timeframe, since=since, limit=limit, params=params)
  266. else:
  267. candles = self.sync_client.fetch_ohlcv(symbol, timeframe, limit=limit, params=params)
  268. if candles:
  269. logger.info(f"✅ Successfully fetched {len(candles)} candles for {symbol} ({timeframe})")
  270. logger.debug(f"📊 Candle data range: {candles[0][0]} to {candles[-1][0]} (timestamps)")
  271. else:
  272. logger.warning(f"⚠️ No candle data returned for {symbol} ({timeframe})")
  273. return candles
  274. except Exception as e:
  275. error_message = self._extract_error_message(e)
  276. logger.error(f"❌ Error fetching candle data for {symbol} ({timeframe}): {error_message} (Full exception: {e})")
  277. return None
  278. def get_candle_data_formatted(self, symbol: str, timeframe: str = '1h', limit: int = 100, since: Optional[int] = None) -> Optional[List[Dict[str, Any]]]:
  279. """
  280. Get OHLCV candle data for a symbol in a formatted dictionary structure.
  281. Args:
  282. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  283. timeframe: Timeframe for candles ('1m', '5m', '15m', '1h', '4h', '1d', etc.)
  284. limit: Maximum number of candles to return (default: 100)
  285. since: Timestamp in milliseconds to start from (optional)
  286. Returns:
  287. List of candle dictionaries with keys: timestamp, open, high, low, close, volume
  288. Returns None if error occurs
  289. """
  290. try:
  291. # Get raw candle data
  292. raw_candles = self.get_candle_data(symbol, timeframe, limit, since)
  293. if not raw_candles:
  294. return None
  295. # Format candles into dictionaries
  296. formatted_candles = []
  297. for candle in raw_candles:
  298. if len(candle) >= 6: # Ensure we have all OHLCV data
  299. formatted_candle = {
  300. 'timestamp': candle[0],
  301. 'open': candle[1],
  302. 'high': candle[2],
  303. 'low': candle[3],
  304. 'close': candle[4],
  305. 'volume': candle[5]
  306. }
  307. formatted_candles.append(formatted_candle)
  308. logger.info(f"✅ Successfully formatted {len(formatted_candles)} candles for {symbol} ({timeframe})")
  309. return formatted_candles
  310. except Exception as e:
  311. error_message = self._extract_error_message(e)
  312. logger.error(f"❌ Error formatting candle data for {symbol} ({timeframe}): {error_message} (Full exception: {e})")
  313. return None
  314. def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  315. """Place a limit order."""
  316. try:
  317. if not self.sync_client:
  318. logger.error("❌ Client not initialized")
  319. return None, "Client not initialized"
  320. order_params = params or {}
  321. # Add margin mode from config
  322. if Config.HYPERLIQUID_MARGIN_MODE:
  323. order_params['marginMode'] = Config.HYPERLIQUID_MARGIN_MODE.lower()
  324. logger.info(f"Placing limit order: {side} {amount} {symbol} @ {price} with params {order_params}")
  325. order = self.sync_client.create_limit_order(symbol, side, amount, price, params=order_params)
  326. logger.info(f"✅ Limit order placed successfully: {order}")
  327. return order, None
  328. except Exception as e:
  329. error_message = self._extract_error_message(e)
  330. logger.error(f"❌ Error placing limit order: {error_message}")
  331. return None, error_message
  332. def place_market_order(self, symbol: str, side: str, amount: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  333. """Place a market order."""
  334. try:
  335. if not self.sync_client:
  336. logger.error("❌ Client not initialized")
  337. return None, "Client not initialized"
  338. # Get current market price for slippage calculation
  339. ticker = self.sync_client.fetch_ticker(symbol)
  340. if not ticker or not ticker.get('last'):
  341. error_msg = f"Could not fetch current price for {symbol} to calculate slippage."
  342. logger.error(f"❌ {error_msg}")
  343. return None, error_msg
  344. current_price = ticker['last']
  345. slippage_percent = 0.5 # 0.5% slippage
  346. slippage_price = current_price * (1 + slippage_percent / 100) if side == 'buy' else current_price * (1 - slippage_percent / 100)
  347. order_params = params or {}
  348. # Add margin mode from config
  349. if Config.HYPERLIQUID_MARGIN_MODE:
  350. order_params['marginMode'] = Config.HYPERLIQUID_MARGIN_MODE.lower()
  351. # Hyperliquid requires a price for market orders for slippage protection.
  352. # This must be passed as the 'price' argument, not within 'params'.
  353. logger.info(f"Placing market order: {side} {amount} {symbol} with slippage price {slippage_price} and params {order_params}")
  354. # Use create_market_order for market orders, passing the slippage price explicitly.
  355. order = self.sync_client.create_market_order(symbol, side, amount, price=slippage_price, params=order_params)
  356. logger.info(f"✅ Market order placed successfully: {order}")
  357. return order, None
  358. except Exception as e:
  359. error_message = self._extract_error_message(e)
  360. logger.error(f"❌ Error placing market order: {error_message}")
  361. return None, error_message
  362. def get_open_orders(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
  363. """Get all open orders for a symbol or all symbols."""
  364. try:
  365. if not self.sync_client:
  366. logger.error("❌ Client not initialized")
  367. return None
  368. # Add user parameter for Hyperliquid CCXT compatibility
  369. params = {}
  370. if Config.HYPERLIQUID_WALLET_ADDRESS:
  371. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  372. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  373. logger.debug(f"🔍 Fetching open orders with params: {params}")
  374. orders = self.sync_client.fetch_open_orders(symbol, params=params)
  375. logger.info(f"✅ Successfully fetched open orders for {symbol or 'all symbols'}")
  376. return orders
  377. except Exception as e:
  378. error_message = self._extract_error_message(e)
  379. logger.error(f"❌ Error fetching open orders: {error_message} (Full exception: {e})")
  380. logger.debug(f"💡 Attempted with params: {params}")
  381. return None
  382. def cancel_order(self, order_id: str, symbol: str, params: Optional[Dict] = None) -> bool:
  383. """Cancel an order with CCXT-style parameters."""
  384. try:
  385. if not self.sync_client:
  386. logger.error("❌ Client not initialized")
  387. return False
  388. cancel_params = params or {}
  389. result = self.sync_client.cancel_order(order_id, symbol, params=cancel_params)
  390. logger.info(f"✅ Successfully cancelled order {order_id}")
  391. return True
  392. except Exception as e:
  393. error_message = self._extract_error_message(e)
  394. logger.error(f"❌ Error cancelling order {order_id}: {error_message} (Full exception: {e})")
  395. return False
  396. def get_recent_trades(self, symbol: str, limit: int = 10) -> Optional[List[Dict[str, Any]]]:
  397. """Get recent trades for a symbol."""
  398. try:
  399. if not self.sync_client:
  400. logger.error("❌ Client not initialized")
  401. return None
  402. trades = self.sync_client.fetch_trades(symbol, limit=limit)
  403. logger.info(f"✅ Successfully fetched {len(trades)} recent trades for {symbol}")
  404. return trades
  405. except Exception as e:
  406. error_message = self._extract_error_message(e)
  407. logger.error(f"❌ Error fetching recent trades for {symbol}: {error_message} (Full exception: {e})")
  408. return None
  409. def get_trading_fee(self, symbol: str) -> Optional[Dict[str, Any]]:
  410. """Get trading fee for a symbol."""
  411. try:
  412. if not self.sync_client:
  413. logger.error("❌ Client not initialized")
  414. return None
  415. fee = self.sync_client.fetch_trading_fee(symbol)
  416. logger.info(f"✅ Successfully fetched trading fee for {symbol}")
  417. return fee
  418. except Exception as e:
  419. error_message = self._extract_error_message(e)
  420. logger.error(f"❌ Error fetching trading fee for {symbol}: {error_message} (Full exception: {e})")
  421. return None
  422. async def get_markets(self) -> Optional[Dict[str, Any]]:
  423. """Get available markets/symbols."""
  424. try:
  425. if not self.sync_client:
  426. logger.error("❌ Client not initialized")
  427. return None
  428. markets = self.sync_client.load_markets()
  429. logger.info(f"✅ Successfully loaded {len(markets)} markets")
  430. return markets
  431. except Exception as e:
  432. error_message = self._extract_error_message(e)
  433. logger.error(f"❌ Error loading markets: {error_message} (Full exception: {e})")
  434. return None
  435. def place_stop_loss_order(self, symbol: str, side: str, amount: float, stop_price_arg: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  436. """
  437. Place a stop loss order (as a stop-market order).
  438. Returns a tuple: (order_object, error_message_string).
  439. Error_message_string is None on success. Order_object is None on failure.
  440. Args:
  441. symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
  442. side: 'buy' or 'sell' (side of the order to be placed when stop is triggered)
  443. amount: Order amount
  444. stop_price_arg: The price at which the stop loss triggers
  445. params: Additional parameters (mostly unused now, but kept for signature compatibility)
  446. """
  447. try:
  448. if not self.sync_client:
  449. logger.error("❌ Client not initialized")
  450. return None, "Client not initialized"
  451. # Construct parameters for a trigger order (stop-market)
  452. # The main order type is 'market', triggered at stop_price_arg.
  453. # The 'price' for the create_order call will be None.
  454. trigger_params = {
  455. 'trigger': {
  456. 'triggerPx': str(stop_price_arg), # The stop price
  457. 'isMarket': True, # Execute as a market order when triggered
  458. 'tpsl': 'sl' # Indicate it's a stop loss
  459. }
  460. }
  461. # Merge with any incoming params, though trigger_params should take precedence for SL logic
  462. if params:
  463. trigger_params.update(params)
  464. logger.info(f"🛑 Placing STOP-MARKET order: {side} {amount} {symbol} with trigger @ ${stop_price_arg:.4f}")
  465. # Pass stop_price_arg as the price parameter for slippage calculation, as required by the exchange for market orders.
  466. order = self.sync_client.create_order(symbol, 'market', side, amount, stop_price_arg, trigger_params)
  467. logger.info(f"✅ Stop-market order placed successfully: {order}")
  468. return order, None
  469. except Exception as e:
  470. error_message = self._extract_error_message(e)
  471. logger.error(f"❌ Error placing stop-market order: {error_message} (Full exception: {e})")
  472. return None, error_message
  473. def place_take_profit_order(self, symbol: str, side: str, amount: float, take_profit_price_arg: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  474. """
  475. Place a take profit order (as a limit order, or can be adapted to trigger like SL).
  476. Currently places a limit order. For true TP trigger, needs similar logic to SL.
  477. Returns a tuple: (order_object, error_message_string).
  478. Args:
  479. symbol: Trading symbol
  480. side: 'buy' or 'sell'
  481. amount: Order amount
  482. take_profit_price_arg: The price for the take profit order
  483. params: Additional parameters
  484. """
  485. # TODO: Implement actual take profit trigger logic (similar to stop_loss_order if needed)
  486. # For now, it remains a limit order as per previous implementation,
  487. # but could be changed to a trigger order: isMarket=False, tpsl='tp'.
  488. logger.warning("⚠️ place_take_profit_order currently places a LIMIT order. For triggered TP, it needs updating similar to SL.")
  489. try:
  490. if not self.sync_client:
  491. logger.error("❌ Client not initialized")
  492. return None, "Client not initialized"
  493. order_params = params or {}
  494. logger.info(f"🎯 Placing take profit (LIMIT) order: {side} {amount} {symbol} @ ${take_profit_price_arg}")
  495. order = self.sync_client.create_limit_order(symbol, side, amount, take_profit_price_arg, params=order_params)
  496. logger.info(f"✅ Successfully placed take profit (LIMIT) order for {amount} {symbol} at ${take_profit_price_arg}")
  497. logger.debug(f"📄 Take profit order details: {order}")
  498. return order, None
  499. except Exception as e:
  500. error_message = self._extract_error_message(e)
  501. logger.error(f"❌ Error placing take profit (LIMIT) order: {error_message} (Full exception: {e})")
  502. return None, error_message
  503. def get_recent_fills(self, limit: int = 100) -> Optional[List[Dict[str, Any]]]:
  504. """
  505. Get recent fills/trades for the account.
  506. Args:
  507. limit: Maximum number of fills to return
  508. Returns:
  509. List of recent fills/trades or None if error
  510. """
  511. try:
  512. if not self.sync_client:
  513. logger.error("❌ Client not initialized")
  514. return None
  515. # Add user parameter for Hyperliquid CCXT compatibility
  516. params = {}
  517. if Config.HYPERLIQUID_WALLET_ADDRESS:
  518. wallet_address = Config.HYPERLIQUID_WALLET_ADDRESS
  519. params['user'] = f"0x{wallet_address}" if not wallet_address.startswith('0x') else wallet_address
  520. # Fetch recent trades/fills for the account
  521. # Use fetch_my_trades to get account-specific trades
  522. logger.debug(f"🔍 Fetching recent fills with params: {params}")
  523. # Get recent fills across all symbols
  524. # We'll fetch trades for all symbols and merge them
  525. try:
  526. # Option 1: Try fetch_my_trades if available
  527. fills = self.sync_client.fetch_my_trades(None, limit=limit, params=params)
  528. logger.info(f"✅ Successfully fetched {len(fills)} recent fills")
  529. return fills
  530. except AttributeError:
  531. # Option 2: If fetch_my_trades not available, try alternative approach
  532. logger.debug("fetch_my_trades not available, trying alternative approach")
  533. # Get positions to determine active symbols
  534. positions = self.get_positions()
  535. if not positions:
  536. logger.info("No positions found, no recent fills to fetch")
  537. return []
  538. # Get symbols from positions
  539. symbols = list(set([pos.get('symbol') for pos in positions if pos.get('symbol')]))
  540. all_fills = []
  541. for symbol in symbols[:5]: # Limit to 5 symbols to avoid too many requests
  542. try:
  543. symbol_trades = self.sync_client.fetch_my_trades(symbol, limit=limit//len(symbols), params=params)
  544. if symbol_trades:
  545. all_fills.extend(symbol_trades)
  546. except Exception as e:
  547. logger.warning(f"Could not fetch trades for {symbol}: {e}")
  548. continue
  549. # Sort by timestamp (newest first)
  550. all_fills.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
  551. # Return only the requested limit
  552. result = all_fills[:limit]
  553. logger.info(f"✅ Successfully fetched {len(result)} recent fills from {len(symbols)} symbols")
  554. return result
  555. except Exception as e:
  556. error_message = self._extract_error_message(e)
  557. logger.error(f"❌ Error fetching recent fills: {error_message} (Full exception: {e})")
  558. logger.debug(f"💡 Attempted with params: {params}")
  559. return None
  560. def set_leverage(self, leverage: int, symbol: str, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  561. """Set leverage for a symbol."""
  562. try:
  563. if not self.sync_client:
  564. logger.error("❌ Client not initialized")
  565. return None, "Client not initialized"
  566. # CCXT's setLeverage usually requires the symbol
  567. response = self.sync_client.setLeverage(leverage, symbol, params=params)
  568. logger.info(f"✅ Successfully set leverage to {leverage}x for {symbol}.")
  569. return response, None
  570. except Exception as e:
  571. error_message = self._extract_error_message(e)
  572. logger.error(f"❌ Error setting leverage for {symbol}: {error_message}")
  573. return None, error_message
  574. def fetch_leverage(self, symbol: str, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
  575. """Fetch leverage for a symbol."""
  576. try:
  577. if not self.sync_client:
  578. logger.error("❌ Client not initialized")
  579. return None, "Client not initialized"
  580. # CCXT's fetchLeverage usually requires the symbol
  581. leverage_data = self.sync_client.fetchLeverage(symbol, params=params if params else {})
  582. logger.info(f"✅ Successfully fetched leverage for {symbol}.")
  583. return leverage_data, None
  584. except Exception as e:
  585. error_message = self._extract_error_message(e)
  586. logger.error(f"❌ Error fetching leverage for {symbol}: {error_message}")
  587. return None, error_message