Improve options order estimates and chain filter defaults.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -298,16 +298,15 @@ def format_options_breakeven_line(
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def estimate_expiry_profit_at_index(
|
||||
def estimate_expiry_value_at_index(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
target_idx: float | None,
|
||||
entry_px: float | None,
|
||||
eth_amount: float | None,
|
||||
) -> float | None:
|
||||
"""到期测算:目标指数价下按内在价值减权利金(每 1 币报价 × 币量)."""
|
||||
if strike is None or target_idx is None or entry_px is None or eth_amount is None:
|
||||
"""到期测算:目标指数价下期权内在价值总额(不含已付权利金)."""
|
||||
if strike is None or target_idx is None or eth_amount is None:
|
||||
return None
|
||||
if eth_amount <= 0:
|
||||
return None
|
||||
@@ -318,7 +317,33 @@ def estimate_expiry_profit_at_index(
|
||||
intrinsic = max(0.0, float(strike) - float(target_idx))
|
||||
else:
|
||||
return None
|
||||
return round((intrinsic - float(entry_px)) * float(eth_amount), 4)
|
||||
return round(intrinsic * float(eth_amount), 2)
|
||||
|
||||
|
||||
def estimate_expiry_profit_at_index(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
target_idx: float | None,
|
||||
entry_px: float | None,
|
||||
eth_amount: float | None,
|
||||
total_premium: float | None = None,
|
||||
) -> float | None:
|
||||
"""到期测算:目标指数价下净盈利 = 预计价值 − 权利金."""
|
||||
value = estimate_expiry_value_at_index(
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
target_idx=target_idx,
|
||||
eth_amount=eth_amount,
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
prem = total_premium
|
||||
if prem is None and entry_px is not None and eth_amount is not None:
|
||||
prem = float(entry_px) * float(eth_amount)
|
||||
if prem is None:
|
||||
return None
|
||||
return round(float(value) - float(prem), 2)
|
||||
|
||||
|
||||
def equivalent_contract_leverage(
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<select id="opt-exp-select"><option value="">选择到期日</option></select>
|
||||
<button type="button" class="btn-secondary opt-type-btn active" data-type="C">看涨 Call</button>
|
||||
<button type="button" class="btn-secondary opt-type-btn" data-type="P">看跌 Put</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn active" data-money="itm">实值</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn active" data-money="all">全部</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn" data-money="itm">实值</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn" data-money="otm">虚值</button>
|
||||
<button type="button" class="btn-secondary" id="opt-load-chain">刷新链</button>
|
||||
</div>
|
||||
@@ -46,7 +47,7 @@
|
||||
<div><span class="k">卖一/张</span><span id="opt-order-ask" class="v">—</span></div>
|
||||
<div><span class="k">买一/张</span><span id="opt-order-bid" class="v">—</span></div>
|
||||
<div><span class="k">张数</span><span id="opt-order-sheets" class="v">—</span></div>
|
||||
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v">—</span></div>
|
||||
<div><span class="k" id="opt-order-eth-label">ETH 数量</span><span id="opt-order-eth" class="v">—</span></div>
|
||||
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v">—</span></div>
|
||||
<div><span class="k">合约杠杆</span><span id="opt-order-leverage" class="v" title="名义价值÷权利金,测算用">—</span></div>
|
||||
<div><span class="k">到期平衡</span><span id="opt-order-expiry-be" class="v">—</span></div>
|
||||
@@ -55,7 +56,9 @@
|
||||
<div class="options-estimate-row">
|
||||
<label class="opt-est-label" for="opt-target-idx">目标位(指数)</label>
|
||||
<input type="number" id="opt-target-idx" class="opt-target-idx" step="0.1" min="0" placeholder="到期时指数价">
|
||||
<span class="k">预计盈利</span>
|
||||
<span class="k">预计价值</span>
|
||||
<span id="opt-est-value" class="v">—</span>
|
||||
<span class="k">盈利</span>
|
||||
<span id="opt-est-profit" class="v">—</span>
|
||||
<span class="k">目标杠杆</span>
|
||||
<span id="opt-est-leverage" class="v" title="目标位名义价值÷权利金">—</span>
|
||||
@@ -212,4 +215,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=26"></script>
|
||||
<script src="/static/options_panel.js?v=27"></script>
|
||||
|
||||
Reference in New Issue
Block a user