fix(hub): show contract-based unrealized PnL in monitor and chart
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -902,18 +902,48 @@
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function estimateLinearSwapUpnl(side, entry, mark, contracts, contractSize) {
|
||||
const e = Number(entry);
|
||||
const m = Number(mark);
|
||||
const c = Math.abs(Number(contracts));
|
||||
let mult = Number(contractSize);
|
||||
if (!Number.isFinite(mult) || mult <= 0) mult = 1;
|
||||
if (!Number.isFinite(e) || !Number.isFinite(m) || !Number.isFinite(c) || c <= 0) {
|
||||
return null;
|
||||
}
|
||||
const diff =
|
||||
(side || "long").toLowerCase() === "long" ? m - e : e - m;
|
||||
return Math.round(diff * c * mult * 100) / 100;
|
||||
}
|
||||
|
||||
function formatPosPnlText(ctx) {
|
||||
const upnl = ctx && ctx.unrealized_pnl;
|
||||
if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
|
||||
const n = Number(upnl);
|
||||
let text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
||||
const margin = ctx.plan_margin;
|
||||
const notional = ctx.notional_usdt;
|
||||
if (margin != null && Number(margin) > 1e-8) {
|
||||
const pct = (n / Number(margin)) * 100;
|
||||
const entry = Number(ctx.entry);
|
||||
const contracts = Math.abs(Number(ctx.contracts));
|
||||
const cs =
|
||||
ctx.contract_size != null && Number(ctx.contract_size) > 0
|
||||
? Number(ctx.contract_size)
|
||||
: 1;
|
||||
let pctBase = null;
|
||||
if (notional != null && Math.abs(Number(notional)) > 1e-8) {
|
||||
pctBase = Math.abs(Number(notional));
|
||||
} else if (
|
||||
Number.isFinite(entry) &&
|
||||
entry > 0 &&
|
||||
Number.isFinite(contracts) &&
|
||||
contracts > 0
|
||||
) {
|
||||
pctBase = entry * contracts * cs;
|
||||
}
|
||||
if (pctBase != null && pctBase > 1e-8) {
|
||||
const pct = (n / pctBase) * 100;
|
||||
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
||||
} else if (notional != null && Number(notional) > 1e-8) {
|
||||
const pct = (n / Math.abs(Number(notional))) * 100;
|
||||
} else if (ctx.plan_margin != null && Number(ctx.plan_margin) > 1e-8) {
|
||||
const pct = (n / Number(ctx.plan_margin)) * 100;
|
||||
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
||||
}
|
||||
return { text: text, cls: n > 0 ? "pnl-up" : n < 0 ? "pnl-down" : "" };
|
||||
@@ -963,20 +993,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** 与四实例 calc_pnl 一致:保证金 × 杠杆 × 价格涨跌幅 */
|
||||
function calcPlanFloatingPnl(ctx, markPx) {
|
||||
/** U 本位线性永续:(标记价-开仓价)×张数×contractSize(空头取反) */
|
||||
function calcContractsUpnl(ctx, markPx) {
|
||||
if (!ctx || markPx == null || !Number.isFinite(Number(markPx))) return null;
|
||||
const entry = Number(ctx.entry);
|
||||
const margin = Number(ctx.plan_margin);
|
||||
const lev = Number(ctx.leverage);
|
||||
if (!Number.isFinite(entry) || entry <= 0) return null;
|
||||
if (!Number.isFinite(margin) || margin <= 0) return null;
|
||||
if (!Number.isFinite(lev) || lev <= 0) return null;
|
||||
const mark = Number(markPx);
|
||||
const side = (ctx.side || "long").toLowerCase();
|
||||
const ratio =
|
||||
side === "short" ? (entry - mark) / entry : (mark - entry) / entry;
|
||||
return Math.round(margin * lev * ratio * 100) / 100;
|
||||
return estimateLinearSwapUpnl(
|
||||
ctx.side,
|
||||
ctx.entry,
|
||||
markPx,
|
||||
ctx.contracts,
|
||||
ctx.contract_size
|
||||
);
|
||||
}
|
||||
|
||||
function latestChartMarkPrice() {
|
||||
@@ -996,12 +1022,22 @@
|
||||
? Number(posContext.mark_price)
|
||||
: null);
|
||||
if (mark == null) return false;
|
||||
const live = calcPlanFloatingPnl(posContext, mark);
|
||||
if (live == null) return false;
|
||||
posContext.unrealized_pnl = live;
|
||||
posContext.mark_price = mark;
|
||||
renderPosPnlDisplay(posContext);
|
||||
return true;
|
||||
const live = calcContractsUpnl(posContext, mark);
|
||||
if (live != null) {
|
||||
posContext.unrealized_pnl = live;
|
||||
posContext.mark_price = mark;
|
||||
renderPosPnlDisplay(posContext);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
posContext.unrealized_pnl != null &&
|
||||
Number.isFinite(Number(posContext.unrealized_pnl))
|
||||
) {
|
||||
posContext.mark_price = mark;
|
||||
renderPosPnlDisplay(posContext);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function syncPosTpslFromAgentPosition(p) {
|
||||
@@ -1080,6 +1116,15 @@
|
||||
const p = positions[j];
|
||||
if ((p.side || "").toLowerCase() !== side) continue;
|
||||
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
|
||||
if (p.entry_price != null && Number.isFinite(Number(p.entry_price))) {
|
||||
posContext.entry = Number(p.entry_price);
|
||||
}
|
||||
if (p.contract_size != null && Number.isFinite(Number(p.contract_size))) {
|
||||
posContext.contract_size = Number(p.contract_size);
|
||||
}
|
||||
if (p.contracts != null && Number.isFinite(Number(p.contracts))) {
|
||||
posContext.contracts = Number(p.contracts);
|
||||
}
|
||||
if (p.mark_price != null && Number.isFinite(Number(p.mark_price))) {
|
||||
posContext.mark_price = Number(p.mark_price);
|
||||
}
|
||||
@@ -1093,7 +1138,12 @@
|
||||
if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) {
|
||||
elPosTp.textContent = fmtPrice(posContext.take_profit);
|
||||
}
|
||||
if (!updateLivePosPnl(p.mark_price)) {
|
||||
const markForPnl =
|
||||
latestChartMarkPrice() ||
|
||||
(p.mark_price != null && Number.isFinite(Number(p.mark_price))
|
||||
? Number(p.mark_price)
|
||||
: null);
|
||||
if (!updateLivePosPnl(markForPnl)) {
|
||||
let upnl =
|
||||
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))
|
||||
? Number(p.unrealized_pnl)
|
||||
|
||||
Reference in New Issue
Block a user