fix(hub): recalc market floating PnL from live chart mark

Use plan margin x leverage x price change with latest K-line close instead of stale board snapshot floating_pnl.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 20:09:35 +08:00
parent e6361a7fcc
commit c9ca106b81
3 changed files with 108 additions and 26 deletions
+9
View File
@@ -1380,6 +1380,14 @@
const planMargin =
trendPlan && trendPlan.plan_margin_capital != null
? num(trendPlan.plan_margin_capital)
: mo.margin_capital != null
? num(mo.margin_capital)
: null;
const leverage =
trendPlan && trendPlan.leverage != null
? num(trendPlan.leverage)
: mo.leverage != null
? num(mo.leverage)
: null;
return {
exchange_id: exchangeId || null,
@@ -1394,6 +1402,7 @@
unrealized_pnl: upnl != null ? Number(upnl) : null,
notional_usdt: num(pos.notional_usdt),
plan_margin: planMargin,
leverage: leverage,
orders: orders,
};
}
+91 -18
View File
@@ -936,7 +936,7 @@
return null;
}
function findTrendPlanMargin(row, sym, side) {
function findTrendPlan(row, sym, side) {
const hm = row.hub_monitor;
if (!hm || !Array.isArray(hm.trends)) return null;
for (let i = 0; i < hm.trends.length; i++) {
@@ -944,12 +944,66 @@
const ts = normalizeMarketSymbol(t.exchange_symbol || t.symbol || "");
if (ts !== sym) continue;
if ((t.direction || "").toLowerCase() !== side) continue;
const m = t.plan_margin_capital;
if (m != null && Number.isFinite(Number(m)) && Number(m) > 0) return Number(m);
return t;
}
return null;
}
function applyTrendPlanFields(row, sym, side) {
if (!posContext) return;
const t = findTrendPlan(row, sym, side);
if (!t) return;
const m = t.plan_margin_capital;
if (m != null && Number.isFinite(Number(m)) && Number(m) > 0) {
posContext.plan_margin = Number(m);
}
const lev = t.leverage;
if (lev != null && Number.isFinite(Number(lev)) && Number(lev) > 0) {
posContext.leverage = Number(lev);
}
}
/** 与四实例 calc_pnl 一致:保证金 × 杠杆 × 价格涨跌幅 */
function calcPlanFloatingPnl(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;
}
function latestChartMarkPrice() {
if (!lastCandles || !lastCandles.length) return null;
const bar = lastCandles[lastCandles.length - 1];
const c = bar && bar.close != null ? Number(bar.close) : null;
return c != null && Number.isFinite(c) && c > 0 ? c : null;
}
function updateLivePosPnl(markOverride) {
if (!posContext) return false;
const mark =
markOverride != null && Number.isFinite(Number(markOverride))
? Number(markOverride)
: latestChartMarkPrice() ||
(posContext.mark_price != null && Number.isFinite(Number(posContext.mark_price))
? 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;
}
function syncPosTpslFromAgentPosition(p) {
if (!posContext || !p) return;
const et = p.exchange_tpsl;
@@ -979,13 +1033,18 @@
}
}
function paintPosPnl(ctx) {
function renderPosPnlDisplay(ctx) {
if (!elPosPnl) return;
const p = formatPosPnlText(ctx);
elPosPnl.textContent = p.text;
elPosPnl.className = "market-pos-pnl " + p.cls;
}
function paintPosPnl(ctx) {
if (ctx === posContext && updateLivePosPnl()) return;
renderPosPnlDisplay(ctx);
}
function stopPosPnlPoll() {
if (posPnlTimer) {
clearInterval(posPnlTimer);
@@ -997,7 +1056,9 @@
stopPosPnlPoll();
if (!posContext || !posContext.exchange_id) return;
refreshPosPnlFromBoard();
posPnlTimer = setInterval(refreshPosPnlFromBoard, 5000);
posPnlTimer = setInterval(function () {
if (!updateLivePosPnl()) refreshPosPnlFromBoard();
}, 2000);
}
async function refreshPosPnlFromBoard() {
@@ -1013,20 +1074,15 @@
const row = rows[i];
const ex = row.exchange || {};
if (ex.id !== posContext.exchange_id) continue;
const planMargin = findTrendPlanMargin(row, sym, side);
if (planMargin != null) posContext.plan_margin = planMargin;
applyTrendPlanFields(row, sym, side);
const positions = (row.agent && row.agent.positions) || [];
for (let j = 0; j < positions.length; j++) {
const p = positions[j];
if ((p.side || "").toLowerCase() !== side) continue;
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
let upnl =
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))
? Number(p.unrealized_pnl)
: null;
if (upnl == null) upnl = findTrendFloatingPnl(row, sym, side);
if (upnl == null) return;
posContext.unrealized_pnl = upnl;
if (p.mark_price != null && Number.isFinite(Number(p.mark_price))) {
posContext.mark_price = Number(p.mark_price);
}
if (p.notional_usdt != null && Number.isFinite(Number(p.notional_usdt))) {
posContext.notional_usdt = Number(p.notional_usdt);
}
@@ -1037,21 +1093,33 @@
if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) {
elPosTp.textContent = fmtPrice(posContext.take_profit);
}
paintPosPnl(posContext);
if (!updateLivePosPnl(p.mark_price)) {
let upnl =
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))
? Number(p.unrealized_pnl)
: findTrendFloatingPnl(row, sym, side);
if (upnl != null) {
posContext.unrealized_pnl = upnl;
renderPosPnlDisplay(posContext);
}
}
updatePositionLines();
try {
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
} catch (_) {}
return;
}
applyTrendPlanFields(row, sym, side);
if (!updateLivePosPnl()) {
const trendUpnl = findTrendFloatingPnl(row, sym, side);
if (trendUpnl != null) {
posContext.unrealized_pnl = trendUpnl;
paintPosPnl(posContext);
renderPosPnlDisplay(posContext);
}
}
try {
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
} catch (_) {}
}
return;
}
} catch (_) {}
@@ -1956,6 +2024,8 @@
localSeriesVersion = sVer;
localChartVersion = ver;
loadChart(false, { autoTick: true });
} else if (posContext) {
updateLivePosPnl();
} else if (ver !== localChartVersion) {
localChartVersion = ver;
}
@@ -2099,7 +2169,10 @@
applyPriceAutoScale();
updateVisibleRangeMarkers();
syncPosContextForView(exKey, sym);
if (posContext) refreshPosPnlFromBoard();
if (posContext) {
updateLivePosPnl();
refreshPosPnlFromBoard();
}
showLatestOhlcv();
try {
updateIndicators();
+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-live"></script>
<script src="/assets/app.js?v=20260604-market-pnl-plan"></script>
<script src="/assets/chart.js?v=20260604-market-pnl-calc"></script>
<script src="/assets/app.js?v=20260604-market-pnl-calc"></script>
</body>
</html>