|
@@ -1,7 +1,9 @@
|
|
|
import logging
|
|
|
-from typing import Optional, Dict, Any, List
|
|
|
+from typing import Optional, Dict, Any, List, Tuple
|
|
|
from hyperliquid import HyperliquidSync
|
|
|
from src.config.config import Config
|
|
|
+import re
|
|
|
+import json
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
@@ -122,6 +124,44 @@ class HyperliquidClient:
|
|
|
logger.warning(f"⚠️ Connection test failed: {e}")
|
|
|
logger.warning("💡 This might be normal if you have no positions/balance")
|
|
|
|
|
|
+ def _extract_error_message(self, exception_obj: Exception) -> str:
|
|
|
+ """Extracts a more specific error message from a Hyperliquid exception."""
|
|
|
+ error_str = str(exception_obj)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ try:
|
|
|
+
|
|
|
+ json_match = re.search(r'{\s*"status":.*}', error_str)
|
|
|
+ if json_match:
|
|
|
+ json_str = json_match.group(0)
|
|
|
+ error_data = json.loads(json_str)
|
|
|
+ if isinstance(error_data, dict):
|
|
|
+ response = error_data.get('response')
|
|
|
+ if isinstance(response, dict):
|
|
|
+ data = response.get('data')
|
|
|
+ if isinstance(data, dict):
|
|
|
+ statuses = data.get('statuses')
|
|
|
+ if isinstance(statuses, list) and statuses:
|
|
|
+ first_status = statuses[0]
|
|
|
+ if isinstance(first_status, dict) and 'error' in first_status:
|
|
|
+ return str(first_status['error'])
|
|
|
+ except (json.JSONDecodeError, AttributeError, TypeError, IndexError) as parse_error:
|
|
|
+ logger.debug(f"Could not parse detailed Hyperliquid error from string '{error_str}': {parse_error}")
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ if hasattr(exception_obj, 'message') and isinstance(exception_obj.message, str) and exception_obj.message:
|
|
|
+ return exception_obj.message
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ prefix_to_remove = "hyperliquid "
|
|
|
+ if error_str.startswith(prefix_to_remove):
|
|
|
+ return error_str[len(prefix_to_remove):].split(',')[0][:150]
|
|
|
+ return error_str[:150]
|
|
|
+
|
|
|
def get_balance(self) -> Optional[Dict[str, Any]]:
|
|
|
"""Get account balance."""
|
|
|
try:
|
|
@@ -150,7 +190,8 @@ class HyperliquidClient:
|
|
|
logger.info("✅ Successfully fetched balance")
|
|
|
return balance
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error fetching balance: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching balance: {error_message} (Full exception: {e})")
|
|
|
logger.debug(f"💡 Attempted with params: {params}")
|
|
|
return None
|
|
|
|
|
@@ -187,7 +228,8 @@ class HyperliquidClient:
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error in alternative balance fetch: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error in alternative balance fetch: {error_message} (Full exception: {e})")
|
|
|
return None
|
|
|
|
|
|
def get_positions(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
|
@@ -208,7 +250,8 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully fetched positions for {symbol or 'all symbols'}")
|
|
|
return positions
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error fetching positions: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching positions: {error_message} (Full exception: {e})")
|
|
|
logger.debug(f"💡 Attempted with params: {params}")
|
|
|
return None
|
|
|
|
|
@@ -231,12 +274,15 @@ class HyperliquidClient:
|
|
|
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}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching market data for {symbol}: {error_message} (Full exception: {e})")
|
|
|
return None
|
|
|
|
|
|
- def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
|
|
|
+ def place_limit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
|
"""
|
|
|
Place a limit order with CCXT-style parameters.
|
|
|
+ Returns a tuple: (order_object, error_message_string).
|
|
|
+ Error_message_string is None on success. Order_object is None on failure.
|
|
|
|
|
|
Args:
|
|
|
symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
|
|
@@ -248,7 +294,7 @@ class HyperliquidClient:
|
|
|
try:
|
|
|
if not self.sync_client:
|
|
|
logger.error("❌ Client not initialized")
|
|
|
- return None
|
|
|
+ return None, "Client not initialized"
|
|
|
|
|
|
|
|
|
order_params = params or {}
|
|
@@ -257,15 +303,18 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully placed {side} limit order for {amount} {symbol} at ${price}")
|
|
|
logger.debug(f"📄 Order details: {order}")
|
|
|
|
|
|
- return order
|
|
|
+ return order, None
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error placing limit order: {e}")
|
|
|
- return None
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error placing limit order: {error_message} (Full exception: {e})")
|
|
|
+ return None, error_message
|
|
|
|
|
|
- def place_market_order(self, symbol: str, side: str, amount: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
|
|
|
+ def place_market_order(self, symbol: str, side: str, amount: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
|
"""
|
|
|
Place a market order with CCXT-style parameters.
|
|
|
-
|
|
|
+ Returns a tuple: (order_object, error_message_string).
|
|
|
+ Error_message_string is None on success. Order_object is None on failure.
|
|
|
+
|
|
|
Args:
|
|
|
symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
|
|
|
side: 'buy' or 'sell'
|
|
@@ -275,18 +324,18 @@ class HyperliquidClient:
|
|
|
try:
|
|
|
if not self.sync_client:
|
|
|
logger.error("❌ Client not initialized")
|
|
|
- return None
|
|
|
+ return None, "Client not initialized"
|
|
|
|
|
|
|
|
|
ticker = self.sync_client.fetch_ticker(symbol)
|
|
|
if not ticker:
|
|
|
logger.error(f"❌ Could not fetch ticker for {symbol}")
|
|
|
- return None
|
|
|
+ return None, f"Could not fetch ticker for {symbol}"
|
|
|
|
|
|
current_price = ticker.get('last')
|
|
|
if not current_price:
|
|
|
logger.error(f"❌ Could not get current price for {symbol}")
|
|
|
- return None
|
|
|
+ return None, f"Could not get current price for {symbol}"
|
|
|
|
|
|
|
|
|
slippage_percent = 0.5
|
|
@@ -308,10 +357,11 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully placed {side} market order for {amount} {symbol}")
|
|
|
logger.debug(f"📄 Order details: {order}")
|
|
|
|
|
|
- return order
|
|
|
+ return order, None
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error placing market order: {e}")
|
|
|
- return None
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error placing market order: {error_message} (Full exception: {e})")
|
|
|
+ return None, error_message
|
|
|
|
|
|
def get_open_orders(self, symbol: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
|
|
"""Get open orders."""
|
|
@@ -331,7 +381,8 @@ class HyperliquidClient:
|
|
|
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}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching open orders: {error_message} (Full exception: {e})")
|
|
|
logger.debug(f"💡 Attempted with params: {params}")
|
|
|
return None
|
|
|
|
|
@@ -348,7 +399,8 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully cancelled order {order_id}")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error cancelling order {order_id}: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error cancelling order {order_id}: {error_message} (Full exception: {e})")
|
|
|
return False
|
|
|
|
|
|
def get_recent_trades(self, symbol: str, limit: int = 10) -> Optional[List[Dict[str, Any]]]:
|
|
@@ -362,7 +414,8 @@ class HyperliquidClient:
|
|
|
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}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching recent trades for {symbol}: {error_message} (Full exception: {e})")
|
|
|
return None
|
|
|
|
|
|
def get_trading_fee(self, symbol: str) -> Optional[Dict[str, Any]]:
|
|
@@ -376,7 +429,8 @@ class HyperliquidClient:
|
|
|
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}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching trading fee for {symbol}: {error_message} (Full exception: {e})")
|
|
|
return None
|
|
|
|
|
|
def get_markets(self) -> Optional[Dict[str, Any]]:
|
|
@@ -390,13 +444,16 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully loaded {len(markets)} markets")
|
|
|
return markets
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error loading markets: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error loading markets: {error_message} (Full exception: {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]]:
|
|
|
+ def place_stop_loss_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
|
"""
|
|
|
Place a stop loss order (implemented as a limit order).
|
|
|
-
|
|
|
+ Returns a tuple: (order_object, error_message_string).
|
|
|
+ Error_message_string is None on success. Order_object is None on failure.
|
|
|
+
|
|
|
Args:
|
|
|
symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
|
|
|
side: 'buy' or 'sell'
|
|
@@ -407,7 +464,7 @@ class HyperliquidClient:
|
|
|
try:
|
|
|
if not self.sync_client:
|
|
|
logger.error("❌ Client not initialized")
|
|
|
- return None
|
|
|
+ return None, "Client not initialized"
|
|
|
|
|
|
|
|
|
|
|
@@ -421,15 +478,18 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully placed stop loss order for {amount} {symbol} at ${price}")
|
|
|
logger.debug(f"📄 Stop loss order details: {order}")
|
|
|
|
|
|
- return order
|
|
|
+ return order, None
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error placing stop loss order: {e}")
|
|
|
- return None
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error placing stop loss order: {error_message} (Full exception: {e})")
|
|
|
+ return None, error_message
|
|
|
|
|
|
- def place_take_profit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
|
|
|
+ def place_take_profit_order(self, symbol: str, side: str, amount: float, price: float, params: Optional[Dict] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
|
"""
|
|
|
Place a take profit order (implemented as a limit order).
|
|
|
-
|
|
|
+ Returns a tuple: (order_object, error_message_string).
|
|
|
+ Error_message_string is None on success. Order_object is None on failure.
|
|
|
+
|
|
|
Args:
|
|
|
symbol: Trading symbol (e.g., 'BTC/USDC:USDC')
|
|
|
side: 'buy' or 'sell'
|
|
@@ -440,7 +500,7 @@ class HyperliquidClient:
|
|
|
try:
|
|
|
if not self.sync_client:
|
|
|
logger.error("❌ Client not initialized")
|
|
|
- return None
|
|
|
+ return None, "Client not initialized"
|
|
|
|
|
|
|
|
|
|
|
@@ -454,10 +514,11 @@ class HyperliquidClient:
|
|
|
logger.info(f"✅ Successfully placed take profit order for {amount} {symbol} at ${price}")
|
|
|
logger.debug(f"📄 Take profit order details: {order}")
|
|
|
|
|
|
- return order
|
|
|
+ return order, None
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error placing take profit order: {e}")
|
|
|
- return None
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error placing take profit order: {error_message} (Full exception: {e})")
|
|
|
+ return None, error_message
|
|
|
|
|
|
def get_recent_fills(self, limit: int = 100) -> Optional[List[Dict[str, Any]]]:
|
|
|
"""
|
|
@@ -523,6 +584,7 @@ class HyperliquidClient:
|
|
|
return result
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"❌ Error fetching recent fills: {e}")
|
|
|
+ error_message = self._extract_error_message(e)
|
|
|
+ logger.error(f"❌ Error fetching recent fills: {error_message} (Full exception: {e})")
|
|
|
logger.debug(f"💡 Attempted with params: {params}")
|
|
|
return None
|