币本位实时盈亏/期权浮盈按ETH展示,避免两位小数抹成0.00U
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, "&")
|
||||
@@ -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>";
|
||||
|
||||
Reference in New Issue
Block a user