feat(hub): show funding, trading account and unrealized PnL on monitor cards
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1062,8 +1062,8 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict
|
|||||||
|
|
||||||
async def _fetch_exchange_flask_bundle(
|
async def _fetch_exchange_flask_bundle(
|
||||||
client: httpx.AsyncClient, ex: dict
|
client: httpx.AsyncClient, ex: dict
|
||||||
) -> tuple[dict | None, dict | None, list | None, dict | None]:
|
) -> tuple[dict | None, dict | None, list | None, dict | None, dict | None]:
|
||||||
"""单所 Flask:monitor / meta / price_snapshot(有 flask_url 时)并行拉取。"""
|
"""单所 Flask:monitor / meta / price_snapshot / account(有 flask_url 时)并行拉取。"""
|
||||||
caps = ex.get("capabilities") or []
|
caps = ex.get("capabilities") or []
|
||||||
tasks = [
|
tasks = [
|
||||||
_fetch_flask_json(client, ex, "/api/hub/monitor"),
|
_fetch_flask_json(client, ex, "/api/hub/monitor"),
|
||||||
@@ -1071,28 +1071,43 @@ async def _fetch_exchange_flask_bundle(
|
|||||||
]
|
]
|
||||||
has_flask = bool((ex.get("flask_url") or "").strip())
|
has_flask = bool((ex.get("flask_url") or "").strip())
|
||||||
if has_flask:
|
if has_flask:
|
||||||
tasks.append(_fetch_flask_json(client, ex, "/api/price_snapshot"))
|
tasks.extend(
|
||||||
|
[
|
||||||
|
_fetch_flask_json(client, ex, "/api/price_snapshot"),
|
||||||
|
_fetch_flask_json(client, ex, "/api/hub/account"),
|
||||||
|
]
|
||||||
|
)
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
hub_mon = results[0]
|
hub_mon = results[0]
|
||||||
meta = results[1]
|
meta = results[1]
|
||||||
snap = results[2] if has_flask and len(results) > 2 else None
|
snap = results[2] if has_flask and len(results) > 2 else None
|
||||||
|
account = results[3] if has_flask and len(results) > 3 else None
|
||||||
key_prices = None
|
key_prices = None
|
||||||
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
|
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
|
||||||
if want_prices and isinstance(snap, dict):
|
if want_prices and isinstance(snap, dict):
|
||||||
key_prices = snap.get("key_prices")
|
key_prices = snap.get("key_prices")
|
||||||
return hub_mon, meta, key_prices, snap if isinstance(snap, dict) else None
|
return (
|
||||||
|
hub_mon,
|
||||||
|
meta,
|
||||||
|
key_prices,
|
||||||
|
snap if isinstance(snap, dict) else None,
|
||||||
|
account if isinstance(account, dict) else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _assemble_board_row(
|
async def _assemble_board_row(
|
||||||
client: httpx.AsyncClient, ex: dict, agent_row: dict
|
client: httpx.AsyncClient, ex: dict, agent_row: dict
|
||||||
) -> dict:
|
) -> dict:
|
||||||
hub_mon, meta, key_prices, snap = await _fetch_exchange_flask_bundle(client, ex)
|
hub_mon, meta, key_prices, snap, account = await _fetch_exchange_flask_bundle(
|
||||||
|
client, ex
|
||||||
|
)
|
||||||
if isinstance(hub_mon, dict):
|
if isinstance(hub_mon, dict):
|
||||||
_merge_flask_order_price_fields(hub_mon, snap)
|
_merge_flask_order_price_fields(hub_mon, snap)
|
||||||
_merge_flask_exchange_tpsl(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
_merge_flask_exchange_tpsl(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||||
_merge_flask_position_breakeven(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
_merge_flask_position_breakeven(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||||
_merge_flask_position_mark_price(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
_merge_flask_position_mark_price(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||||
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
|
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
|
||||||
|
acct_ok = isinstance(account, dict) and account.get("ok") is not False
|
||||||
raw_review = (ex.get("review_url") or "").strip()
|
raw_review = (ex.get("review_url") or "").strip()
|
||||||
review_link = browser_url(raw_review) if raw_review else default_review_url(
|
review_link = browser_url(raw_review) if raw_review else default_review_url(
|
||||||
ex.get("flask_url")
|
ex.get("flask_url")
|
||||||
@@ -1107,6 +1122,10 @@ async def _assemble_board_row(
|
|||||||
"flask_error": _flask_error_from_hub_mon(hub_mon if isinstance(hub_mon, dict) else None),
|
"flask_error": _flask_error_from_hub_mon(hub_mon if isinstance(hub_mon, dict) else None),
|
||||||
"meta": (meta or {}).get("meta") if isinstance(meta, dict) else meta,
|
"meta": (meta or {}).get("meta") if isinstance(meta, dict) else meta,
|
||||||
"key_prices": key_prices,
|
"key_prices": key_prices,
|
||||||
|
"funding_usdt": account.get("funding_usdt") if acct_ok else None,
|
||||||
|
"trading_usdt": account.get("trading_usdt") if acct_ok else None,
|
||||||
|
"available_trading_usdt": account.get("available_trading_usdt") if acct_ok else None,
|
||||||
|
"account_ok": acct_ok,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1354,7 +1354,7 @@ body.market-chart-fs-open {
|
|||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -2463,12 +2463,16 @@ body.login-page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-row {
|
.stat-row {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-fullscreen {
|
.exchange-fullscreen {
|
||||||
|
|||||||
@@ -854,7 +854,11 @@
|
|||||||
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||||
const flaskOk = row.flask_ok !== false && hm.ok !== false;
|
const flaskOk = row.flask_ok !== false && hm.ok !== false;
|
||||||
const upnl = Number(ag.total_unrealized_pnl);
|
const upnl = Number(ag.total_unrealized_pnl);
|
||||||
const balance = Number(ag.balance_usdt);
|
const tradingBal = Number(row.trading_usdt);
|
||||||
|
const balance =
|
||||||
|
Number.isFinite(tradingBal) && tradingBal > 0
|
||||||
|
? tradingBal
|
||||||
|
: Number(ag.balance_usdt);
|
||||||
const sortUpnl = Number.isFinite(upnl) ? upnl : 0;
|
const sortUpnl = Number.isFinite(upnl) ? upnl : 0;
|
||||||
|
|
||||||
if (!row.http_ok) {
|
if (!row.http_ok) {
|
||||||
@@ -2298,12 +2302,18 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderAccountStatRow(row, ag) {
|
||||||
|
const upnl = ag.total_unrealized_pnl;
|
||||||
|
return `<div class="stat-row">
|
||||||
|
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fmt(row.funding_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||||
|
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${fmt(row.trading_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||||
|
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
|
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
|
||||||
const tickMap = buildPriceTickMap(row);
|
const tickMap = buildPriceTickMap(row);
|
||||||
let inner = `<div class="stat-row">
|
let inner = renderAccountStatRow(row, ag);
|
||||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
|
||||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 2)}</div></div>
|
|
||||||
</div>`;
|
|
||||||
inner += `<div class="section-title">交易所持仓 · ${pos.length} 仓</div>`;
|
inner += `<div class="section-title">交易所持仓 · ${pos.length} 仓</div>`;
|
||||||
if (pos.length) {
|
if (pos.length) {
|
||||||
inner += renderGridPositionsTable(
|
inner += renderGridPositionsTable(
|
||||||
@@ -2353,10 +2363,7 @@
|
|||||||
html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
|
html += `<div class="err">${esc(row.error || ag.error || "子代理不可用")}</div>`;
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
html += `<div class="stat-row">
|
html += renderAccountStatRow(row, ag);
|
||||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} U</div></div>
|
|
||||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 2)}</div></div>
|
|
||||||
</div>`;
|
|
||||||
const posCount = pos.length;
|
const posCount = pos.length;
|
||||||
const posListCls = hubPosListCountClass(posCount);
|
const posListCls = hubPosListCountClass(posCount);
|
||||||
html += `<div class="section-title">持仓(${posCount} 仓 · 每币种一卡)</div>`;
|
html += `<div class="section-title">持仓(${posCount} 仓 · 每币种一卡)</div>`;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260607-hub-ai-v6" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260607-hub-board-v1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -295,6 +295,6 @@
|
|||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
|
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||||
<script src="/assets/app.js?v=20260607-hub-ai-v2"></script>
|
<script src="/assets/app.js?v=20260607-hub-board-v1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user