699 lines
27 KiB
Python
699 lines
27 KiB
Python
"""
|
||
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
|