Show editable exit on live cards; price perp PnL via close-side book VWAP.

Align monitor profit-exit with the same book VWAP so card and auto-exit stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 17:29:58 +08:00
parent 3f98ba1e88
commit 9295dcffe3
10 changed files with 597 additions and 10 deletions
+164 -2
View File
@@ -929,19 +929,46 @@
return m + "分";
}
function formatExitLine(ex) {
const e = ex || {};
const mode = String(e.mode || "").toLowerCase() === "target" ? "target" : "profit";
if (mode === "profit") {
return (
"出场 · 盈利金额 <strong>" +
(e.profit_target_usdt != null ? fmt(e.profit_target_usdt, 2) + " U" : "—") +
"</strong>"
);
}
return (
"出场 · 目标位 期权±<strong>" +
(e.option_target_points != null ? fmt(e.option_target_points, 0) : "—") +
"</strong> / 永续±<strong>" +
(e.perp_target_points != null ? fmt(e.perp_target_points, 0) : "—") +
"</strong>"
);
}
function renderLivePosCard(card) {
const opt = card.option || {};
const perp = card.perp || {};
const combo = card.combo || {};
const exit = card.exit || {};
const optType = String(opt.opt_type || "").toUpperCase() === "P" ? "Put" : "Call";
const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多";
const comboCls = pnlClsNum(combo.net_usdt);
const liqWarn = opt.liq_ok === false;
const perpLiqWarn = perp.liq_ok === false;
const unflat = !!card.unflat || String(card.status || "") === "closed";
const bidTxt =
opt.bid != null
? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "")
: "—";
const bookLab = String(perp.book_side || "") === "asks" ? "卖一" : "买一";
const perpTopTxt =
perp.top_px != null
? fmt(perp.top_px, 2) + (perp.top_sz != null ? "/" + fmt(perp.top_sz, 0) : "")
: "—";
const canEditExit = !unflat;
return (
'<article class="hp-live-card' +
(unflat ? " hp-live-card--unflat" : "") +
@@ -969,6 +996,24 @@
fmtDuration(card.remain_sec) +
"</strong></span>" +
"</div>" +
'<div class="hp-live-exit-row">' +
"<span>" +
formatExitLine(exit) +
"</span>" +
(canEditExit
? ' <button type="button" class="btn-secondary hp-btn-exit" data-id="' +
(card.plan_id || "") +
'" data-mode="' +
String(exit.mode || "profit") +
'" data-profit="' +
(exit.profit_target_usdt != null ? exit.profit_target_usdt : "") +
'" data-opt-pts="' +
(exit.option_target_points != null ? exit.option_target_points : "") +
'" data-perp-pts="' +
(exit.perp_target_points != null ? exit.perp_target_points : "") +
'">改出场</button>'
: "") +
"</div>" +
'<div class="hp-live-legs">' +
'<div class="hp-live-leg">' +
'<div class="hp-live-leg-title">期权 · ' +
@@ -1025,9 +1070,22 @@
"<span>开仓 <strong>" +
fmt(perp.avg_open, 2) +
"</strong></span>" +
"<span>标记 <strong>" +
'<span class="muted" title="仅参考,净利按盘口均价">标记 <strong>' +
fmt(perp.mark_px, 2) +
"</strong></span>" +
'<span class="' +
(perpLiqWarn ? "hp-liq-warn" : "") +
'" title="' +
String(perp.liq_msg || "").replace(/"/g, "&quot;") +
'">' +
bookLab +
"/流动性 <strong>" +
perpTopTxt +
(perpLiqWarn ? " · 不足" : "") +
"</strong></span>" +
'<span title="平仓方向扫盘口估成交均价">估均价 <strong>' +
(perp.vwap != null ? fmt(perp.vwap, 2) : "—") +
"</strong></span>" +
"<span>保证金 <strong>" +
(perp.margin != null ? fmt(perp.margin, 2) + " U" : "—") +
"</strong></span>" +
@@ -1039,7 +1097,7 @@
"</div></div>" +
"</div>" +
'<div class="hp-live-combo">' +
'<span class="muted">组合净盈亏(扣开平手续费)</span>' +
'<span class="muted">组合净盈亏(扣开平手续费·永续盘口均价)</span>' +
'<strong class="' +
comboCls +
'">' +
@@ -1057,6 +1115,89 @@
);
}
function setExitModalMode(mode) {
const m = mode === "target" ? "target" : "profit";
document.querySelectorAll(".hp-exit-mode-btn").forEach(function (b) {
const on = b.getAttribute("data-exit") === m;
b.classList.toggle("is-selected", on);
});
const profitBox = $("hp-exit-fields-profit");
const targetBox = $("hp-exit-fields-target");
if (profitBox) {
const show = m === "profit";
profitBox.classList.toggle("hidden", !show);
if (show) profitBox.removeAttribute("hidden");
else profitBox.setAttribute("hidden", "hidden");
}
if (targetBox) {
const show = m === "target";
targetBox.classList.toggle("hidden", !show);
if (show) targetBox.removeAttribute("hidden");
else targetBox.setAttribute("hidden", "hidden");
}
}
function openExitModal(btn) {
const modal = $("hp-exit-modal");
if (!modal || !btn) return;
const id = Number(btn.getAttribute("data-id") || 0);
if (!(id > 0)) return;
if ($("hp-exit-plan-id")) $("hp-exit-plan-id").value = String(id);
const mode = btn.getAttribute("data-mode") === "target" ? "target" : "profit";
setExitModalMode(mode);
if ($("hp-exit-profit")) $("hp-exit-profit").value = btn.getAttribute("data-profit") || "";
if ($("hp-exit-opt-pts")) $("hp-exit-opt-pts").value = btn.getAttribute("data-opt-pts") || "";
if ($("hp-exit-perp-pts")) $("hp-exit-perp-pts").value = btn.getAttribute("data-perp-pts") || "";
if ($("hp-exit-title")) $("hp-exit-title").textContent = "改出场 #" + id;
if ($("hp-exit-err")) {
$("hp-exit-err").textContent = "";
$("hp-exit-err").setAttribute("hidden", "");
}
modal.hidden = false;
}
function closeExitModal() {
const modal = $("hp-exit-modal");
if (modal) modal.hidden = true;
}
async function saveExitModal() {
const id = Number(($("hp-exit-plan-id") && $("hp-exit-plan-id").value) || 0);
const err = $("hp-exit-err");
if (!(id > 0)) return;
const modeBtn = document.querySelector(".hp-exit-mode-btn.is-selected");
const mode = modeBtn && modeBtn.getAttribute("data-exit") === "target" ? "target" : "profit";
const body =
mode === "profit"
? {
exit_mode: "profit",
profit_target_usdt: Number(($("hp-exit-profit") && $("hp-exit-profit").value) || 0),
}
: {
exit_mode: "target",
option_target_points: Number(($("hp-exit-opt-pts") && $("hp-exit-opt-pts").value) || 0),
perp_target_points: Number(($("hp-exit-perp-pts") && $("hp-exit-perp-pts").value) || 0),
};
try {
const d = await apiJson("/api/hedge-plan/" + id + "/exit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
closeExitModal();
alert(d.msg || "出场已更新");
void loadLivePositions();
void loadActivePlans();
} catch (e) {
if (err) {
err.textContent = e.message || String(e);
err.removeAttribute("hidden");
} else {
alert(e.message || String(e));
}
}
}
async function loadLivePositions() {
const box = $("hp-live-pos-list");
if (!box) return;
@@ -1078,6 +1219,11 @@
void endActivePlan(Number(btn.getAttribute("data-id")));
});
});
box.querySelectorAll(".hp-btn-exit").forEach(function (btn) {
btn.addEventListener("click", function () {
openExitModal(btn);
});
});
} catch (e) {
box.innerHTML = '<p class="err">' + (e.message || e) + "</p>";
}
@@ -2823,6 +2969,22 @@
if (ev.target === modal) closeModal();
});
}
document.querySelectorAll(".hp-exit-mode-btn").forEach(function (b) {
b.addEventListener("click", function () {
setExitModalMode(b.getAttribute("data-exit") === "target" ? "target" : "profit");
});
});
if ($("hp-exit-close")) $("hp-exit-close").addEventListener("click", closeExitModal);
if ($("hp-exit-cancel")) $("hp-exit-cancel").addEventListener("click", closeExitModal);
if ($("hp-exit-save")) $("hp-exit-save").addEventListener("click", function () {
void saveExitModal();
});
const exitModal = $("hp-exit-modal");
if (exitModal) {
exitModal.addEventListener("click", function (ev) {
if (ev.target === exitModal) closeExitModal();
});
}
}
async function loadHistory() {
+40
View File
@@ -4202,6 +4202,46 @@ html[data-theme="light"] .opt-be-dist-down {
color: #e8ecf4;
font-weight: 600;
}
.hedge-plan-page-wrap .hp-live-exit-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
margin: 0 0 10px;
padding: 8px 10px;
border-radius: 8px;
background: rgba(110, 140, 255, 0.08);
border: 1px solid rgba(110, 140, 255, 0.18);
font-size: 0.78rem;
color: #c5d0ee;
}
.hedge-plan-page-wrap .hp-live-exit-row strong {
color: #e8ecf4;
font-weight: 650;
}
.hedge-plan-page-wrap .hp-live-exit-row .hp-btn-exit {
padding: 2px 8px;
font-size: 0.72rem;
min-height: 26px;
}
.hedge-plan-page-wrap .hp-exit-modal .hp-exit-mode-seg {
margin-bottom: 12px;
}
.hedge-plan-page-wrap .hp-exit-fields {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.hedge-plan-page-wrap .hp-exit-fields label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.82rem;
}
.hedge-plan-page-wrap .hp-exit-fields input {
max-width: 220px;
}
.hedge-plan-page-wrap .hp-liq-warn,
.hedge-plan-page-wrap .hp-liq-warn strong {
color: #f5c542 !important;