Clear target input after set and show estimated value/profit.

Position委托 row keeps the field empty for new entry and displays target value plus estimated PnL after arming.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-15 16:05:54 +08:00
parent 61f63c456b
commit 0bedb2e972
3 changed files with 136 additions and 6 deletions
+28
View File
@@ -4122,6 +4122,34 @@ html[data-theme="light"] .options-estimate-row {
.options-page-wrap .opt-target-row-hint {
font-size: 0.7rem;
}
.options-page-wrap .opt-target-armed {
font-size: 0.78rem;
color: #9ad0ff;
font-variant-numeric: tabular-nums;
}
.options-page-wrap .opt-target-est {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
font-size: 0.78rem;
}
.options-page-wrap .opt-target-est--idle:empty {
display: none;
}
.options-page-wrap .opt-target-est-item {
display: inline-flex;
align-items: baseline;
gap: 4px;
}
.options-page-wrap .opt-target-est-item .k {
color: #9aa8c7;
font-size: 0.7rem;
}
.options-page-wrap .opt-target-est-item .v {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.opt-target-monitors {
margin: 0 0 10px;
padding: 10px 12px;
+81 -5
View File
@@ -859,23 +859,90 @@
);
}
function posEthAmount(p) {
if (p.eth_amount != null && Number(p.eth_amount) > 0) return Number(p.eth_amount);
const sheets = Number(p.avail_pos != null ? p.avail_pos : p.pos);
const ct = Number(p.ct_mult != null ? p.ct_mult : 0.01);
if (Number.isFinite(sheets) && sheets > 0 && Number.isFinite(ct) && ct > 0) return sheets * ct;
return null;
}
function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid) {
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid);
if (value == null && profit == null) return "";
let html = '<span class="opt-target-est">';
html += '<span class="opt-target-est-item"><span class="k">价值</span><span class="v">' +
(value == null ? "—" : fmtUsdc(value) + " USDC") + "</span></span>";
html += '<span class="opt-target-est-item"><span class="k">预估盈利</span><span class="v ' + pnlCls(profit) + '">' +
(profit == null ? "—" : fmtUsdcSigned(profit)) + "</span></span>";
html += "</span>";
return html;
}
function renderTargetDelegateRow(p) {
const inst = p.inst_id || "";
const tgt = p.target_index != null && p.target_index !== "" ? String(p.target_index) : "";
const armed = tgt !== "";
const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null;
const armed = tgt != null && Number.isFinite(tgt) && tgt > 0;
const ethAmt = posEthAmount(p);
const prem = p.premium_paid;
const estHtml = armed
? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem)
: '<span class="opt-target-est opt-target-est--idle"></span>';
return (
'<div class="opt-target-row">' +
'<div class="opt-target-row" data-inst="' + inst + '"' +
' data-opt-type="' + (p.opt_type || "") + '"' +
' data-strike="' + (p.strike != null ? p.strike : "") + '"' +
' data-eth="' + (ethAmt != null ? ethAmt : "") + '"' +
' data-prem="' + (prem != null ? prem : "") + '"' +
' data-armed-target="' + (armed ? tgt : "") + '">' +
'<span class="opt-target-row-label">委托</span>' +
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="监控目标指数" value="' + tgt + '">' +
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="监控目标指数" value="">' +
'<button type="button" class="btn-secondary opt-target-set-btn" data-inst="' + inst + '">设定</button>' +
'<button type="button" class="btn-secondary opt-target-cancel-btn" data-inst="' + inst + '"' + (armed ? "" : " disabled") + ">取消</button>" +
(armed
? '<span class="opt-target-armed">目标 ' + fmt(tgt, 1) + "</span>"
: "") +
estHtml +
'<span class="muted opt-target-row-hint">' +
(armed ? "监控中 · 到位按买一限价平 · 无止损" : "目标=监控指数 · 到位按买一限价平 · 到期即止损") +
(armed ? "监控中 · 到位按买一限价平" : "输入后设定 · 到位按买一限价平 · 到期即止损") +
"</span>" +
"</div>"
);
}
function updatePosTargetEstimate(row) {
if (!row) return;
const est = row.querySelector(".opt-target-est");
if (!est) return;
const inp = row.querySelector(".opt-pos-target-input");
const typed = inp ? String(inp.value || "").trim() : "";
const armed = row.getAttribute("data-armed-target") || "";
const targetRaw = typed !== "" ? typed : armed;
if (targetRaw === "") {
est.className = "opt-target-est opt-target-est--idle";
est.innerHTML = "";
return;
}
const html = formatTargetEstimateHtml(
row.getAttribute("data-opt-type"),
row.getAttribute("data-strike"),
targetRaw,
row.getAttribute("data-eth"),
row.getAttribute("data-prem")
);
if (!html) {
est.className = "opt-target-est opt-target-est--idle";
est.innerHTML = "";
return;
}
const tmp = document.createElement("div");
tmp.innerHTML = html;
const node = tmp.firstChild;
est.className = "opt-target-est";
est.innerHTML = node ? node.innerHTML : "";
}
function renderPositionCard(p) {
return (
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
@@ -953,6 +1020,9 @@
});
container.querySelectorAll(".opt-pos-target-input").forEach(function (inp) {
inp.addEventListener("click", function (e) { e.stopPropagation(); });
inp.addEventListener("input", function () {
updatePosTargetEstimate(inp.closest(".opt-target-row"));
});
inp.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
@@ -979,6 +1049,7 @@
if (!inst) return;
const card = document.querySelector('.opt-pos-card[data-inst="' + inst + '"]') ||
document.querySelector('.opt-pos-accordion-item[data-inst="' + inst + '"]');
const row = card ? card.querySelector(".opt-target-row") : null;
const inp = card ? card.querySelector(".opt-pos-target-input") : null;
const raw = inp ? String(inp.value || "").trim() : "";
const tgt = parseFloat(raw);
@@ -997,6 +1068,11 @@
alert(d.msg || "设定失败");
return;
}
if (inp) inp.value = "";
if (row) {
row.setAttribute("data-armed-target", String(tgt));
updatePosTargetEstimate(row);
}
await refreshAllPositions();
} finally {
if (btn) btn.disabled = false;
+27 -1
View File
@@ -115,7 +115,33 @@
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid, hub) + "</span></div>" +
"</div>" +
(p.target_index != null
? '<div class="opt-target-row opt-target-row--ro"><span class="opt-target-row-label">委托</span><span class="pos-value">目标指数 ' + fmt(p.target_index, 1) + "</span><span class="muted opt-target-row-hint">监控中 · 到位按买一限价平</span></div>"
? (function () {
const eth = p.eth_amount != null ? Number(p.eth_amount)
: (Number(p.pos) > 0 ? Number(p.pos) * Number(p.ct_mult || 0.01) : null);
const strike = Number(p.strike);
const tgt = Number(p.target_index);
const prem = Number(p.premium_paid);
let profit = null;
let value = null;
if (Number.isFinite(tgt) && Number.isFinite(strike) && eth > 0) {
const o = String(p.opt_type || "").toUpperCase();
const intrinsic = o === "C" ? Math.max(0, tgt - strike) : o === "P" ? Math.max(0, strike - tgt) : null;
if (intrinsic != null) {
value = Math.round(intrinsic * eth * 100) / 100;
if (Number.isFinite(prem)) profit = Math.round((value - prem) * 100) / 100;
}
}
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
return (
'<div class="opt-target-row opt-target-row--ro">' +
'<span class="opt-target-row-label">委托</span>' +
'<span class="pos-value">目标 ' + fmt(p.target_index, 1) + "</span>" +
'<span class="pos-value">价值 ' + (value == null ? "—" : fmtUsdc(value) + " USDC") + "</span>" +
'<span class="pos-value' + profitCls + '">预估盈利 ' + profitTxt + "</span>" +
'<span class="muted opt-target-row-hint">监控中 · 到位按买一限价平</span></div>'
);
})()
: "")
);
}