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
+10 -1
View File
@@ -1380,7 +1380,15 @@
const planMargin = const planMargin =
trendPlan && trendPlan.plan_margin_capital != null trendPlan && trendPlan.plan_margin_capital != null
? num(trendPlan.plan_margin_capital) ? num(trendPlan.plan_margin_capital)
: null; : 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 { return {
exchange_id: exchangeId || null, exchange_id: exchangeId || null,
symbol: (pos.symbol || "").trim(), symbol: (pos.symbol || "").trim(),
@@ -1394,6 +1402,7 @@
unrealized_pnl: upnl != null ? Number(upnl) : null, unrealized_pnl: upnl != null ? Number(upnl) : null,
notional_usdt: num(pos.notional_usdt), notional_usdt: num(pos.notional_usdt),
plan_margin: planMargin, plan_margin: planMargin,
leverage: leverage,
orders: orders, orders: orders,
}; };
} }
+96 -23
View File
@@ -936,7 +936,7 @@
return null; return null;
} }
function findTrendPlanMargin(row, sym, side) { function findTrendPlan(row, sym, side) {
const hm = row.hub_monitor; const hm = row.hub_monitor;
if (!hm || !Array.isArray(hm.trends)) return null; if (!hm || !Array.isArray(hm.trends)) return null;
for (let i = 0; i < hm.trends.length; i++) { for (let i = 0; i < hm.trends.length; i++) {
@@ -944,12 +944,66 @@
const ts = normalizeMarketSymbol(t.exchange_symbol || t.symbol || ""); const ts = normalizeMarketSymbol(t.exchange_symbol || t.symbol || "");
if (ts !== sym) continue; if (ts !== sym) continue;
if ((t.direction || "").toLowerCase() !== side) continue; if ((t.direction || "").toLowerCase() !== side) continue;
const m = t.plan_margin_capital; return t;
if (m != null && Number.isFinite(Number(m)) && Number(m) > 0) return Number(m);
} }
return null; 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) { function syncPosTpslFromAgentPosition(p) {
if (!posContext || !p) return; if (!posContext || !p) return;
const et = p.exchange_tpsl; const et = p.exchange_tpsl;
@@ -979,13 +1033,18 @@
} }
} }
function paintPosPnl(ctx) { function renderPosPnlDisplay(ctx) {
if (!elPosPnl) return; if (!elPosPnl) return;
const p = formatPosPnlText(ctx); const p = formatPosPnlText(ctx);
elPosPnl.textContent = p.text; elPosPnl.textContent = p.text;
elPosPnl.className = "market-pos-pnl " + p.cls; elPosPnl.className = "market-pos-pnl " + p.cls;
} }
function paintPosPnl(ctx) {
if (ctx === posContext && updateLivePosPnl()) return;
renderPosPnlDisplay(ctx);
}
function stopPosPnlPoll() { function stopPosPnlPoll() {
if (posPnlTimer) { if (posPnlTimer) {
clearInterval(posPnlTimer); clearInterval(posPnlTimer);
@@ -997,7 +1056,9 @@
stopPosPnlPoll(); stopPosPnlPoll();
if (!posContext || !posContext.exchange_id) return; if (!posContext || !posContext.exchange_id) return;
refreshPosPnlFromBoard(); refreshPosPnlFromBoard();
posPnlTimer = setInterval(refreshPosPnlFromBoard, 5000); posPnlTimer = setInterval(function () {
if (!updateLivePosPnl()) refreshPosPnlFromBoard();
}, 2000);
} }
async function refreshPosPnlFromBoard() { async function refreshPosPnlFromBoard() {
@@ -1013,20 +1074,15 @@
const row = rows[i]; const row = rows[i];
const ex = row.exchange || {}; const ex = row.exchange || {};
if (ex.id !== posContext.exchange_id) continue; if (ex.id !== posContext.exchange_id) continue;
const planMargin = findTrendPlanMargin(row, sym, side); applyTrendPlanFields(row, sym, side);
if (planMargin != null) posContext.plan_margin = planMargin;
const positions = (row.agent && row.agent.positions) || []; const positions = (row.agent && row.agent.positions) || [];
for (let j = 0; j < positions.length; j++) { for (let j = 0; j < positions.length; j++) {
const p = positions[j]; const p = positions[j];
if ((p.side || "").toLowerCase() !== side) continue; if ((p.side || "").toLowerCase() !== side) continue;
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue; if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
let upnl = if (p.mark_price != null && Number.isFinite(Number(p.mark_price))) {
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl)) posContext.mark_price = Number(p.mark_price);
? Number(p.unrealized_pnl) }
: null;
if (upnl == null) upnl = findTrendFloatingPnl(row, sym, side);
if (upnl == null) return;
posContext.unrealized_pnl = upnl;
if (p.notional_usdt != null && Number.isFinite(Number(p.notional_usdt))) { if (p.notional_usdt != null && Number.isFinite(Number(p.notional_usdt))) {
posContext.notional_usdt = Number(p.notional_usdt); posContext.notional_usdt = Number(p.notional_usdt);
} }
@@ -1037,21 +1093,33 @@
if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) { if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) {
elPosTp.textContent = fmtPrice(posContext.take_profit); 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(); updatePositionLines();
try { try {
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext)); sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
} catch (_) {} } catch (_) {}
return; return;
} }
const trendUpnl = findTrendFloatingPnl(row, sym, side); applyTrendPlanFields(row, sym, side);
if (trendUpnl != null) { if (!updateLivePosPnl()) {
posContext.unrealized_pnl = trendUpnl; const trendUpnl = findTrendFloatingPnl(row, sym, side);
paintPosPnl(posContext); if (trendUpnl != null) {
try { posContext.unrealized_pnl = trendUpnl;
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext)); renderPosPnlDisplay(posContext);
} catch (_) {} }
} }
try {
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
} catch (_) {}
return; return;
} }
} catch (_) {} } catch (_) {}
@@ -1956,6 +2024,8 @@
localSeriesVersion = sVer; localSeriesVersion = sVer;
localChartVersion = ver; localChartVersion = ver;
loadChart(false, { autoTick: true }); loadChart(false, { autoTick: true });
} else if (posContext) {
updateLivePosPnl();
} else if (ver !== localChartVersion) { } else if (ver !== localChartVersion) {
localChartVersion = ver; localChartVersion = ver;
} }
@@ -2099,7 +2169,10 @@
applyPriceAutoScale(); applyPriceAutoScale();
updateVisibleRangeMarkers(); updateVisibleRangeMarkers();
syncPosContextForView(exKey, sym); syncPosContextForView(exKey, sym);
if (posContext) refreshPosPnlFromBoard(); if (posContext) {
updateLivePosPnl();
refreshPosPnlFromBoard();
}
showLatestOhlcv(); showLatestOhlcv();
try { try {
updateIndicators(); updateIndicators();
+2 -2
View File
@@ -249,7 +249,7 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <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/chart.js?v=20260604-market-pnl-calc"></script>
<script src="/assets/app.js?v=20260604-market-pnl-plan"></script> <script src="/assets/app.js?v=20260604-market-pnl-calc"></script>
</body> </body>
</html> </html>