Dashboard OKX card: 3-column perpetual/options split when options enabled.
Show perpetual and options funding metrics separately, total funds row, then positions; other exchanges keep the 2-column layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,29 @@ def _account_capital_base(ac: dict) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _options_layout_enabled(ac: dict) -> bool:
|
||||
if str(ac.get("key") or "").lower() != "okx":
|
||||
return False
|
||||
snap = ac.get("options_snapshot")
|
||||
if isinstance(snap, dict) and snap.get("enabled") is not False:
|
||||
return True
|
||||
return ac.get("options_funding_usdt") is not None or ac.get("options_trading_usdt") is not None
|
||||
|
||||
|
||||
def _perpetual_float_pnl_u(ac: dict) -> Optional[float]:
|
||||
try:
|
||||
total = float(ac.get("float_pnl_u") or 0)
|
||||
except (TypeError, ValueError):
|
||||
total = 0.0
|
||||
opt = ac.get("options_float_pnl_u")
|
||||
if opt is None:
|
||||
return round(total, 4)
|
||||
try:
|
||||
return round(total - float(opt), 4)
|
||||
except (TypeError, ValueError):
|
||||
return round(total, 4)
|
||||
|
||||
|
||||
def _enrich_account_row(ac: dict) -> dict:
|
||||
st = ac.get("trade_stats") or {}
|
||||
capital = _account_capital_base(ac)
|
||||
@@ -61,6 +84,8 @@ def _enrich_account_row(ac: dict) -> dict:
|
||||
"options_trading_usdt": ac.get("options_trading_usdt"),
|
||||
"options_float_pnl_u": ac.get("options_float_pnl_u"),
|
||||
"options_open_position_count": ac.get("options_open_position_count"),
|
||||
"options_layout": _options_layout_enabled(ac),
|
||||
"perpetual_float_pnl_u": _perpetual_float_pnl_u(ac),
|
||||
"capital_total_usdt": round(capital, 4) if capital is not None else None,
|
||||
"available_trading_usdt": ac.get("available_trading_usdt"),
|
||||
"pnl_u": st.get("total_pnl_u"),
|
||||
|
||||
@@ -276,6 +276,71 @@ body.hub-page-dashboard .page#page-dashboard {
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.dash-ac-metrics-3col {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dash-ac-section-label {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--dash-accent);
|
||||
margin-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px dashed color-mix(in srgb, var(--dash-card-border) 80%, transparent);
|
||||
}
|
||||
|
||||
.dash-ac-section-label:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dash-ac-metric-empty {
|
||||
visibility: hidden;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dash-ac-total-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--dash-card-border);
|
||||
background: color-mix(in srgb, var(--dash-accent) 6%, var(--dash-card-bg));
|
||||
font-family: JetBrains Mono, monospace;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.dash-ac-total-row span {
|
||||
color: var(--dash-muted);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.dash-ac-total-row strong {
|
||||
color: var(--dash-text);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.dash-ac-card-options .dash-ac-remark {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--dash-card-border) 80%, transparent);
|
||||
}
|
||||
|
||||
.dash-ac-metrics-3col .dash-ac-metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dash-ac-metrics-3col .dash-ac-metric span,
|
||||
.dash-ac-metrics-3col .dash-ac-metric strong {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dash-ac-metric span {
|
||||
display: block;
|
||||
color: var(--dash-muted);
|
||||
|
||||
@@ -170,8 +170,57 @@
|
||||
});
|
||||
}
|
||||
|
||||
function dashMetric(label, value, valCls) {
|
||||
return `<div class="dash-ac-metric"><span>${esc(label)}</span><strong class="${valCls || ""}">${value}</strong></div>`;
|
||||
}
|
||||
|
||||
function dashSectionLabel(text) {
|
||||
return `<div class="dash-ac-section-label">${esc(text)}</div>`;
|
||||
}
|
||||
|
||||
function renderOkxOptionsMetrics(ac) {
|
||||
const pnl = Number(ac.pnl_u);
|
||||
const perpFloat = Number(
|
||||
ac.perpetual_float_pnl_u != null ? ac.perpetual_float_pnl_u : ac.float_pnl_u
|
||||
);
|
||||
const optFloat = Number(ac.options_float_pnl_u);
|
||||
const pf =
|
||||
ac.perpetual_funding_usdt != null ? ac.perpetual_funding_usdt : ac.funding_usdt;
|
||||
const pt =
|
||||
ac.perpetual_trading_usdt != null ? ac.perpetual_trading_usdt : ac.trading_usdt;
|
||||
return `<div class="dash-ac-metrics dash-ac-metrics-3col">
|
||||
${dashSectionLabel("永续账户")}
|
||||
${dashMetric("资金账户", `${fmt(pf, 2)}U`)}
|
||||
${dashMetric("交易账户", `${fmt(pt, 2)}U`)}
|
||||
${dashMetric("今日盈亏", pnlSigned(pnl, 2), pnlClass(pnl))}
|
||||
${dashMetric("平仓笔数", String(Number(ac.closed_count) || 0))}
|
||||
${dashMetric("浮盈亏", pnlSigned(perpFloat, 2), pnlClass(perpFloat))}
|
||||
<div class="dash-ac-metric dash-ac-metric-empty" aria-hidden="true"></div>
|
||||
${dashSectionLabel("期权账户")}
|
||||
${dashMetric("资金账户", `${fmt(ac.options_funding_usdt, 2)}U`)}
|
||||
${dashMetric("交易账户", `${fmt(ac.options_trading_usdt, 2)}U`)}
|
||||
${dashMetric("期权浮盈亏", pnlSigned(optFloat, 2), pnlClass(optFloat))}
|
||||
</div>
|
||||
<div class="dash-ac-total-row">
|
||||
<span>总资金</span>
|
||||
<strong>${fmt(ac.capital_total_usdt, 2)}U</strong>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStandardMetrics(ac) {
|
||||
const pnl = Number(ac.pnl_u);
|
||||
const floatPnl = Number(ac.float_pnl_u);
|
||||
return `<div class="dash-ac-metrics">
|
||||
${dashMetric("资金账户", `${fmt(ac.funding_usdt, 2)}U`)}
|
||||
${dashMetric("交易账户", `${fmt(ac.trading_usdt, 2)}U`)}
|
||||
${dashMetric("资金合计", `${fmt(ac.capital_total_usdt, 2)}U`)}
|
||||
${dashMetric("今日盈亏", pnlSigned(pnl, 2), pnlClass(pnl))}
|
||||
${dashMetric("平仓笔数", String(Number(ac.closed_count) || 0))}
|
||||
${dashMetric("浮盈亏", pnlSigned(floatPnl, 2), pnlClass(floatPnl))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderAccounts(accounts, threshold) {
|
||||
if (!elAccounts) return;
|
||||
const rows = Array.isArray(accounts) ? accounts : [];
|
||||
if (!rows.length) {
|
||||
elAccounts.innerHTML = '<div class="dash-empty">暂无账户数据</div>';
|
||||
@@ -181,8 +230,6 @@
|
||||
.map((ac) => {
|
||||
const alert = !!ac.loss_alert;
|
||||
const unmon = !ac.monitored;
|
||||
const pnl = Number(ac.pnl_u);
|
||||
const floatPnl = Number(ac.float_pnl_u);
|
||||
const lossPct = Number(ac.daily_loss_pct);
|
||||
const barW =
|
||||
alert && Number.isFinite(lossPct)
|
||||
@@ -195,38 +242,16 @@
|
||||
alert && barW > 0
|
||||
? `<div class="dash-loss-bar" title="占资金合计 ${fmt(lossPct, 2)}%"><i style="width:${barW}%"></i></div>`
|
||||
: "";
|
||||
const optFunding = ac.options_funding_usdt;
|
||||
const optTrading = ac.options_trading_usdt;
|
||||
const optFloat = Number(ac.options_float_pnl_u);
|
||||
const optPos = Number(ac.options_open_position_count) || 0;
|
||||
const optMetrics =
|
||||
optFunding != null || optTrading != null || optPos > 0
|
||||
? `<div class="dash-ac-metric"><span>期权资金</span><strong>${fmt(
|
||||
optFunding != null && optTrading != null
|
||||
? Number(optFunding) + Number(optTrading)
|
||||
: null,
|
||||
2
|
||||
)}U</strong></div>
|
||||
<div class="dash-ac-metric"><span>期权持仓</span><strong>${optPos} 仓</strong></div>
|
||||
<div class="dash-ac-metric"><span>期权浮盈</span><strong class="${pnlClass(optFloat)}">${pnlSigned(
|
||||
optFloat,
|
||||
2
|
||||
)}</strong></div>`
|
||||
: "";
|
||||
return `<article class="dash-ac-card${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
||||
const metricsHtml = ac.options_layout
|
||||
? renderOkxOptionsMetrics(ac)
|
||||
: renderStandardMetrics(ac);
|
||||
const cardCls = ac.options_layout ? " dash-ac-card-options" : "";
|
||||
return `<article class="dash-ac-card${cardCls}${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
||||
<div class="dash-ac-top">
|
||||
<div class="dash-ac-name">${esc(ac.name || "—")}</div>
|
||||
${badge}
|
||||
</div>
|
||||
<div class="dash-ac-metrics">
|
||||
<div class="dash-ac-metric"><span>资金账户</span><strong>${fmt(ac.funding_usdt, 2)}U</strong></div>
|
||||
<div class="dash-ac-metric"><span>交易账户</span><strong>${fmt(ac.trading_usdt, 2)}U</strong></div>
|
||||
<div class="dash-ac-metric"><span>资金合计</span><strong>${fmt(ac.capital_total_usdt, 2)}U</strong></div>
|
||||
<div class="dash-ac-metric"><span>今日盈亏</span><strong class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</strong></div>
|
||||
<div class="dash-ac-metric"><span>平仓笔数</span><strong>${Number(ac.closed_count) || 0}</strong></div>
|
||||
<div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div>
|
||||
${optMetrics}
|
||||
</div>
|
||||
${metricsHtml}
|
||||
${lossBar}
|
||||
${renderAccountDetail(ac)}
|
||||
</article>`;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=3" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260707-dash-okx-options" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -1146,7 +1146,7 @@
|
||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
||||
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||
<script src="/assets/dashboard.js?v=20260707-hub-options-funds"></script>
|
||||
<script src="/assets/dashboard.js?v=20260707-dash-okx-options"></script>
|
||||
<script src="/assets/strategy.js?v=3"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||
|
||||
Reference in New Issue
Block a user