From cb6ab397fed2a4e94991e114775765907e08134f Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 22 Mar 2026 11:50:50 +0800 Subject: [PATCH] feat: Add Hyperliquid trading monitoring page and API - Add hyperliquid API with account/positions/orders endpoints - Add hyperliquid.html monitoring page - Add position close and order cancel actions - Auto-refresh every 30 seconds - Display account value, leverage, drawdown, positions, and orders --- backend/app/api/hyperliquid.py | 383 ++++++++++++++++++ backend/app/main.py | 11 +- frontend/hyperliquid.html | 716 +++++++++++++++++++++++++++++++++ 3 files changed, 1109 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/hyperliquid.py create mode 100644 frontend/hyperliquid.html diff --git a/backend/app/api/hyperliquid.py b/backend/app/api/hyperliquid.py new file mode 100644 index 0000000..057fe30 --- /dev/null +++ b/backend/app/api/hyperliquid.py @@ -0,0 +1,383 @@ +""" +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", {}).get("value", "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)) diff --git a/backend/app/main.py b/backend/app/main.py index 9604d0e..76686d6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from fastapi.responses import FileResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger -from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading, news, astock +from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks, signals, system, real_trading, news, astock, hyperliquid from app.utils.error_handler import setup_global_exception_handler, init_error_notifier from app.utils.system_status import get_system_monitor import os @@ -673,6 +673,7 @@ app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(llm.router, tags=["LLM模型"]) app.include_router(paper_trading.router, tags=["交易"]) app.include_router(real_trading.router, tags=["实盘交易"]) +app.include_router(hyperliquid.router, tags=["Hyperliquid"]) app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"]) app.include_router(astock.router, prefix="/api/astock", tags=["A股分析"]) app.include_router(signals.router, tags=["信号管理"]) @@ -737,6 +738,14 @@ async def status_page(): return FileResponse(page_path) return {"message": "页面不存在"} +@app.get("/hyperliquid") +async def hyperliquid_page(): + """Hyperliquid 交易监控页面""" + page_path = os.path.join(frontend_path, "hyperliquid.html") + if os.path.exists(page_path): + return FileResponse(page_path) + return {"message": "页面不存在"} + if __name__ == "__main__": import uvicorn diff --git a/frontend/hyperliquid.html b/frontend/hyperliquid.html new file mode 100644 index 0000000..5a528fa --- /dev/null +++ b/frontend/hyperliquid.html @@ -0,0 +1,716 @@ + + +
+ + +Hyperliquid 实盘交易未启用
+请在 .env 文件中设置 hyperliquid_trading_enabled=true
+暂无持仓
+暂无挂单
+