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:
parent
d826c6685f
commit
cb6ab397fe
383
backend/app/api/hyperliquid.py
Normal file
383
backend/app/api/hyperliquid.py
Normal 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))
|
||||
@ -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
716
frontend/hyperliquid.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user