diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 2174310..ad239bb 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -2588,6 +2588,18 @@ html[data-theme="light"] .settings-side-export-label {
.options-page-wrap .options-strike-table code {
font-size: 0.66rem;
}
+.opt-be-dist-up {
+ color: #5ee89a;
+}
+.opt-be-dist-down {
+ color: #ff8a8a;
+}
+html[data-theme="light"] .opt-be-dist-up {
+ color: #0d7a45;
+}
+html[data-theme="light"] .opt-be-dist-down {
+ color: #c62828;
+}
.options-chain-toolbar .btn-secondary.active,
.opt-uly-btn.active,
.opt-type-btn.active {
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index c051f2b..16cf88b 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -149,6 +149,21 @@
"指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 实值=价内 · 虚值=价外";
}
+ function fmtDist(v) {
+ if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
+ const n = Number(v);
+ const sign = n > 0 ? "+" : "";
+ return sign + n.toFixed(1);
+ }
+
+ function distBeClass(v) {
+ if (v === null || v === undefined || Number.isNaN(Number(v))) return "";
+ const n = Number(v);
+ if (n > 0) return "opt-be-dist-up";
+ if (n < 0) return "opt-be-dist-down";
+ return "";
+ }
+
function renderStrikes() {
const tbody = document.getElementById("opt-strike-tbody");
const expMs = document.getElementById("opt-exp-select").value;
@@ -156,7 +171,7 @@
parkOrderPanel();
tbody.innerHTML = "";
if (!expMs || !state.chain) {
- tbody.innerHTML = '
| 请选择到期日 |
';
+ tbody.innerHTML = '| 请选择到期日 |
';
state.selectedInst = null;
return;
}
@@ -168,7 +183,7 @@
return c.opt_type === state.optType;
});
if (!list.length) {
- tbody.innerHTML = '| 该到期日暂无报价 |
';
+ tbody.innerHTML = '| 该到期日暂无报价 |
';
state.selectedInst = null;
return;
}
@@ -184,6 +199,8 @@
"" + c.inst_id + " | " +
"" + fmt(c.ask, 4) + " | " +
"" + fmt(c.bid, 4) + " | " +
+ "" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + " | " +
+ '' + fmtDist(c.dist_expiry_be) + " | " +
'' +
' ' +
'' +
@@ -216,6 +233,15 @@
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" : "—";
+ const beEl = document.getElementById("opt-order-expiry-be");
+ const distEl = document.getElementById("opt-order-dist-be");
+ if (beEl) {
+ beEl.textContent = d.expiry_be_px != null ? fmt(d.expiry_be_px, 0) : "—";
+ }
+ if (distEl) {
+ distEl.textContent = fmtDist(d.dist_expiry_be);
+ distEl.className = "v " + distBeClass(d.dist_expiry_be);
+ }
const msgEl = document.getElementById("opt-order-msg");
if (!d.ok) {
msgEl.textContent = d.msg || "报价失败";
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index 21f79d3..9ecd860 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -9,7 +9,13 @@ from typing import Any, Callable
import ccxt
-from lib.options.options_pricing_lib import is_shallow_itm, option_moneyness, option_moneyness_label
+from lib.options.options_pricing_lib import (
+ expiry_breakeven_from_ask,
+ idx_distance_to_be,
+ is_shallow_itm,
+ option_moneyness,
+ option_moneyness_label,
+)
_OKX_OPTION_ERR_ZH: dict[str, str] = {
"51018": "期权账户不能持有净空头头寸",
@@ -331,8 +337,15 @@ def build_option_chain(
t = tickers.get(inst_id) or {}
ask = _safe_float(t.get("askPx"))
bid = _safe_float(t.get("bidPx"))
- if ask is None and bid is None:
+ mark = _safe_float(t.get("markPx"))
+ if ask is None and bid is None and mark is None:
continue
+ expiry_be = expiry_breakeven_from_ask(
+ opt_type=opt_type,
+ strike=strike,
+ ask_px=ask,
+ mark_px=mark,
+ )
mny = option_moneyness(opt_type=opt_type, strike=strike, index_px=idx)
exp_key = str(exp_ms)
expiries.setdefault(exp_key, []).append(
@@ -343,6 +356,9 @@ def build_option_chain(
"exp_time": exp_ms,
"ask": ask,
"bid": bid,
+ "mark_px": mark,
+ "expiry_be_px": expiry_be,
+ "dist_expiry_be": idx_distance_to_be(idx, expiry_be),
"moneyness": mny,
"moneyness_label": option_moneyness_label(mny),
"ct_mult": _safe_float(meta.get("ctMult")) or 0.01,
@@ -383,6 +399,14 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]:
bid = round_option_px(mark, tick_sz, "sell")
uly = str(meta.get("uly") or "")
idx = fetch_index_price(ex, uly)
+ opt_type = meta.get("optType")
+ strike = _safe_float(meta.get("stk"))
+ expiry_be = expiry_breakeven_from_ask(
+ opt_type=str(opt_type or ""),
+ strike=strike,
+ ask_px=ask,
+ mark_px=mark,
+ )
return {
"ok": True,
"inst_id": inst_id,
@@ -391,11 +415,13 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]:
"bid": bid,
"mark": mark,
"index_px": idx,
+ "expiry_be_px": expiry_be,
+ "dist_expiry_be": idx_distance_to_be(idx, expiry_be),
"ct_mult": _safe_float(meta.get("ctMult")) or 0.01,
"min_sz": int(_safe_float(meta.get("minSz")) or 1),
"tick_sz": tick_sz,
- "strike": _safe_float(meta.get("stk")),
- "opt_type": meta.get("optType"),
+ "strike": strike,
+ "opt_type": opt_type,
"exp_time": meta.get("expTime"),
}
except Exception as e:
diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py
index ea5945e..2a528e3 100644
--- a/lib/options/options_pricing_lib.py
+++ b/lib/options/options_pricing_lib.py
@@ -134,6 +134,18 @@ def option_moneyness_label(moneyness: str) -> str:
return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "")
+def expiry_breakeven_from_ask(
+ *,
+ opt_type: str,
+ strike: float | None,
+ ask_px: float | None,
+ mark_px: float | None = None,
+) -> float | None:
+ """买入前预估到期平衡:权利金按卖一;无卖一时回退标记价。"""
+ prem = ask_px if ask_px is not None and ask_px > 0 else mark_px
+ return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem)
+
+
def expiry_breakeven_px(
*,
opt_type: str,
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index 649ba4a..1d9ab1e 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -7,7 +7,7 @@
期权下单
- 报价单位为每 1 ETH/BTC;1 张 = 0.01。链展示近 14 日到期,标注实值/虚值。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。
+ 报价单位为每 1 ETH/BTC;1 张 = 0.01。链展示近 14 日到期,标注实值/虚值;到期平衡按卖一预估(无卖一按标记价)。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。
@@ -26,11 +26,13 @@
合约 |
卖一 |
买一 |
+ 到期平衡 |
+ 距平衡 |
操作 |
- | 请选择到期日 |
+ | 请选择到期日 |
@@ -43,6 +45,8 @@
张数—
ETH/BTC 数量—
预估权利金—
+ 到期平衡—
+ 距平衡—
diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py
index 5add479..b830eb7 100644
--- a/tests/test_options_pricing.py
+++ b/tests/test_options_pricing.py
@@ -67,6 +67,14 @@ def test_option_moneyness():
assert option_moneyness_label("otm") == "虚值"
+def test_expiry_breakeven_from_ask():
+ from lib.options.options_pricing_lib import expiry_breakeven_from_ask
+
+ assert expiry_breakeven_from_ask(opt_type="C", strike=1760, ask_px=15.6) == 1775.6
+ assert expiry_breakeven_from_ask(opt_type="P", strike=1760, ask_px=15.6) == 1744.4
+ assert expiry_breakeven_from_ask(opt_type="C", strike=1760, ask_px=None, mark_px=14.2) == 1774.2
+
+
def test_calc_order_size_too_small():
r = calc_order_size(
quote_per_unit=2000.0,
|