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
This commit is contained in:
aaron 2026-03-22 11:50:50 +08:00
parent d826c6685f
commit cb6ab397fe
3 changed files with 1109 additions and 1 deletions

View File

@ -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))

View File

@ -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

716
frontend/hyperliquid.html Normal file
View File

@ -0,0 +1,716 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hyperliquid 交易监控 | AI Agent</title>
<link rel="stylesheet" href="/static/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #30363d;
}
.title {
font-size: 24px;
font-weight: 600;
color: #58a6ff;
}
.status-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-enabled {
background: #238636;
color: #3fb950;
}
.status-disabled {
background: #8c959f;
color: #6e7681;
}
.refresh-btn {
padding: 8px 16px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.refresh-btn:hover {
background: #30363d;
}
.refresh-btn.loading {
opacity: 0.6;
cursor: wait;
}
/* 账户信息卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
padding: 20px;
}
.stat-label {
font-size: 12px;
color: #8b949e;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #58a6ff;
}
.stat-value.positive {
color: #3fb950;
}
.stat-value.negative {
color: #f85149;
}
.stat-value.warning {
color: #d29922;
}
.stat-sub {
font-size: 11px;
color: #8b949e;
margin-top: 4px;
}
/* 内容区域 */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.panel {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
overflow: hidden;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #30363d;
background: #0d1117;
}
.panel-title {
font-size: 16px;
font-weight: 600;
color: #c9d1d9;
}
.panel-count {
font-size: 12px;
color: #8b949e;
}
.panel-body {
padding: 0;
}
/* 列表样式 */
.list-item {
padding: 16px 20px;
border-bottom: 1px solid #21262d;
display: flex;
justify-content: space-between;
align-items: center;
}
.list-item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
gap: 16px;
}
.symbol {
font-size: 16px;
font-weight: 600;
color: #c9d1d9;
min-width: 100px;
}
.side-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.side-long {
background: #238636;
color: #3fb950;
}
.side-short {
background: #8b3c3c;
color: #f85149;
}
.item-details {
display: flex;
gap: 20px;
font-size: 13px;
color: #8b949e;
}
.item-detail span {
color: #c9d1d9;
}
.item-right {
text-align: right;
}
.pnl {
font-size: 14px;
font-weight: 600;
}
.pnl.positive {
color: #3fb950;
}
.pnl.negative {
color: #f85149;
}
/* 空状态 */
.empty-state {
padding: 60px 20px;
text-align: center;
color: #6e7681;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
/* 操作按钮 */
.action-btn {
padding: 6px 12px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: #30363d;
}
.action-btn.danger {
border-color: #f85149;
color: #f85149;
}
.action-btn.danger:hover {
background: #f85149;
color: white;
}
/* 响应式 */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.content-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.stat-value {
font-size: 20px;
}
.symbol {
min-width: 80px;
font-size: 14px;
}
.item-details {
flex-direction: column;
gap: 4px;
font-size: 11px;
}
}
/* 加载动画 */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #30363d;
border-top-color: #58a6ff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
/* 订单类型标签 */
.order-type-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
}
.order-entry {
background: #1f6feb;
color: #58a6ff;
}
.order-tp {
background: #238636;
color: #3fb950;
}
.order-sl {
background: #8b3c3c;
color: #f85149;
}
/* 风险指标 */
.risk-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.risk-bar {
flex: 1;
height: 4px;
background: #21262d;
border-radius: 2px;
overflow: hidden;
}
.risk-fill {
height: 100%;
background: #58a6ff;
transition: width 0.3s ease;
}
.risk-fill.warning {
background: #d29922;
}
.risk-fill.danger {
background: #f85149;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<!-- 头部 -->
<div class="header">
<div class="title">🔥 Hyperliquid 交易监控</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span class="status-badge" :class="enabled ? 'status-enabled' : 'status-disabled'">
{{ enabled ? '已启用' : '未启用' }}
</span>
<button class="refresh-btn" @click="refreshData" :class="{ loading: loading }">
<span v-if="loading" class="spinner"></span>
{{ loading ? '刷新中...' : '刷新' }}
</button>
</div>
</div>
<!-- 未启用提示 -->
<div v-if="!enabled" class="empty-state">
<div class="empty-icon">🔒</div>
<p>Hyperliquid 实盘交易未启用</p>
<p style="font-size: 13px; margin-top: 8px;">请在 .env 文件中设置 hyperliquid_trading_enabled=true</p>
</div>
<!-- 已启用内容 -->
<div v-else>
<!-- 账户信息 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">账户价值</div>
<div class="stat-value">${{ formatNumber(account.account_value) }}</div>
<div class="stat-sub">可用: ${{ formatNumber(account.available_balance) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">总持仓价值</div>
<div class="stat-value">${{ formatNumber(account.total_position_value) }}</div>
<div class="stat-sub">{{ positions.length }} 个持仓</div>
</div>
<div class="stat-card">
<div class="stat-label">总杠杆率</div>
<div class="stat-value" :class="getLeverageClass(account.current_leverage)">
{{ account.current_leverage?.toFixed(1) || '0.0' }}x
</div>
<div class="stat-sub">
上限: {{ account.max_leverage }}x
<span v-if="leverageUtilization > 80" style="color: #d29922; margin-left: 8px;">
⚠️
</span>
</div>
</div>
<div class="stat-card">
<div class="stat-label">回撤</div>
<div class="stat-value" :class="getDrawdownClass(account.drawdown)">
{{ account.drawdown?.toFixed(1) || '0.0' }}%
</div>
<div class="stat-sub">熔断线: {{ account.circuit_breaker_threshold?.toFixed(0) }}%</div>
</div>
<div class="stat-card">
<div class="stat-label">已用保证金</div>
<div class="stat-value">${{ formatNumber(account.total_margin_used) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">挂单数量</div>
<div class="stat-value">{{ orders.entry_orders + orders.tp_sl_orders }}</div>
<div class="stat-sub">
开仓: {{ orders.entry_orders }} |
TP/SL: {{ orders.tp_sl_orders }}
</div>
</div>
</div>
<!-- 持仓和挂单 -->
<div class="content-grid">
<!-- 持仓列表 -->
<div class="panel">
<div class="panel-header">
<div class="panel-title">📈 持仓 ({{ positions.length }})</div>
</div>
<div class="panel-body">
<div v-if="positions.length === 0" class="empty-state">
<div class="empty-icon">📊</div>
<p>暂无持仓</p>
</div>
<div v-else>
<div v-for="pos in positions" :key="pos.symbol" class="list-item">
<div class="item-left">
<div>
<div class="symbol">{{ pos.symbol }}</div>
<span class="side-badge" :class="pos.side === 'long' ? 'side-long' : 'side-short'">
{{ pos.side === 'long' ? '做多' : '做空' }}
</span>
</div>
<div class="item-details">
<div class="item-detail">
数量: <span>{{ pos.size?.toFixed(6) }}</span>
</div>
<div class="item-detail">
入场价: <span>${{ formatNumber(pos.entry_price) }}</span>
</div>
<div class="item-detail">
杠杆: <span>{{ pos.leverage }}x</span>
</div>
</div>
</div>
<div class="item-right">
<div class="pnl" :class="getPnLClass(pos.unrealized_pnl)">
{{ pos.unrealized_pnl >= 0 ? '+' : '' }}${{ formatNumber(pos.unrealized_pnl) }}
</div>
<div style="font-size: 11px; color: #8b949e; margin-top: 4px;">
TP: ${{ formatNumber(pos.take_profit) }} | SL: ${{ formatNumber(pos.stop_loss) }}
</div>
<button class="action-btn danger" @click="closePosition(pos.symbol)" style="margin-top: 8px;">
平仓
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 挂单列表 -->
<div class="panel">
<div class="panel-header">
<div class="panel-title">📋 挂单 ({{ orders.entry_orders + orders.tp_sl_orders }})</div>
</div>
<div class="panel-body">
<div v-if="orders.entry_orders + orders.tp_sl_orders === 0" class="empty-state">
<div class="empty-icon">📋</div>
<p>暂无挂单</p>
</div>
<div v-else>
<!-- 入场单 -->
<div v-if="entryOrdersList.length > 0">
<div style="padding: 12px 20px; font-size: 12px; color: #8b949e; background: #0d1117;">
入场单 ({{ entryOrdersList.length }})
</div>
<div v-for="order in entryOrdersList" :key="order.order_id" class="list-item">
<div class="item-left">
<div class="symbol">{{ order.symbol }}</div>
<div class="item-details">
<div class="item-detail">
{{ order.side === 'B' ? '买入' : '卖出' }}
</div>
<div class="item-detail">
@ ${{ formatNumber(order.price) }}
</div>
<div class="item-detail">
{{ order.size }}
</div>
</div>
</div>
<div class="item-right">
<button class="action-btn danger" @click="cancelOrder(order.symbol)">
取消
</button>
</div>
</div>
</div>
<!-- 止盈止损单 -->
<div v-if="tpSlOrdersList.length > 0">
<div style="padding: 12px 20px; font-size: 12px; color: #8b949e; background: #0d1117; border-top: 1px solid #30363d;">
止盈止损 ({{ tpSlOrdersList.length }})
</div>
<div v-for="order in tpSlOrdersList" :key="order.order_id" class="list-item">
<div class="item-left">
<div class="symbol">{{ order.symbol }}</div>
<div class="item-details">
<div class="item-detail">
<span class="order-type-badge order-tp">TP</span>
<span class="order-type-badge order-sl">SL</span>
</div>
<div class="item-detail">
@ ${{ formatNumber(order.price) }}
</div>
<div class="item-detail">
{{ order.size }}
</div>
</div>
</div>
<div class="item-right">
<span style="font-size: 11px; color: #8b949e;">自动执行</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
loading: false,
enabled: false,
account: {
account_value: 0,
available_balance: 0,
total_margin_used: 0,
total_position_value: 0,
current_leverage: 0,
max_leverage: 10,
drawdown: 0,
circuit_breaker_threshold: 10
},
positions: [],
orders: {
entry_orders: 0,
tp_sl_orders: 0,
all: []
}
}
},
computed: {
leverageUtilization() {
if (!this.account.max_leverage) return 0;
return (this.account.current_leverage / this.account.max_leverage) * 100;
},
entryOrdersList() {
return this.orders.all.filter(o => !o.is_reduce_only);
},
tpSlOrdersList() {
return this.orders.all.filter(o => o.is_reduce_only);
}
},
methods: {
async refreshData() {
this.loading = true;
try {
// 并发获取所有数据
const [accountRes, positionsRes, ordersRes] = await Promise.all([
fetch('/api/hyperliquid/account').then(r => r.json()),
fetch('/api/hyperliquid/positions').then(r => r.json()),
fetch('/api/hyperliquid/orders').then(r => r.json())
]);
// 处理账户数据
if (accountRes.success && accountRes.data) {
this.enabled = accountRes.enabled;
this.account = accountRes.data;
} else {
this.enabled = accountRes.enabled;
}
// 处理持仓数据
if (positionsRes.success && positionsRes.positions) {
this.positions = positionsRes.positions;
}
// 处理订单数据
if (ordersRes.success) {
this.orders = {
entry_orders: ordersRes.counts.entry_orders,
tp_sl_orders: ordersRes.counts.tp_sl_orders,
all: [...ordersRes.entry_orders, ...ordersRes.tp_sl_orders]
};
}
} catch (error) {
console.error('刷新数据失败:', error);
} finally {
this.loading = false;
}
},
formatNumber(num) {
if (num === null || num === undefined) return '0.00';
return parseFloat(num).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
},
getLeverageClass(leverage) {
if (leverage >= 8) return 'warning';
if (leverage >= 9) return 'negative';
return '';
},
getDrawdownClass(drawdown) {
if (drawdown >= 8) return 'warning';
if (drawdown >= 9) return 'negative';
if (drawdown < 0) return 'positive';
return '';
},
getPnLClass(pnl) {
if (pnl > 0) return 'positive';
if (pnl < 0) return 'negative';
return '';
},
async closePosition(symbol) {
if (!confirm(`确定要平掉 ${symbol} 的持仓吗?`)) return;
try {
const response = await fetch(`/api/hyperliquid/positions/close?symbol=${symbol}`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('平仓成功');
await this.refreshData();
} else {
alert('平仓失败: ' + result.message);
}
} catch (error) {
alert('平仓失败: ' + error.message);
}
},
async cancelOrder(symbol) {
if (!confirm(`确定要取消 ${symbol} 的所有挂单吗?`)) return;
try {
const response = await fetch(`/api/hyperliquid/orders/cancel?symbol=${symbol}`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('取消成功');
await this.refreshData();
} else {
alert('取消失败: ' + result.message);
}
} catch (error) {
alert('取消失败: ' + error.message);
}
}
},
async mounted() {
await this.refreshData();
// 自动刷新每30秒
setInterval(() => {
this.refreshData();
}, 30000);
}
}).mount('#app');
</script>
</body>
</html>