币本位实时盈亏/期权浮盈按ETH展示,避免两位小数抹成0.00U

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 16:31:02 +08:00
parent a7bec5e121
commit b763937ec2
4 changed files with 178 additions and 24 deletions
+40 -8
View File
@@ -1075,7 +1075,7 @@ function refreshOrderDefaults(){
}).catch(()=>{});
}
function paintRealtimePnl(v){
function paintRealtimePnl(v, unit){
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
if(!nodes.length) return;
if(v === null || v === undefined || Number.isNaN(Number(v))){
@@ -1086,8 +1086,16 @@ function paintRealtimePnl(v){
return;
}
const n = Number(v);
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
lastRealtimePnlUnit = u;
const sign = n > 0 ? "+" : "";
const text = `${sign}${n.toFixed(2)}U`;
let text;
if (u === "ETH" || u === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
text = `${n < 0 ? "-" : sign}${abs}${u}`;
} else {
text = `${sign}${n.toFixed(2)}U`;
}
nodes.forEach((pnlEl) => {
pnlEl.innerText = text;
pnlEl.classList.toggle("pnl-pos", n > 0);
@@ -1095,14 +1103,16 @@ function paintRealtimePnl(v){
});
}
let lastRealtimePnl = null;
function updateRealtimePnl(v){
let lastRealtimePnlUnit = "U";
function updateRealtimePnl(v, unit){
if(v != null && !Number.isNaN(Number(v))){
lastRealtimePnl = Number(v);
paintRealtimePnl(v);
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
paintRealtimePnl(v, lastRealtimePnlUnit);
return;
}
if(lastRealtimePnl != null) return;
paintRealtimePnl(v);
paintRealtimePnl(v, lastRealtimePnlUnit);
}
function sumOrdersFloatPnl(orders){
if(!orders || !orders.length) return null;
@@ -1130,9 +1140,31 @@ function paintRealtimePnlFromSnapshot(data){
const perp = data.order_prices && data.order_prices.length
? sumOrdersFloatPnl(data.order_prices)
: null;
const combined = combineRealtimeFloatPnl(perp, data.options_unrealized_pnl);
if(combined !== null || perp !== null || data.options_unrealized_pnl != null){
paintRealtimePnl(combined);
const opt = data.options_unrealized_pnl;
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
const perpSign = Number(perp) > 0 ? "+" : "";
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
lastRealtimePnl = Number(opt);
lastRealtimePnlUnit = underly;
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
pnlEl.innerText = text;
const n = Number(opt) + Number(perp);
pnlEl.classList.toggle("pnl-pos", n > 0);
pnlEl.classList.toggle("pnl-neg", n < 0);
});
return;
}
paintRealtimePnl(opt, underly);
return;
}
const combined = combineRealtimeFloatPnl(perp, opt);
if(combined !== null || perp !== null || opt != null){
paintRealtimePnl(combined, "U");
}
}
+41 -8
View File
@@ -1556,7 +1556,7 @@ function refreshOrderDefaults(){
}).catch(()=>{});
}
function paintRealtimePnl(v){
function paintRealtimePnl(v, unit){
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
if(!nodes.length) return;
if(v === null || v === undefined || Number.isNaN(Number(v))){
@@ -1567,8 +1567,16 @@ function paintRealtimePnl(v){
return;
}
const n = Number(v);
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
lastRealtimePnlUnit = u;
const sign = n > 0 ? "+" : "";
const text = `${sign}${n.toFixed(2)}U`;
let text;
if (u === "ETH" || u === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
text = `${n < 0 ? "-" : sign}${abs}${u}`;
} else {
text = `${sign}${n.toFixed(2)}U`;
}
nodes.forEach((pnlEl) => {
pnlEl.innerText = text;
pnlEl.classList.toggle("pnl-pos", n > 0);
@@ -1576,14 +1584,16 @@ function paintRealtimePnl(v){
});
}
let lastRealtimePnl = null;
function updateRealtimePnl(v){
let lastRealtimePnlUnit = "U";
function updateRealtimePnl(v, unit){
if(v != null && !Number.isNaN(Number(v))){
lastRealtimePnl = Number(v);
paintRealtimePnl(v);
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
paintRealtimePnl(v, lastRealtimePnlUnit);
return;
}
if(lastRealtimePnl != null) return;
paintRealtimePnl(v);
paintRealtimePnl(v, lastRealtimePnlUnit);
}
function sumOrdersFloatPnl(orders){
if(!orders || !orders.length) return null;
@@ -1611,9 +1621,32 @@ function paintRealtimePnlFromSnapshot(data){
const perp = data.order_prices && data.order_prices.length
? sumOrdersFloatPnl(data.order_prices)
: null;
const combined = combineRealtimeFloatPnl(perp, data.options_unrealized_pnl);
if(combined !== null || perp !== null || data.options_unrealized_pnl != null){
paintRealtimePnl(combined);
const opt = data.options_unrealized_pnl;
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
// 币本位期权盈亏单位为币,勿与永续 U 混加成「xxU」
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
const perpSign = Number(perp) > 0 ? "+" : "";
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
lastRealtimePnl = Number(opt);
lastRealtimePnlUnit = underly;
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
pnlEl.innerText = text;
const n = Number(opt) + Number(perp);
pnlEl.classList.toggle("pnl-pos", n > 0);
pnlEl.classList.toggle("pnl-neg", n < 0);
});
return;
}
paintRealtimePnl(opt, underly);
return;
}
const combined = combineRealtimeFloatPnl(perp, opt);
if(combined !== null || perp !== null || opt != null){
paintRealtimePnl(combined, "U");
}
}
+73 -7
View File
@@ -746,6 +746,53 @@
return Number(n).toLocaleString(undefined, { maximumFractionDigits: d });
}
/** 币本位权利金/盈亏:ETH/BTC 保留足量小数;USDC 两位. */
function optPremiumCcyOf(p, fallbackUnderly) {
const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase();
if (ccy) return ccy;
const mode = String((p && p.margin_mode) || "").toLowerCase();
const inst = String((p && p.inst_id) || "");
if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) {
return (inst.split("-")[0] || fallbackUnderly || "ETH").toUpperCase() || "ETH";
}
return "USDC";
}
function optionsPanelCcy(optMeta) {
if (!optMeta || typeof optMeta !== "object") return "USDC";
const mode = String(optMeta.options_margin_mode || optMeta.margin_mode || "").toLowerCase();
const underly = String(optMeta.options_underly || "ETH").toUpperCase() || "ETH";
if (mode === "coin") return underly;
const pos = Array.isArray(optMeta.positions) ? optMeta.positions : [];
for (let i = 0; i < pos.length; i++) {
const c = optPremiumCcyOf(pos[i], underly);
if (c && c !== "USDC") return c;
}
return "USDC";
}
function fmtOptPnlAmt(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const unit = String(ccy || "USDC").toUpperCase();
if (unit === "ETH" || unit === "BTC") {
const s = n.toFixed(8).replace(/\.?0+$/, "");
return s || "0";
}
return fmt(n, 2);
}
function fmtOptPnlText(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const unit = String(ccy || "USDC").toUpperCase();
const sign = n > 0 ? "+" : "";
if (unit === "ETH" || unit === "BTC") {
return `${sign}${fmtOptPnlAmt(n, unit)} ${unit}`;
}
return `${sign}${fmt(n, 2)}U`;
}
/** 交易所持仓开仓价(三所子代理 entry_price) */
function positionEntryPrice(pos) {
if (!pos) return null;
@@ -3900,10 +3947,14 @@
? `${fmt(fundUsdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small>`
: "—";
const tradeTxt = formatCoinTradingLabel(usdt, eth, btc);
const pnlCcy = optionsPanelCcy(optMeta);
const pnlTxt = upnl == null || Number.isNaN(Number(upnl))
? "—"
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy)}</span>`;
return `<div class="${rowCls}">
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fundTxt}</div></div>
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${tradeTxt}</div></div>
<div class="stat-box"><div class="stat-label">${pnlLabel}</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
<div class="stat-box"><div class="stat-label">${pnlLabel}</div><div class="stat-value">${pnlTxt}</div></div>
</div>`;
}
let fundTxt = `${fmt(funding, 2)} <small style="font-size:12px;color:var(--muted)">U</small>`;
@@ -4064,7 +4115,8 @@
<td>${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
${renderOptionsTargetCell(target, p)}`;
if (showPnl) {
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmt(net, 2)}</td>
const premCcy = optPremiumCcyOf(p);
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmtOptPnlText(net, premCcy)}</td>
<td class="${pnlCls(net)}">${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}</td>`;
}
html += "</tr>";
@@ -4549,6 +4601,7 @@
let optLine = "";
let pnlShow = upnl;
let pnlSuffix = "";
let pnlUnit = "U";
if (hasOptCap) {
if (opt.enabled === false) {
optLine = "期权未启用";
@@ -4561,17 +4614,28 @@
const n = Number.isFinite(optCount) ? optCount : 0;
const bal = optionsBalanceFields(opt);
const optUpl = bal.upl != null ? bal.upl : null;
const optCcy = optionsPanelCcy(opt);
const parts = [n > 0 ? `期权 ${n}` : "期权 空仓"];
if (showAccountPnlPref()) {
if (bal.funding != null) parts.push(`资金 ${fmt(bal.funding, 2)}U`);
if (bal.trading != null) parts.push(`交易 ${fmt(bal.trading, 2)}U`);
if (opt.options_margin_mode === "coin") {
const coinTrade = formatCoinTradingLabel(
bal.trading != null ? bal.trading : opt.trading_usdt,
(opt.balances || {}).trading_eth,
(opt.balances || {}).trading_btc
);
if (coinTrade && coinTrade !== "—") parts.push(`交易 ${coinTrade}`);
} else if (bal.trading != null) {
parts.push(`交易 ${fmt(bal.trading, 2)}U`);
}
if (optUpl != null && Number.isFinite(Number(optUpl))) {
parts.push(`浮盈 ${fmt(optUpl, 2)}U`);
parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy)}`);
}
if (optUpl != null && Number.isFinite(Number(optUpl)) && openCount === 0) {
// 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U
pnlShow = optUpl;
pnlSuffix = "期权";
pnlUnit = optCcy;
}
}
optLine = parts.join(" · ");
@@ -4580,6 +4644,10 @@
const hm = row.hub_monitor || {};
const flaskOk = row.flask_ok !== false && hm.ok !== false;
const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
const tilePnlHtml =
pnlUnit === "ETH" || pnlUnit === "BTC"
? `${fmtOptPnlAmt(pnlShow, pnlUnit)} <small>${esc(pnlUnit)}${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`
: `${fmt(pnlShow, 2)} <small>U${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`;
return `<div class="card hub-tile ${tileCls}" data-ex-id="${esc(row.id)}">
<div class="hub-tile-body card-expand-zone" title="点击进入全屏详情">
<div class="hub-tile-top">
@@ -4589,9 +4657,7 @@
</div>
${
showAccountPnlPref()
? `<div class="hub-tile-pnl ${pnlCls(pnlShow)}">${fmt(pnlShow, 2)} <small>U${
pnlSuffix ? " · " + pnlSuffix : ""
}</small></div>`
? `<div class="hub-tile-pnl ${pnlCls(pnlShow)}">${tilePnlHtml}</div>`
: ""
}
<div class="hub-tile-meta">${esc(posLine)}</div>
+24 -1
View File
@@ -40,6 +40,29 @@
return `${n > 0 ? "+" : "-"}${abs}U`;
}
function optPremiumCcyOf(p) {
const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase();
if (ccy) return ccy;
const mode = String((p && p.margin_mode) || "").toLowerCase();
const inst = String((p && p.inst_id) || "");
if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) {
return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH";
}
return "USDC";
}
function pnlSignedOpt(v, ccy) {
const n = Number(v);
if (!Number.isFinite(n)) return "—";
const unit = String(ccy || "USDC").toUpperCase();
if (unit === "ETH" || unit === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
const sign = n > 0 ? "+" : n < 0 ? "-" : "";
return `${sign}${abs} ${unit}`;
}
return pnlSigned(n, 2);
}
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
@@ -338,7 +361,7 @@
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
<td><span class="${targetCls}">${esc(target)}</span></td>`;
if (showPnl) {
html += `<td class="${pnlClass(net)}">${net != null ? pnlSigned(net, 2) : "—"}</td>
html += `<td class="${pnlClass(net)}">${net != null ? pnlSignedOpt(net, optPremiumCcyOf(p)) : "—"}</td>
<td class="${pnlClass(roi)}">${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"}</td>`;
}
html += "</tr>";