diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index b9f70b0..c8de900 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -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"); } } diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 0730f3b..f572fb3 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -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"); } } diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 491f8f1..3ba78cb 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -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)} U` : "—"; const tradeTxt = formatCoinTradingLabel(usdt, eth, btc); + const pnlCcy = optionsPanelCcy(optMeta); + const pnlTxt = upnl == null || Number.isNaN(Number(upnl)) + ? "—" + : `${fmtOptPnlText(upnl, pnlCcy)}`; return `
资金账户
${fundTxt}
交易账户
${tradeTxt}
-
${pnlLabel}
${fmt(upnl, 2)}
+
${pnlLabel}
${pnlTxt}
`; } let fundTxt = `${fmt(funding, 2)} U`; @@ -4064,7 +4115,8 @@ ${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} ${renderOptionsTargetCell(target, p)}`; if (showPnl) { - html += `${net == null ? "—" : fmt(net, 2)} + const premCcy = optPremiumCcyOf(p); + html += `${net == null ? "—" : fmtOptPnlText(net, premCcy)} ${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}`; } html += ""; @@ -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)} ${esc(pnlUnit)}${pnlSuffix ? " · " + esc(pnlSuffix) : ""}` + : `${fmt(pnlShow, 2)} U${pnlSuffix ? " · " + esc(pnlSuffix) : ""}`; return `
@@ -4589,9 +4657,7 @@
${ showAccountPnlPref() - ? `
${fmt(pnlShow, 2)} U${ - pnlSuffix ? " · " + pnlSuffix : "" - }
` + ? `
${tilePnlHtml}
` : "" }
${esc(posLine)}
diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index c8d389a..ce3757e 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -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 @@ ${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} ${esc(target)}`; if (showPnl) { - html += `${net != null ? pnlSigned(net, 2) : "—"} + html += `${net != null ? pnlSignedOpt(net, optPremiumCcyOf(p)) : "—"} ${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"}`; } html += "";