Add expiry profit estimate and remove chain quick-buy button.

Let users enter a target index in the order panel for estimated expiry P&L, and remove the redundant buy button from the options chain row.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 16:46:21 +08:00
parent 2cfc1a490f
commit 27921213b0
5 changed files with 146 additions and 10 deletions
+34
View File
@@ -2658,6 +2658,40 @@ html[data-theme="light"] .opt-be-dist-down {
.options-page-wrap .options-order-grid .v {
font-size: 0.8rem;
}
.options-estimate-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
margin: 8px 0 10px;
padding: 8px 10px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px dashed rgba(255, 255, 255, 0.08);
font-size: 0.74rem;
}
.options-estimate-row .opt-est-label {
color: #8892b0;
}
.options-estimate-row .opt-target-idx {
width: 120px;
font-size: 0.74rem;
padding: 3px 6px;
}
.options-estimate-row .k {
color: #8892b0;
}
.options-estimate-row .v {
font-size: 0.82rem;
font-weight: 600;
}
.options-estimate-row .opt-est-note {
font-size: 0.66rem;
}
html[data-theme="light"] .options-estimate-row {
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.08);
}
.options-hint {
font-size: 0.75rem;
margin-bottom: 6px;
+67 -10
View File
@@ -13,6 +13,7 @@
optType: "C",
chain: panelCache.chain || null,
selectedInst: null,
orderQuote: null,
};
function fmt(v, d) {
@@ -72,7 +73,7 @@
const tr = document.createElement("tr");
tr.className = "opt-order-inline-row";
const td = document.createElement("td");
td.colSpan = 6;
td.colSpan = 8;
td.appendChild(panel);
tr.appendChild(td);
row.after(tr);
@@ -158,6 +159,60 @@
return price + "/" + size;
}
function pnlCls(v) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "";
const n = Number(v);
if (n > 0) return "pos-pnl-profit";
if (n < 0) return "pos-pnl-loss";
return "";
}
function estimateExpiryProfit(optType, strike, targetIdx, entryPx, ethAmount) {
if (strike == null || targetIdx == null || entryPx == null || ethAmount == null) return null;
const amt = Number(ethAmount);
const entry = Number(entryPx);
const tgt = Number(targetIdx);
const k = Number(strike);
if (!Number.isFinite(amt) || !Number.isFinite(entry) || !Number.isFinite(tgt) || !Number.isFinite(k) || amt <= 0) {
return null;
}
const o = (optType || "").toUpperCase();
let intrinsic = 0;
if (o === "C") intrinsic = Math.max(0, tgt - k);
else if (o === "P") intrinsic = Math.max(0, k - tgt);
else return null;
return Math.round((intrinsic - entry) * amt * 10000) / 10000;
}
function updateEstimatedProfit() {
const el = document.getElementById("opt-est-profit");
const targetEl = document.getElementById("opt-target-idx");
if (!el || !targetEl) return;
const q = state.orderQuote;
if (!q || !q.ok) {
el.textContent = "—";
el.className = "v";
return;
}
const targetRaw = targetEl.value;
if (targetRaw === "" || targetRaw == null) {
el.textContent = "—";
el.className = "v";
return;
}
const sz = q.sizing || {};
const ethAmount = sz.eth_amount;
const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), q.ask, ethAmount);
if (profit == null || Number.isNaN(profit)) {
el.textContent = "—";
el.className = "v";
return;
}
const sign = profit > 0 ? "+" : "";
el.textContent = sign + profit.toFixed(4) + " USDC";
el.className = "v " + pnlCls(profit);
}
function fmtDist(v) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
@@ -211,8 +266,7 @@
"<td>" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "</td>" +
'<td class="' + distBeClass(c.dist_expiry_be) + '">' + fmtDist(c.dist_expiry_be) + "</td>" +
'<td class="opt-row-actions">' +
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button> ' +
'<button type="button" class="btn-primary opt-buy-btn" data-inst="' + c.inst_id + '">买入</button>' +
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button>' +
"</td>";
tbody.appendChild(tr);
if (c.inst_id === prevSelected) stillVisible = true;
@@ -222,12 +276,6 @@
selectContract(btn.getAttribute("data-inst"), btn);
});
});
tbody.querySelectorAll(".opt-buy-btn").forEach(function (btn) {
btn.addEventListener("click", async function () {
await selectContract(btn.getAttribute("data-inst"), null);
await openPosition();
});
});
if (stillVisible && prevSelected) {
selectContract(prevSelected, null, true);
} else {
@@ -236,6 +284,7 @@
}
function fillOrderPanel(d) {
state.orderQuote = d && d.ok ? d : null;
const sz = d.sizing || {};
document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || "";
document.getElementById("opt-order-ask").textContent = fmtPxSz(d.ask, d.ask_sz);
@@ -264,6 +313,7 @@
msgEl.textContent = "";
msgEl.classList.remove("opt-error");
}
updateEstimatedProfit();
}
async function selectContract(instId, pickBtn, silent) {
@@ -553,12 +603,19 @@
});
});
["opt-sheets-amount", "opt-eth-amount"].forEach(function (id) {
["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("change", function () {
if (id === "opt-target-idx") {
updateEstimatedProfit();
return;
}
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
if (id === "opt-target-idx") {
el.addEventListener("input", updateEstimatedProfit);
}
});
bootOptionsPanel();