384 lines
12 KiB
Python
384 lines
12 KiB
Python
"""
|
||
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))
|