fix(hub): show contract-based unrealized PnL in monitor and chart

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 20:20:36 +08:00
parent ef8656e95d
commit 806350231e
6 changed files with 267 additions and 63 deletions
+73 -10
View File
@@ -336,15 +336,73 @@
return "—";
}
function resolveTrendFloatingPnl(pos, trendPlan) {
let upnl = pos && pos.unrealized_pnl;
if (upnl != null && upnl !== "" && Number.isFinite(Number(upnl))) return Number(upnl);
const t = trendPlan || {};
if (t.floating_pnl != null && t.floating_pnl !== "") {
const n = Number(t.floating_pnl);
if (Number.isFinite(n)) return n;
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;
}
return null;
const diff =
(side || "long").toLowerCase() === "long" ? m - e : e - m;
return Math.round(diff * c * mult * 100) / 100;
}
/** 展示浮盈:子代理 unrealized_pnl;与 entry/mark/张数 推算偏差 >20% 时用推算值 */
function resolvePositionUpnlUsdt(pos, trendPlan, markOverride) {
const p = pos || {};
const t = trendPlan || {};
let exchange =
p.unrealized_pnl != null && p.unrealized_pnl !== ""
? Number(p.unrealized_pnl)
: null;
if (exchange != null && !Number.isFinite(exchange)) exchange = null;
const entry =
p.entry_price != null && p.entry_price !== ""
? Number(p.entry_price)
: t.trigger_price != null
? Number(t.trigger_price)
: null;
let mark =
markOverride != null && Number.isFinite(Number(markOverride))
? Number(markOverride)
: p.mark_price != null && p.mark_price !== ""
? Number(p.mark_price)
: t.floating_mark != null
? Number(t.floating_mark)
: t.last_mark_price != null
? Number(t.last_mark_price)
: null;
const contracts = p.contracts;
const cs =
p.contract_size != null && p.contract_size !== ""
? Number(p.contract_size)
: 1;
const computed = estimateLinearSwapUpnl(
p.side || t.direction,
entry,
mark,
contracts,
cs
);
if (computed == null) {
if (exchange != null) return exchange;
if (t.floating_pnl != null && t.floating_pnl !== "") {
const n = Number(t.floating_pnl);
if (Number.isFinite(n)) return n;
}
return null;
}
if (exchange == null) return computed;
const ref = Math.max(Math.abs(computed), 1);
if (Math.abs(exchange - computed) / ref > 0.2) return computed;
return exchange;
}
function resolveTrendFloatingPnl(pos, trendPlan, markOverride) {
return resolvePositionUpnlUsdt(pos, trendPlan, markOverride);
}
function formatFloatingPnlText(upnl, notionalUsdt) {
@@ -1393,7 +1451,10 @@
amount: num(o.amount),
});
});
const upnl = resolveTrendFloatingPnl(pos, trendPlan);
const entryPx = num(pos.entry_price != null ? pos.entry_price : tpsl.entry);
const markPx = num(pos.mark_price);
const contractSize = num(pos.contract_size);
const upnl = resolvePositionUpnlUsdt(pos, trendPlan, markPx);
const planMargin =
trendPlan && trendPlan.plan_margin_capital != null
? num(trendPlan.plan_margin_capital)
@@ -1410,12 +1471,14 @@
exchange_id: exchangeId || null,
symbol: (pos.symbol || "").trim(),
side: (pos.side || "long").toLowerCase(),
entry: num(tpsl.entry),
entry: entryPx,
mark_price: markPx,
stop_loss: num(tpsl.sl),
take_profit: num(tpsl.tp),
tp_monitored: !!tpsl.tp_monitored,
is_trend: !!tpsl.is_trend,
contracts: num(pos.contracts),
contract_size: contractSize != null ? contractSize : 1,
unrealized_pnl: upnl != null ? Number(upnl) : null,
notional_usdt: num(pos.notional_usdt),
plan_margin: planMargin,
+75 -25
View File
@@ -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)
+2 -2
View File
@@ -249,7 +249,7 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-market-pnl-calc"></script>
<script src="/assets/app.js?v=20260604-mo-undef-fix"></script>
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
<script src="/assets/app.js?v=20260604-upnl-contracts"></script>
</body>
</html>