Improve options order estimates and chain filter defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-11 14:37:53 +08:00
parent 9f0a30a040
commit c9c8388250
4 changed files with 204 additions and 63 deletions
+126 -45
View File
@@ -11,7 +11,7 @@
const state = {
underlying: root.dataset.defaultUnderly || "ETH",
optType: "C",
moneyFilter: "itm",
moneyFilter: "all",
chain: panelCache.chain || null,
selectedInst: null,
orderQuote: null,
@@ -137,12 +137,37 @@
function matchesMoneyFilter(moneyness) {
const m = (moneyness || "").toLowerCase();
if (state.moneyFilter === "all") return true;
if (state.moneyFilter === "otm") return m === "otm";
return m === "itm" || m === "atm";
}
function moneyFilterLabel() {
return state.moneyFilter === "otm" ? "虚值" : "实值";
if (state.moneyFilter === "otm") return "虚值";
if (state.moneyFilter === "itm") return "实值";
return "";
}
function countContractsForType(contracts) {
return (contracts || []).filter(function (c) {
return c.opt_type === state.optType;
}).length;
}
function syncMoneyFilterButtons() {
document.querySelectorAll(".opt-money-btn").forEach(function (b) {
b.classList.toggle("active", (b.getAttribute("data-money") || "") === state.moneyFilter);
});
}
function resetMoneyFilterToAll() {
state.moneyFilter = "all";
syncMoneyFilterButtons();
}
function updateUnderlyingLabel() {
const el = document.getElementById("opt-order-eth-label");
if (el) el.textContent = state.underlying + " 数量";
}
function filterChainContracts(contracts) {
@@ -173,24 +198,40 @@
}
}
function renderExpiries() {
const sel = document.getElementById("opt-exp-select");
sel.innerHTML = '<option value="">选择到期日</option>';
const exps = (state.chain && state.chain.expiries) || [];
exps.forEach(function (e) {
const o = document.createElement("option");
o.value = String(e.exp_time);
o.textContent = expLabel(e.exp_time) + " (" + filterChainContracts(e.contracts).length + ")";
sel.appendChild(o);
});
function renderIndexLine() {
const idx = state.chain && state.chain.index_px;
const dte = state.chain && state.chain.chain_max_dte_days;
if (dte != null) {
const el = document.getElementById("opt-chain-dte");
if (el) el.textContent = String(Math.round(dte));
}
document.getElementById("opt-index-line").textContent =
"指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 实值含平值 · 虚值=价外";
const line = document.getElementById("opt-index-line");
if (line) {
line.textContent =
"指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 默认显示全部 · 实值含平值 · 虚值=价外";
}
}
function renderExpiryOptions(preserveSelection) {
const sel = document.getElementById("opt-exp-select");
if (!sel) return;
const prev = preserveSelection !== false ? sel.value : "";
sel.innerHTML = '<option value="">选择到期日</option>';
const exps = (state.chain && state.chain.expiries) || [];
exps.forEach(function (e) {
const o = document.createElement("option");
o.value = String(e.exp_time);
o.textContent = expLabel(e.exp_time) + " (" + countContractsForType(e.contracts) + ")";
sel.appendChild(o);
});
if (prev && exps.some(function (e) { return String(e.exp_time) === String(prev); })) {
sel.value = prev;
}
}
function renderExpiries() {
renderExpiryOptions(true);
renderIndexLine();
}
function fmtPxSz(px, sz, estimated) {
@@ -259,21 +300,28 @@
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);
function expiryIntrinsicPerUnit(optType, strike, targetIdx) {
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;
}
if (!Number.isFinite(tgt) || !Number.isFinite(k)) 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;
if (o === "C") return Math.max(0, tgt - k);
if (o === "P") return Math.max(0, k - tgt);
return null;
}
function estimateExpiryValue(optType, strike, targetIdx, ethAmount) {
const amt = Number(ethAmount);
const intrinsic = expiryIntrinsicPerUnit(optType, strike, targetIdx);
if (intrinsic == null || !Number.isFinite(amt) || amt <= 0) return null;
return Math.round(intrinsic * amt * 100) / 100;
}
function estimateExpiryProfit(optType, strike, targetIdx, ethAmount, totalPremium) {
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
const prem = Number(totalPremium);
if (value == null || !Number.isFinite(prem)) return null;
return Math.round((value - prem) * 100) / 100;
}
function calcContractLeverage(indexPx, ethAmount, totalPremium) {
@@ -292,14 +340,23 @@
return "约 " + Number(v).toFixed(1) + "×";
}
function fmtUsdcSigned(v) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const sign = n > 0 ? "+" : "";
return sign + fmtUsdc(n) + " USDC";
}
function updateOrderEstimates() {
const levEl = document.getElementById("opt-order-leverage");
const valueEl = document.getElementById("opt-est-value");
const profitEl = document.getElementById("opt-est-profit");
const targetLevEl = document.getElementById("opt-est-leverage");
const targetEl = document.getElementById("opt-target-idx");
const q = state.orderQuote;
if (!q || !q.ok) {
if (levEl) levEl.textContent = "—";
if (valueEl) valueEl.textContent = "—";
if (profitEl) {
profitEl.textContent = "—";
profitEl.className = "v";
@@ -313,20 +370,26 @@
const lev = calcContractLeverage(q.index_px, ethAmount, premium);
if (levEl) levEl.textContent = fmtLeverage(lev);
if (profitEl && targetEl) {
if (valueEl && profitEl && targetEl) {
const targetRaw = targetEl.value;
if (targetRaw === "" || targetRaw == null) {
valueEl.textContent = "—";
profitEl.textContent = "—";
profitEl.className = "v";
if (targetLevEl) targetLevEl.textContent = "—";
} else {
const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), q.ask, ethAmount);
const value = estimateExpiryValue(q.opt_type, q.strike, Number(targetRaw), ethAmount);
const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium);
if (value == null || Number.isNaN(value)) {
valueEl.textContent = "—";
} else {
valueEl.textContent = fmtUsdc(value) + " USDC";
}
if (profit == null || Number.isNaN(profit)) {
profitEl.textContent = "—";
profitEl.className = "v";
} else {
const sign = profit > 0 ? "+" : "";
profitEl.textContent = sign + profit.toFixed(4) + " USDC";
profitEl.textContent = fmtUsdcSigned(profit);
profitEl.className = "v " + pnlCls(profit);
}
const targetLev = calcContractLeverage(Number(targetRaw), ethAmount, premium);
@@ -358,9 +421,9 @@
const tbody = document.getElementById("opt-strike-tbody");
const expMs = document.getElementById("opt-exp-select").value;
const prevSelected = state.selectedInst;
parkOrderPanel();
tbody.innerHTML = "";
if (!expMs || !state.chain) {
parkOrderPanel();
tbody.innerHTML = '<tr><td colspan="8" class="muted">请选择到期日</td></tr>';
state.selectedInst = null;
return;
@@ -368,14 +431,23 @@
const exp = (state.chain.expiries || []).find(function (e) {
return String(e.exp_time) === String(expMs);
});
if (!exp) return;
const list = filterChainContracts(exp.contracts);
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="8" class="muted">该到期日暂无' + moneyFilterLabel() + "合约</td></tr>";
if (!exp) {
parkOrderPanel();
state.selectedInst = null;
return;
}
let stillVisible = false;
const list = filterChainContracts(exp.contracts);
if (!list.length) {
parkOrderPanel();
const label = moneyFilterLabel();
const suffix = label ? label : optTypeLabel(state.optType);
tbody.innerHTML = '<tr><td colspan="8" class="muted">该到期日暂无' + suffix + "合约</td></tr>";
state.selectedInst = null;
return;
}
const stillVisible = !!prevSelected && list.some(function (c) { return c.inst_id === prevSelected; });
if (!stillVisible) parkOrderPanel();
let matchedSelected = false;
list.forEach(function (c) {
const tr = document.createElement("tr");
tr.className = "opt-strike-row";
@@ -393,14 +465,14 @@
'<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;
if (c.inst_id === prevSelected) matchedSelected = true;
});
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
selectContract(btn.getAttribute("data-inst"), btn);
});
});
if (stillVisible && prevSelected) {
if (matchedSelected && prevSelected) {
selectContract(prevSelected, null, true);
} else {
state.selectedInst = null;
@@ -416,7 +488,8 @@
if (bidEl) bidEl.textContent = fmtPxSz(d.bid, d.bid_sz);
document.getElementById("opt-order-sheets").textContent = sz.sheets != null ? sz.sheets : "—";
document.getElementById("opt-order-eth").textContent = sz.eth_amount != null ? sz.eth_amount : "—";
document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmt(sz.total_premium, 4) + " USDC" : "—";
updateUnderlyingLabel();
document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmtUsdc(sz.total_premium) + " USDC" : "—";
const beEl = document.getElementById("opt-order-expiry-be");
const distEl = document.getElementById("opt-order-dist-be");
if (beEl) {
@@ -473,7 +546,9 @@
panelCache.underlying = state.underlying;
panelCache.optType = state.optType;
state.selectedInst = null;
resetMoneyFilterToAll();
parkOrderPanel();
updateUnderlyingLabel();
renderExpiries();
renderStrikes();
}
@@ -973,8 +1048,15 @@
}, 120);
}
function onExpiryChange() {
resetMoneyFilterToAll();
renderStrikes();
}
function bootOptionsPanel() {
updateSizeInputs();
syncMoneyFilterButtons();
updateUnderlyingLabel();
const hasCache =
panelCache.chain &&
panelCache.underlying === state.underlying &&
@@ -1006,22 +1088,21 @@
document.querySelectorAll(".opt-type-btn").forEach(function (b) { b.classList.remove("active"); });
btn.classList.add("active");
state.optType = btn.getAttribute("data-type");
renderExpiries();
resetMoneyFilterToAll();
renderExpiryOptions(true);
renderStrikes();
});
});
document.querySelectorAll(".opt-money-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".opt-money-btn").forEach(function (b) { b.classList.remove("active"); });
btn.classList.add("active");
state.moneyFilter = btn.getAttribute("data-money") || "itm";
renderExpiries();
state.moneyFilter = btn.getAttribute("data-money") || "all";
syncMoneyFilterButtons();
renderStrikes();
});
});
document.getElementById("opt-exp-select").addEventListener("change", renderStrikes);
document.getElementById("opt-exp-select").addEventListener("change", onExpiryChange);
document.getElementById("opt-load-chain").addEventListener("click", loadChain);
document.getElementById("opt-refresh-positions").addEventListener("click", refreshAllPositions);
document.getElementById("opt-open-btn").addEventListener("click", openPosition);