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:
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user