""" Hyperliquid 交易服务 - ClawFi 集成 """ import os from typing import Dict, Any, Optional, List from datetime import datetime from app.config import get_settings from app.utils.logger import logger try: from hyperliquid.info import Info from hyperliquid.exchange import Exchange from eth_account import Account HYPERLIQUID_AVAILABLE = True except ImportError: HYPERLIQUID_AVAILABLE = False logger.warning("Hyperliquid SDK 未安装,请运行: npx clawfi-hyperliquid-skill") class HyperliquidTradingService: """Hyperliquid 交易服务(ClawFi 集成)""" def __init__(self): """初始化 Hyperliquid 交易服务""" if not HYPERLIQUID_AVAILABLE: raise ImportError("Hyperliquid SDK 未安装") self.settings = get_settings() # 从 settings 加载认证信息 self.wallet_address = self.settings.clawfi_wallet_address self.private_key = self.settings.clawfi_private_key if not self.wallet_address or not self.private_key: raise ValueError( "缺少 Hyperliquid 认证信息。请在 .env 中设置: " "CLAWFI_WALLET_ADDRESS 和 CLAWFI_PRIVATE_KEY" ) # 风控配置 self.max_total_leverage = self.settings.hyperliquid_max_total_leverage self.circuit_breaker_drawdown = self.settings.hyperliquid_circuit_breaker_drawdown self.max_single_position = self.settings.hyperliquid_max_single_position # 初始化 SDK self.info = Info(base_url="https://api.hyperliquid.xyz") account = Account.from_key(self.private_key) self.exchange = Exchange(account, base_url="https://api.hyperliquid.xyz", account_address=self.wallet_address) # 初始账户价值(用于熔断检查) self.initial_balance: Optional[float] = None self._initialize_account() logger.info(f"Hyperliquid 交易服务初始化完成") logger.info(f" 钱包地址: {self.wallet_address}") logger.info(f" 总杠杆上限: {self.max_total_leverage}x") logger.info(f" 熔断阈值: {self.circuit_breaker_drawdown * 100}%") def _initialize_account(self): """初始化账户信息""" try: state = self.get_account_state() self.initial_balance = state["account_value"] logger.info(f" 初始账户价值: ${self.initial_balance:,.2f}") except Exception as e: logger.error(f"初始化账户失败: {e}") raise def get_account_state(self) -> Dict[str, Any]: """获取账户状态""" try: state = self.info.user_state(self.wallet_address) margin_summary = state.get("marginSummary", {}) account_value = float(margin_summary.get("accountValue", 0)) total_margin_used = float(margin_summary.get("totalMarginUsed", 0)) return { "account_value": account_value, "current_balance": account_value, "total_margin_used": total_margin_used, "available_balance": account_value - total_margin_used, "positions": state.get("assetPositions", []), "margin_summary": margin_summary } except Exception as e: logger.error(f"获取账户状态失败: {e}") raise def check_risk_limits(self) -> Dict[str, Any]: """ 检查风险限制(ClawFi 强制规则) Returns: 风险检查结果 """ state = self.get_account_state() current_value = state["account_value"] # 计算回撤 if self.initial_balance is None: self.initial_balance = current_value drawdown = (self.initial_balance - current_value) / self.initial_balance if self.initial_balance > 0 else 0 # 10% 熔断检查 circuit_breaker_triggered = drawdown >= self.circuit_breaker_drawdown if circuit_breaker_triggered: logger.error(f"🚨 触发 10% 熔断!当前回撤: {drawdown * 100:.2f}%") # 平掉所有持仓 self.market_close_all() raise Exception(f"触发 10% 熔断 - 所有持仓已平仓(回撤: {drawdown * 100:.2f}%)") return { "initial_balance": self.initial_balance, "current_value": current_value, "drawdown": drawdown, "drawdown_percent": drawdown * 100, "circuit_breaker_triggered": circuit_breaker_triggered, "safe_to_trade": not circuit_breaker_triggered } def update_leverage(self, symbol: str, leverage: int): """ 更新杠杆(必须在开仓前调用) Args: symbol: 交易对(如 "BTC") leverage: 杠杆倍数(≤10) """ if leverage > 10: raise ValueError(f"杠杆不能超过 10x(ClawFi 规则),当前: {leverage}x") try: result = self.exchange.update_leverage(leverage, symbol, is_cross=False) logger.info(f"更新杠杆: {symbol} → {leverage}x") return result except Exception as e: logger.error(f"更新杠杆失败: {e}") raise def place_market_order( self, symbol: str, is_buy: bool, size: float, reduce_only: bool = False ) -> Dict[str, Any]: """ 下市价单 Args: symbol: 交易对(如 "BTC") is_buy: True=做多,False=做空 size: 数量 reduce_only: 是否仅平仓 """ # 风险检查 self.check_risk_limits() # 精度保护:确保 size 符合 szDecimals 要求 size = self._sanitize_size(symbol, size) try: if reduce_only: # 平仓使用 market_close(不需要指定 is_buy,自动判断) result = self.exchange.market_close(symbol, sz=size) else: # 开仓使用 market_open result = self.exchange.market_open(symbol, is_buy, size) # 检查 API 响应状态 if result.get("status") != "ok": error_msg = result.get("response", "Unknown error") logger.error(f"❌ Hyperliquid 市价单失败: {error_msg}") return {"success": False, "error": str(error_msg), "result": result} # 检查单个订单状态 statuses = result.get("response", {}).get("data", {}).get("statuses", []) error_statuses = [s for s in statuses if "error" in s] if error_statuses: logger.error(f"❌ Hyperliquid 市价单错误: {error_statuses}") return {"success": False, "error": str(error_statuses), "result": result} # statuses 为空 → 静默拒绝 if not statuses: logger.error(f"❌ Hyperliquid 市价单:返回 statuses 为空,订单未成功提交") return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} side = "买入" if is_buy else "卖出" order_type = "平仓" if reduce_only else "开仓" logger.info(f"✅ Hyperliquid 市价单: {order_type} {side} {symbol} {size}") return { "success": True, "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size, "reduce_only": reduce_only, "result": result } except Exception as e: logger.error(f"下单失败: {e}") return { "success": False, "error": str(e) } def place_limit_order( self, symbol: str, is_buy: bool, size: float, price: float, reduce_only: bool = False ) -> Dict[str, Any]: """下限价单""" self.check_risk_limits() # 精度保护:确保 size 和 price 符合要求 size = self._sanitize_size(symbol, size) price = round(float(price), 5) # Hyperliquid 价格最多 5 位小数 try: result = self.exchange.order(symbol, is_buy, size, price, {"limit": {"tif": "Gtc"}}, reduce_only=reduce_only) # 检查 API 响应状态 if result.get("status") != "ok": error_msg = result.get("response", "Unknown error") logger.error(f"❌ Hyperliquid 限价单失败: {error_msg}") return {"success": False, "error": str(error_msg), "result": result} # 检查单个订单状态 statuses = result.get("response", {}).get("data", {}).get("statuses", []) # 有错误 → 失败 error_statuses = [s for s in statuses if "error" in s] if error_statuses: logger.error(f"❌ Hyperliquid 限价单错误: {error_statuses}") return {"success": False, "error": str(error_statuses), "result": result} # statuses 为空 → Hyperliquid 静默拒绝,视为失败 if not statuses: logger.error(f"❌ Hyperliquid 限价单:返回 statuses 为空,订单未成功提交") return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} # 判断订单实际状态:resting(挂单中)还是 filled(立即成交) first_status = statuses[0] if "resting" in first_status: order_id = first_status["resting"].get("oid") order_status = "resting" side = "买入" if is_buy else "卖出" logger.info(f"✅ Hyperliquid 限价单已挂出: {side} {symbol} {size} @ ${price} (oid={order_id})") elif "filled" in first_status: order_status = "filled" filled_info = first_status["filled"] avg_px = filled_info.get("avgPx", price) logger.info(f"✅ Hyperliquid 限价单立即成交: {symbol} {size} @ ${avg_px}") order_id = filled_info.get("oid") else: # 未知状态,记录并视为成功但标记 unknown order_status = "unknown" order_id = None logger.warning(f"⚠️ Hyperliquid 限价单状态未知: {first_status}") side = "买入" if is_buy else "卖出" return { "success": True, "order_status": order_status, # "resting" | "filled" | "unknown" "order_id": order_id, "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size, "price": price, "result": result } except Exception as e: logger.error(f"下单失败: {e}") return { "success": False, "error": str(e) } def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """ 获取所有挂单(包括止盈止损订单) Args: symbol: 可选,指定币种 Returns: 挂单列表 """ try: # 使用 open_orders API 获取挂单 orders_data = self.info.open_orders(self.wallet_address) orders = [] for order in orders_data or []: coin = order.get("coin") if symbol and coin != symbol: continue # side: "A" = ask (sell/做空), "B" = bid (buy/做多) side = order.get("side") is_buy = (side == "B") # Hyperliquid API 不直接返回 reduce_only 标记 # 但我们可以根据其他信息判断 # 暂时将所有订单都标记为非 reduce_only # Hyperliquid API 返回 reduceOnly(驼峰),不是 reduce_only is_reduce_only = order.get("reduceOnly", order.get("reduce_only", False)) orders.append({ "order_id": order.get("oid"), "symbol": coin, "side": "buy" if is_buy else "sell", "size": float(order.get("sz", 0)), "price": float(order.get("limitPx", 0)), "is_reduce_only": is_reduce_only, "order_type": order.get("orderType", {}), "timestamp": order.get("timestamp"), "original_size": float(order.get("origSz", 0)), "raw_side": side, "created_at": datetime.fromtimestamp(order.get("timestamp", 0) / 1000).isoformat() if order.get("timestamp") else None, }) return orders except Exception as e: logger.error(f"获取挂单失败: {e}") return [] def get_tp_sl_prices(self, symbol: str) -> Dict[str, Optional[float]]: """ 获取指定币种的止盈止损价格 Args: symbol: 币种(如 "BTC") Returns: {'take_profit': price, 'stop_loss': price} """ try: orders = self.get_open_orders(symbol) tp_price = None sl_price = None for order in orders: if not order.get("is_reduce_only"): continue order_type = order.get("order_type", {}) # 防御性检查:确保 order_type 是 dict if not isinstance(order_type, dict): continue # 止盈:限价单 if "limit" in order_type and order["price"] > 0: tp_price = order["price"] # 止损:触发单 if "trigger" in order_type: trigger_px = order_type.get("trigger", {}).get("triggerPx") if trigger_px: sl_price = float(trigger_px) return { "take_profit": tp_price, "stop_loss": sl_price } except Exception as e: logger.error(f"获取止盈止损价格失败: {e}") return {"take_profit": None, "stop_loss": None} def set_tp_sl( self, symbol: str, is_long: bool, size: float, tp_price: Optional[float] = None, sl_price: Optional[float] = None ) -> Dict[str, Any]: """ 设置止盈止损(开仓后调用) Args: symbol: 币种(如 "BTC") is_long: 是否多头 size: 数量 tp_price: 止盈价格(可选) sl_price: 止损价格(可选) Returns: {"success": bool, "tp_set": bool, "sl_set": bool, "errors": [...]} success=True 仅当所有请求的都设置成功 """ result = {"success": False, "tp_set": False, "sl_set": False, "errors": []} close_is_buy = not is_long # 平多头=卖出,平空头=买入 # 设置止盈(限价单)— 独立 try-except,失败不影响止损 if tp_price: try: tp_price = round(float(tp_price), 5) tp_result = self.exchange.order( symbol, close_is_buy, size, tp_price, {"limit": {"tif": "Gtc"}}, reduce_only=True ) # 验证响应 if tp_result.get("status") == "ok": statuses = tp_result.get("response", {}).get("data", {}).get("statuses", []) error_statuses = [s for s in statuses if "error" in s] if error_statuses: err_msg = error_statuses[0]["error"] logger.warning(f"设置止盈失败: {symbol} {err_msg}") result["errors"].append(f"止盈设置失败: {err_msg}") else: result["tp_set"] = True logger.info(f"✅ 设置止盈: {symbol} @ ${tp_price}") else: err_msg = tp_result.get("response", str(tp_result)) logger.warning(f"设置止盈失败: {symbol} {err_msg}") result["errors"].append(f"止盈设置失败: {err_msg}") except Exception as e: logger.warning(f"设置止盈失败: {symbol} {e}") result["errors"].append(f"止盈设置失败: {e}") # 设置止损(触发单)— 独立 try-except,失败不影响止盈 if sl_price: try: # 买单止损:exec_px 略高于 trigger(接受更高的买入价) # 卖单止损:exec_px 略低于 trigger(接受更低的卖出价) exec_px = sl_price * 1.001 if close_is_buy else sl_price * 0.999 sl_price = round(float(sl_price), 5) exec_px = round(float(exec_px), 5) sl_result = self.exchange.order( symbol, close_is_buy, size, exec_px, {"trigger": {"triggerPx": sl_price, "isMarket": True, "tpsl": "sl"}}, reduce_only=True ) # 验证响应 if sl_result.get("status") == "ok": statuses = sl_result.get("response", {}).get("data", {}).get("statuses", []) error_statuses = [s for s in statuses if "error" in s] if error_statuses: err_msg = error_statuses[0]["error"] logger.warning(f"设置止损失败: {symbol} {err_msg}") result["errors"].append(f"止损设置失败: {err_msg}") else: result["sl_set"] = True logger.info(f"✅ 设置止损: {symbol} @ ${sl_price}(触发)") else: err_msg = sl_result.get("response", str(sl_result)) logger.warning(f"设置止损失败: {symbol} {err_msg}") result["errors"].append(f"止损设置失败: {err_msg}") except Exception as e: logger.warning(f"设置止损失败: {symbol} {e}") result["errors"].append(f"止损设置失败: {e}") # 判断整体成功 requested_tp = tp_price is not None requested_sl = sl_price is not None all_ok = (not requested_tp or result["tp_set"]) and (not requested_sl or result["sl_set"]) result["success"] = all_ok if all_ok: logger.info(f"✅ 止盈止损设置完成: {symbol} TP={tp_price} SL={sl_price}") elif result["tp_set"] or result["sl_set"]: logger.warning(f"⚠️ 止盈止损部分成功: {symbol} tp_set={result['tp_set']} sl_set={result['sl_set']}") else: logger.error(f"❌ 止盈止损设置失败: {symbol} errors={result['errors']}") return result def cancel_tp_sl_orders(self, symbol: str) -> Dict[str, Any]: """ 取消指定币种的所有止盈止损订单 Args: symbol: 币种(如 "BTC") Returns: 取消结果 """ try: orders = self.get_open_orders(symbol) cancelled_count = 0 for order in orders: if order.get("is_reduce_only"): result = self.exchange.cancel(symbol, order["order_id"]) if result.get("status") == "ok": cancelled_count += 1 logger.info(f"✅ 取消 {symbol} 的止盈止损订单: {cancelled_count} 个") return { "success": True, "cancelled_count": cancelled_count } except Exception as e: logger.error(f"取消止盈止损订单失败: {e}") return { "success": False, "error": str(e) } def cancel_order(self, symbol: str, order_id: int) -> Dict[str, Any]: """取消订单""" try: if isinstance(order_id, str): order_id = int(order_id) result = self.exchange.cancel(symbol, order_id) logger.info(f"取消订单: {symbol} #{order_id}") return {"success": True, "result": result} except Exception as e: logger.error(f"取消订单失败: {e}") return {"success": False, "error": str(e)} def cancel_all_orders(self, symbol: Optional[str] = None) -> Dict[str, Any]: """取消所有订单""" try: # Hyperliquid SDK 没有 cancel_all_orders 方法,需要先查询再逐个取消 orders = self.get_open_orders(symbol) results = [] for order in orders: order_symbol = order.get('symbol') order_id = order.get('order_id') try: result = self.exchange.cancel(order_symbol, order_id) results.append(result) except Exception as e: logger.warning(f"取消订单失败: {order_symbol} #{order_id} - {e}") logger.info(f"取消所有订单: {symbol or '全部'} ({len(results)} 个)") return {"success": True, "result": results, "cancelled_count": len(results)} except Exception as e: logger.error(f"取消所有订单失败: {e}") return {"success": False, "error": str(e)} def market_close_all(self) -> Dict[str, Any]: """紧急平仓所有持仓(熔断时使用)""" try: state = self.get_account_state() positions = state["positions"] results = [] for pos in positions: position_data = pos.get("position", {}) coin = position_data.get("coin") if not coin: continue results.append(self.market_close_position(coin)) all_ok = all(result.get("success") for result in results) logger.info(f"🚨 紧急平仓完成,共平仓 {len(results)} 个持仓") return {"success": all_ok, "closed_positions": len(results), "results": results} except Exception as e: logger.error(f"紧急平仓失败: {e}") return {"success": False, "error": str(e)} def market_close_position(self, symbol: str) -> Dict[str, Any]: """按交易对市价平仓单个持仓""" position = self.get_position_for_symbol(symbol) if not position: return {"success": False, "symbol": symbol, "error": "未找到持仓"} coin = position.get("coin", symbol.replace('USDT', '').replace('/', '').upper()) size = abs(float(position.get("size", 0))) if size <= 0: return {"success": True, "symbol": coin, "size": 0} self.cancel_all_orders(coin) is_long = position.get("size", 0) > 0 result = self.place_market_order( symbol=coin, is_buy=not is_long, size=size, reduce_only=True ) if result.get("success"): result["symbol"] = coin return result def close_position(self, symbol: str, order_id: Optional[int] = None) -> Dict[str, Any]: """兼容旧调用:按交易对平仓,忽略 order_id""" return self.market_close_position(symbol) def get_open_positions(self) -> List[Dict[str, Any]]: """获取所有持仓""" try: state = self.get_account_state() positions = [] for pos in state["positions"]: position_data = pos.get("position", {}) coin = position_data.get("coin") size = float(position_data.get("szi", 0)) if size == 0: continue tp_sl_prices = self.get_tp_sl_prices(coin) positions.append({ "coin": coin, "symbol": f"{coin}USDT", "side": "buy" if size > 0 else "sell", "size": size, # 正数=多头,负数=空头 "entry_price": float(position_data.get("entryPx", 0)), "unrealized_pnl": float(position_data.get("unrealizedPnl", 0)), "leverage": position_data.get("leverage", {}).get("value") if isinstance(position_data.get("leverage"), dict) else position_data.get("leverage"), "liquidation_price": position_data.get("liquidationPx"), "stop_loss": tp_sl_prices.get("stop_loss"), "take_profit": tp_sl_prices.get("take_profit"), "opened_at": datetime.fromtimestamp(position_data.get("timestamp", 0) / 1000).isoformat() if position_data.get("timestamp") else None, "position": position_data # 保留原始数据 }) return positions except Exception as e: logger.error(f"获取持仓失败: {e}") return [] def get_position_for_symbol(self, symbol: str) -> Optional[Dict[str, Any]]: """获取指定币种的持仓""" normalized_symbol = symbol.replace('USDT', '').replace('/', '').upper() positions = self.get_open_positions() for pos in positions: if pos["coin"] == normalized_symbol: return pos return None def get_sz_decimals(self, symbol: str) -> int: """ 获取交易对的数量精度(szDecimals) Hyperliquid 要求订单数量必须符合各币种精度,否则报 'Order has invalid size' 例如 ETH=3(最小 0.001),BTC=5(最小 0.00001) """ try: asset = self.info.name_to_asset(symbol) return self.info.asset_to_sz_decimals.get(asset, 3) except Exception: logger.warning(f"获取 {symbol} szDecimals 失败,使用默认值 3") return 3 def _sanitize_size(self, symbol: str, size: float) -> float: """ 精度保护:确保 size 符合 Hyperliquid szDecimals 要求 这是防止 float_to_wire causes rounding 错误的最后防线。 """ import math try: sz_decimals = self.get_sz_decimals(symbol) factor = 10 ** sz_decimals sanitized = math.floor(float(size) * factor) / factor if sanitized != size: logger.info(f" 精度截断: {size} → {sanitized} ({symbol} szDecimals={sz_decimals})") return sanitized except Exception as e: logger.warning(f" 精度截断失败: {e},使用原值 {size}") return size # 单例 _hyperliquid_service_instance = None def get_hyperliquid_service() -> Optional[HyperliquidTradingService]: """获取 Hyperliquid 交易服务单例""" global _hyperliquid_service_instance settings = get_settings() # 如果未启用,返回 None if not settings.hyperliquid_trading_enabled: return None if _hyperliquid_service_instance is None: try: _hyperliquid_service_instance = HyperliquidTradingService() except Exception as e: logger.error(f"初始化 Hyperliquid 服务失败: {e}") return None return _hyperliquid_service_instance