""" Hyperliquid 交易 API 提供 Hyperliquid 实盘交易数据接口 """ from fastapi import APIRouter, HTTPException, Query from typing import Optional from datetime import datetime from pydantic import BaseModel from app.services.hyperliquid_trading_service import get_hyperliquid_service from app.utils.logger import logger router = APIRouter(prefix="/api/hyperliquid", tags=["Hyperliquid"]) class AccountStatusResponse(BaseModel): """账户状态响应""" success: bool enabled: bool message: str data: Optional[dict] = None @router.get("/account") async def get_account_status(): """ 获取 Hyperliquid 账户状态 返回: - account_value: 账户总价值 - available_balance: 可用余额 - total_margin_used: 已用保证金 - total_position_value: 总持仓价值 - current_total_leverage: 当前总杠杆 - max_total_leverage: 最大总杠杆限制 - initial_balance: 初始余额(用于计算回撤) - drawdown_percent: 回撤百分比 """ try: service = get_hyperliquid_service() if service is None: return AccountStatusResponse( success=True, enabled=False, message="Hyperliquid 服务未启用。请在 .env 中设置 hyperliquid_trading_enabled=true", data=None ) # 获取账户状态 state = service.get_account_state() # 计算回撤 if service.initial_balance and service.initial_balance > 0: drawdown = (service.initial_balance - state["account_value"]) / service.initial_balance else: drawdown = 0 # 获取持仓 positions = service.get_open_positions() total_position_value = sum( abs(pos["size"]) * pos["entry_price"] for pos in positions ) # 计算当前总杠杆 if state["account_value"] > 0: current_total_leverage = total_position_value / state["account_value"] else: current_total_leverage = 0 return AccountStatusResponse( success=True, enabled=True, message="Hyperliquid 服务正常", data={ "account_value": state["account_value"], "available_balance": state["available_balance"], "total_margin_used": state["total_margin_used"], "total_position_value": total_position_value, "current_total_leverage": current_total_leverage, "max_total_leverage": service.max_total_leverage, "initial_balance": service.initial_balance, "drawdown_percent": drawdown * 100, "wallet_address": service.wallet_address, "leverage_limit": 10.0, # Hyperliquid 强制限制 "circuit_breaker_threshold": service.circuit_breaker_drawdown * 100 } ) except Exception as e: logger.error(f"获取 Hyperliquid 账户状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/positions") async def get_positions( symbol: Optional[str] = Query(None, description="币种筛选,如 BTC") ): """ 获取 Hyperliquid 持仓信息 返回所有净持仓(Position Netting 模式) """ try: service = get_hyperliquid_service() if service is None: return { "success": True, "enabled": False, "message": "Hyperliquid 服务未启用", "positions": [] } # 获取持仓 all_positions = service.get_open_positions() # 筛选指定币种 if symbol: symbol = symbol.replace("USDT", "") # 兼容输入 all_positions = [p for p in all_positions if p["coin"] == symbol] # 获取每个持仓的止盈止损价格 positions_data = [] for pos in all_positions: coin = pos["coin"] size = pos["size"] # 获取止盈止损 tp_sl = service.get_tp_sl_prices(coin) positions_data.append({ "symbol": f"{coin}USDT", "side": "long" if size > 0 else "short", "size": abs(size), "entry_price": pos["entry_price"], "unrealized_pnl": pos["unrealized_pnl"], "leverage": (pos.get("leverage") or {}).get("value", "N/A") if isinstance(pos.get("leverage"), dict) else (pos.get("leverage") or "N/A"), "liquidation_price": pos.get("liquidation_price"), "take_profit": tp_sl.get("take_profit"), "stop_loss": tp_sl.get("stop_loss"), "position": pos.get("position") # 原始数据 }) return { "success": True, "enabled": True, "count": len(positions_data), "positions": positions_data } except Exception as e: logger.error(f"获取 Hyperliquid 持仓失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/orders") async def get_orders( symbol: Optional[str] = Query(None, description="币种筛选,如 BTC") ): """ 获取 Hyperliquid 挂单信息 包括: - 限价单(未成交的开仓/平仓单) - 止盈止损单(reduce_only=True) """ try: service = get_hyperliquid_service() if service is None: return { "success": True, "enabled": False, "message": "Hyperliquid 服务未启用", "orders": [] } # 获取所有挂单 all_orders = service.get_open_orders() # 筛选指定币种 if symbol: symbol = symbol.replace("USDT", "") # 兼容输入 all_orders = [o for o in all_orders if o["symbol"] == symbol] # 分类挂单 entry_orders = [] tp_sl_orders = [] for order in all_orders: order_data = { "order_id": order.get("order_id"), "symbol": f"{order['symbol']}USDT", "side": order.get("side"), "size": order.get("size"), "price": order.get("price"), "is_reduce_only": order.get("is_reduce_only", False), "order_type": order.get("order_type", {}) } if order.get("is_reduce_only"): tp_sl_orders.append(order_data) else: entry_orders.append(order_data) return { "success": True, "enabled": True, "counts": { "entry_orders": len(entry_orders), "tp_sl_orders": len(tp_sl_orders), "total": len(all_orders) }, "entry_orders": entry_orders, "tp_sl_orders": tp_sl_orders } except Exception as e: logger.error(f"获取 Hyperliquid 挂单失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/summary") async def get_summary(): """ 获取 Hyperliquid 交易摘要 一次性获取账户、持仓、订单的概要信息 """ try: service = get_hyperliquid_service() if service is None: return { "success": True, "enabled": False, "message": "Hyperliquid 服务未启用" } # 获取账户状态 account_state = service.get_account_state() # 获取持仓 positions = service.get_open_positions() total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in positions) # 获取挂单 orders = service.get_open_orders() # 计算杠杆 if account_state["account_value"] > 0: current_leverage = total_position_value / account_state["account_value"] else: current_leverage = 0 # 计算回撤 if service.initial_balance and service.initial_balance > 0: drawdown = (service.initial_balance - account_state["account_value"]) / service.initial_balance else: drawdown = 0 return { "success": True, "enabled": True, "data": { "account": { "account_value": account_state["account_value"], "available_balance": account_state["available_balance"], "total_margin_used": account_state["total_margin_used"] }, "positions": { "count": len(positions), "total_value": total_position_value }, "orders": { "count": len(orders), "entry_orders": len([o for o in orders if not o.get("is_reduce_only", False)]), "tp_sl_orders": len([o for o in orders if o.get("is_reduce_only", False)]) }, "risk": { "current_leverage": current_leverage, "max_leverage": service.max_total_leverage, "leverage_utilization": (current_leverage / service.max_total_leverage * 100) if service.max_total_leverage > 0 else 0, "drawdown": drawdown * 100, "circuit_breaker_threshold": service.circuit_breaker_drawdown * 100 } } } except Exception as e: logger.error(f"获取 Hyperliquid 摘要失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/orders/cancel") async def cancel_orders( symbol: str = Query(..., description="币种,如 BTC") ): """ 取消指定币种的所有挂单 包括开仓单和止盈止损单 """ try: service = get_hyperliquid_service() if service is None: return { "success": False, "message": "Hyperliquid 服务未启用" } # 取消该币种的所有订单 result = service.cancel_all_orders(symbol.replace("USDT", "")) if result.get("success"): return { "success": True, "message": f"已取消 {symbol} 的所有挂单" } else: return { "success": False, "message": result.get("error", "取消失败") } except Exception as e: logger.error(f"取消 Hyperliquid 挂单失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/positions/close") async def close_position( symbol: str = Query(..., description="币种,如 BTC") ): """ 平掉指定币种的所有持仓(市价平仓) ⚠️ 警告:此操作会立即以市价平仓,请谨慎使用 """ try: service = get_hyperliquid_service() if service is None: return { "success": False, "message": "Hyperliquid 服务未启用" } # 获取持仓 position = service.get_position_for_symbol(symbol.replace("USDT", "")) if not position: return { "success": False, "message": f"未找到 {symbol} 的持仓" } # 取消所有挂单(包括止盈止损) service.cancel_tp_sl_orders(symbol.replace("USDT", "")) # 市价平仓 size = abs(position["size"]) is_long = position["size"] > 0 result = service.place_market_order( symbol=symbol.replace("USDT", ""), is_buy=not is_long, size=size, reduce_only=True ) if result.get("success"): return { "success": True, "message": f"已平仓 {symbol} {size} @ 市价" } else: return { "success": False, "message": result.get("error", "平仓失败") } except Exception as e: logger.error(f"Hyperliquid 平仓失败: {e}") raise HTTPException(status_code=500, detail=str(e))