diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 502e56b..cb92e6a 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -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;
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 36a23e1..4f4bbbe 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -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 @@
"
" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + " | " +
'' + fmtDist(c.dist_expiry_be) + " | " +
'' +
- ' ' +
- '' +
+ '' +
" | ";
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();
diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py
index 24a8028..5be60da 100644
--- a/lib/options/options_pricing_lib.py
+++ b/lib/options/options_pricing_lib.py
@@ -234,3 +234,26 @@ def format_options_breakeven_line(
if idx_px is not None and parts:
return " ".join(parts) + f"(指数{idx_px:.0f})"
return " ".join(parts)
+
+
+def estimate_expiry_profit_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:
+ return None
+ if eth_amount <= 0:
+ return None
+ o = (opt_type or "").upper()
+ if o == "C":
+ intrinsic = max(0.0, float(target_idx) - float(strike))
+ elif o == "P":
+ intrinsic = max(0.0, float(strike) - float(target_idx))
+ else:
+ return None
+ return round((intrinsic - float(entry_px)) * float(eth_amount), 4)
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index ea4ba7d..e61ceaf 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -49,6 +49,13 @@
到期平衡—
距平衡—
+
+
+
+ 预计盈利
+ —
+ 到期测算,仅供参考
+
diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py
index f3e509e..54cd604 100644
--- a/tests/test_options_pricing.py
+++ b/tests/test_options_pricing.py
@@ -67,6 +67,21 @@ def test_option_moneyness():
assert option_moneyness_label("otm") == "虚值"
+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
+ )
+ assert p == 0.008
+ # 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
+ )
+ assert p2 == round(-12.2 * 0.01, 4)
+
+
def test_format_quote_liquidity():
from lib.options.options_pricing_lib import format_quote_liquidity