diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index d163871..2d7d3c4 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -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 = '';
- 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 = '';
+ 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 = '
| 请选择到期日 |
';
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 = '| 该到期日暂无' + moneyFilterLabel() + "合约 |
";
+ 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 = '| 该到期日暂无' + suffix + "合约 |
";
+ 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 @@
'' +
"";
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);
diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py
index b05a8ac..aa944c1 100644
--- a/lib/options/options_pricing_lib.py
+++ b/lib/options/options_pricing_lib.py
@@ -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(
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index 24a0d77..c7bfaa0 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -14,7 +14,8 @@
-
+
+
@@ -46,7 +47,7 @@
卖一/张—
买一/张—
张数—
- ETH/BTC 数量—
+ ETH 数量—
预估权利金—
合约杠杆—
到期平衡—
@@ -55,7 +56,9 @@
- 预计盈利
+ 预计价值
+ —
+ 盈利
—
目标杠杆
—
@@ -212,4 +215,4 @@
-
+
diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py
index 34e4031..7e003ca 100644
--- a/tests/test_options_pricing.py
+++ b/tests/test_options_pricing.py
@@ -75,19 +75,51 @@ def test_equivalent_contract_leverage():
assert lev == 144.9
-def test_estimate_expiry_profit_at_index():
- from lib.options.options_pricing_lib import estimate_expiry_profit_at_index
-
- # Call 1780, ask 12.2, 0.01 ETH, target 1793 -> (13-12.2)*0.01 = 0.008
- p = estimate_expiry_profit_at_index(
- opt_type="C", strike=1780, target_idx=1793, entry_px=12.2, eth_amount=0.01
+def test_estimate_expiry_value_and_profit_at_index():
+ from lib.options.options_pricing_lib import (
+ estimate_expiry_profit_at_index,
+ estimate_expiry_value_at_index,
)
- assert p == 0.008
+
+ value = estimate_expiry_value_at_index(
+ opt_type="C", strike=1800, target_idx=2000, eth_amount=1.0
+ )
+ assert value == 200.0
+
+ profit = estimate_expiry_profit_at_index(
+ opt_type="C",
+ strike=1800,
+ target_idx=2000,
+ entry_px=0.148,
+ eth_amount=1.0,
+ total_premium=14.8,
+ )
+ assert profit == 185.2
+
+ # Call 1780, ask 12.2, 0.01 ETH, target 1793 -> value 0.13, profit 0.01
+ v = estimate_expiry_value_at_index(
+ opt_type="C", strike=1780, target_idx=1793, eth_amount=0.01
+ )
+ assert v == 0.13
+ p = estimate_expiry_profit_at_index(
+ opt_type="C",
+ strike=1780,
+ target_idx=1793,
+ entry_px=12.2,
+ eth_amount=0.01,
+ total_premium=0.122,
+ )
+ assert p == 0.01
# OTM call loses premium
p2 = estimate_expiry_profit_at_index(
- opt_type="C", strike=1780, target_idx=1770, entry_px=12.2, eth_amount=0.01
+ opt_type="C",
+ strike=1780,
+ target_idx=1770,
+ entry_px=12.2,
+ eth_amount=0.01,
+ total_premium=0.122,
)
- assert p2 == round(-12.2 * 0.01, 4)
+ assert p2 == -0.12
def test_resolve_chain_quote_otm_no_quote():