stock-ai-agent/backend/app/services/hyperliquid_trading_service.py
2026-04-22 10:38:25 +08:00

699 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"杠杆不能超过 10xClawFi 规则),当前: {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.001BTC=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