stock-ai-agent/backend/app/api/hyperliquid.py
2026-03-31 21:38:25 +08:00

384 lines
12 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 交易 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))