Split live positions from hedge plans; show option+perp net PnL after fees.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 16:32:13 +08:00
parent 5c95625817
commit 0b3dabbbd9
7 changed files with 446 additions and 12 deletions
+136 -6
View File
@@ -862,7 +862,8 @@
}
function setHpPosTab(tab) {
const next = tab === "pending" || tab === "history" || tab === "stats" ? tab : "live";
const allowed = { live: 1, plans: 1, pending: 1, history: 1, stats: 1 };
const next = allowed[tab] ? tab : "live";
state.posTab = next;
document.querySelectorAll(".hp-pos-tab").forEach(function (btn) {
const on = btn.getAttribute("data-hp-pos-tab") === next;
@@ -875,17 +876,146 @@
if (on) pane.removeAttribute("hidden");
else pane.setAttribute("hidden", "");
});
if (next === "live" || next === "pending") void loadActivePlans();
if (next === "live") void loadLivePositions();
else if (next === "plans" || next === "pending") void loadActivePlans();
else if (next === "history") void loadHistory();
else if (next === "stats") void loadStats();
}
function refreshHpPosPanel() {
void loadLivePositions();
void loadActivePlans();
void loadHistory();
if (state.posTab === "stats") void loadStats();
}
function pnlClsNum(v) {
if (v == null || Number.isNaN(Number(v))) return "";
const n = Number(v);
if (n > 0) return "hp-pnl-pos";
if (n < 0) return "hp-pnl-neg";
return "";
}
function fmtSigned(v, d) {
if (v == null || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const s = fmt(n, d == null ? 2 : d);
return n > 0 ? "+" + s : s;
}
function renderLivePosCard(card) {
const opt = card.option || {};
const perp = card.perp || {};
const combo = card.combo || {};
const optType = String(opt.opt_type || "").toUpperCase() === "P" ? "Put" : "Call";
const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多";
const comboCls = pnlClsNum(combo.net_usdt);
return (
'<article class="hp-live-card" data-plan-id="' +
(card.plan_id || "") +
'">' +
'<div class="hp-live-card-head">' +
"<strong>#" +
(card.plan_id || "—") +
" · " +
(card.underlying || "") +
'</strong><span class="muted">' +
(card.opened_at || "") +
"</span></div>" +
'<div class="hp-live-legs">' +
'<div class="hp-live-leg">' +
'<div class="hp-live-leg-title">期权 · ' +
optType +
"</div>" +
"<code>" +
(opt.inst_id || "—") +
"</code>" +
'<div class="hp-live-leg-grid">' +
"<span>张数 <strong>" +
fmt(opt.sheets, 0) +
"</strong></span>" +
"<span>开仓 <strong>" +
fmt(opt.avg_open, 4) +
"</strong></span>" +
"<span>标记 <strong>" +
fmt(opt.mark_px, 4) +
"</strong></span>" +
'<span>净利 <strong class="' +
pnlClsNum(opt.net_usdt) +
'">' +
fmtSigned(opt.net_usdt, 2) +
" U</strong></span>" +
"</div></div>" +
'<div class="hp-live-leg">' +
'<div class="hp-live-leg-title">永续 · ' +
perpSide +
"</div>" +
"<code>" +
(perp.symbol || "—") +
"</code>" +
'<div class="hp-live-leg-grid">' +
"<span>张数 <strong>" +
fmt(perp.contracts, 4) +
"</strong></span>" +
"<span>开仓 <strong>" +
fmt(perp.avg_open, 2) +
"</strong></span>" +
"<span>标记 <strong>" +
fmt(perp.mark_px, 2) +
"</strong></span>" +
'<span>净利 <strong class="' +
pnlClsNum(perp.net_usdt) +
'">' +
fmtSigned(perp.net_usdt, 2) +
" U</strong></span>" +
"</div></div>" +
"</div>" +
'<div class="hp-live-combo">' +
'<span class="muted">组合净盈亏(扣开平手续费)</span>' +
'<strong class="' +
comboCls +
'">' +
fmtSigned(combo.net_usdt, 2) +
" U</strong>" +
"</div>" +
'<div class="hp-live-actions">' +
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
(card.plan_id || "") +
'">成交细节</button> ' +
'<button type="button" class="btn-secondary hp-btn-end" data-id="' +
(card.plan_id || "") +
'">结束计划</button>' +
"</div></article>"
);
}
async function loadLivePositions() {
const box = $("hp-live-pos-list");
if (!box) return;
try {
const d = await apiJson("/api/hedge-plan/live-positions");
const cards = d.cards || [];
if (!cards.length) {
box.innerHTML = '<p class="muted">暂无期权/永续对冲持仓</p>';
return;
}
box.innerHTML = cards.map(renderLivePosCard).join("");
box.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
btn.addEventListener("click", function () {
void showPlanDetail(Number(btn.getAttribute("data-id")));
});
});
box.querySelectorAll(".hp-btn-end").forEach(function (btn) {
btn.addEventListener("click", function () {
void endActivePlan(Number(btn.getAttribute("data-id")));
});
});
} catch (e) {
box.innerHTML = '<p class="err">' + (e.message || e) + "</p>";
}
}
/** 手动选约:不自动代选;仅在已选失效(换 Call/Put 或筛掉)时清空. */
function autoMatchPoOption(force) {
if (!isOptionPrimary()) return;
@@ -3379,8 +3509,8 @@
if (state.tab === "perp_options" && isOptionPrimary()) void refreshPoStrategyStatus();
}, 15000);
setInterval(function () {
if (state.tab === "perp_options" && (state.posTab === "live" || state.posTab === "pending")) {
void loadActivePlans();
}
}, 20000);
if (state.tab !== "perp_options") return;
if (state.posTab === "live") void loadLivePositions();
else if (state.posTab === "plans" || state.posTab === "pending") void loadActivePlans();
}, 8000);
})();