diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index ad239bb..502e56b 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -2588,6 +2588,10 @@ html[data-theme="light"] .settings-side-export-label { .options-page-wrap .options-strike-table code { font-size: 0.66rem; } +.opt-px-sz { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} .opt-be-dist-up { color: #5ee89a; } diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 16cf88b..36a23e1 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -149,6 +149,15 @@ "指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 实值=价内 · 虚值=价外"; } + function fmtPxSz(px, sz) { + if (px === null || px === undefined || Number.isNaN(Number(px))) return "—"; + const price = Number(px).toFixed(4).replace(/\.?0+$/, ""); + if (sz === null || sz === undefined || sz === "" || Number.isNaN(Number(sz))) return price; + const s = Number(sz); + const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s); + return price + "/" + size; + } + function fmtDist(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); @@ -197,8 +206,8 @@ "" + c.strike + "" + "" + moneynessBadge(c) + "" + "" + c.inst_id + "" + - "" + fmt(c.ask, 4) + "" + - "" + fmt(c.bid, 4) + "" + + "" + fmtPxSz(c.ask, c.ask_sz) + "" + + "" + fmtPxSz(c.bid, c.bid_sz) + "" + "" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "" + '' + fmtDist(c.dist_expiry_be) + "" + '' + @@ -229,7 +238,9 @@ function fillOrderPanel(d) { const sz = d.sizing || {}; document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || ""; - document.getElementById("opt-order-ask").textContent = fmt(d.ask, 4); + document.getElementById("opt-order-ask").textContent = fmtPxSz(d.ask, d.ask_sz); + const bidEl = document.getElementById("opt-order-bid"); + 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" : "—"; diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 9ecd860..31ce7f2 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -116,18 +116,27 @@ def format_option_px(px: float, tick_sz: Any) -> str: def _fetch_book_bid_ask(ex: ccxt.okx, inst_id: str) -> tuple[float | None, float | None]: + bid, ask, _, _ = _fetch_book_top(ex, inst_id) + return bid, ask + + +def _fetch_book_top( + ex: ccxt.okx, inst_id: str +) -> tuple[float | None, float | None, float | None, float | None]: try: rows = ex.public_get_market_books({"instId": inst_id, "sz": "1"}).get("data") or [] if not rows: - return None, None + return None, None, None, None row = rows[0] asks = row.get("asks") or [] bids = row.get("bids") or [] ask = _safe_float(asks[0][0]) if asks else None bid = _safe_float(bids[0][0]) if bids else None - return bid, ask + ask_sz = _safe_float(asks[0][1]) if asks and len(asks[0]) > 1 else None + bid_sz = _safe_float(bids[0][1]) if bids and len(bids[0]) > 1 else None + return bid, ask, bid_sz, ask_sz except Exception: - return None, None + return None, None, None, None def _pos_side_from_position(pos: dict[str, Any] | None) -> str | None: @@ -338,6 +347,8 @@ def build_option_chain( ask = _safe_float(t.get("askPx")) bid = _safe_float(t.get("bidPx")) mark = _safe_float(t.get("markPx")) + ask_sz = _safe_float(t.get("askSz")) + bid_sz = _safe_float(t.get("bidSz")) if ask is None and bid is None and mark is None: continue expiry_be = expiry_breakeven_from_ask( @@ -356,6 +367,8 @@ def build_option_chain( "exp_time": exp_ms, "ask": ask, "bid": bid, + "ask_sz": ask_sz, + "bid_sz": bid_sz, "mark_px": mark, "expiry_be_px": expiry_be, "dist_expiry_be": idx_distance_to_be(idx, expiry_be), @@ -385,12 +398,18 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]: t = t_rows[0] if t_rows else {} ask = _safe_float(t.get("askPx")) bid = _safe_float(t.get("bidPx")) - if ask is None or bid is None: - book_bid, book_ask = _fetch_book_bid_ask(ex, inst_id) + ask_sz = _safe_float(t.get("askSz")) + bid_sz = _safe_float(t.get("bidSz")) + if ask is None or bid is None or ask_sz is None or bid_sz is None: + book_bid, book_ask, book_bid_sz, book_ask_sz = _fetch_book_top(ex, inst_id) if ask is None: ask = book_ask if bid is None: bid = book_bid + if ask_sz is None: + ask_sz = book_ask_sz + if bid_sz is None: + bid_sz = book_bid_sz mark = _safe_float(t.get("markPx")) tick_sz = meta.get("tickSz") if ask is None and mark is not None: @@ -413,6 +432,8 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]: "meta": meta, "ask": ask, "bid": bid, + "ask_sz": ask_sz, + "bid_sz": bid_sz, "mark": mark, "index_px": idx, "expiry_be_px": expiry_be, diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py index 2a528e3..24a8028 100644 --- a/lib/options/options_pricing_lib.py +++ b/lib/options/options_pricing_lib.py @@ -28,6 +28,24 @@ def premium_per_sheet(quote_per_unit: float, ct_mult: float = 0.01) -> float: return float(quote_per_unit) * float(ct_mult) +def format_quote_liquidity(px: float | None, sz: float | None, *, px_decimals: int = 4) -> str | None: + """盘口展示:价格/张数,如 17.2/150。""" + if px is None: + return None + try: + price = f"{float(px):.{px_decimals}f}".rstrip("0").rstrip(".") + except (TypeError, ValueError): + return None + if sz is None: + return price + try: + s = float(sz) + size = str(int(s)) if abs(s - int(s)) < 1e-9 else str(s).rstrip("0").rstrip(".") + except (TypeError, ValueError): + return price + return f"{price}/{size}" + + def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.01) -> float: return float(quote_per_unit) * float(eth_amount) diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 1d9ab1e..ea4ba7d 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 兑换见「系统设置 → 期权设置」。

@@ -24,8 +24,8 @@ 行权价 类型 合约 - 卖一 - 买一 + 卖一/张 + 买一/张 到期平衡 距平衡 操作 @@ -41,7 +41,8 @@

下单

-
卖一(每1币)
+
卖一/张
+
买一/张
张数
ETH/BTC 数量
预估权利金
diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py index b830eb7..f3e509e 100644 --- a/tests/test_options_pricing.py +++ b/tests/test_options_pricing.py @@ -67,6 +67,15 @@ def test_option_moneyness(): assert option_moneyness_label("otm") == "虚值" +def test_format_quote_liquidity(): + from lib.options.options_pricing_lib import format_quote_liquidity + + assert format_quote_liquidity(17.2, 150) == "17.2/150" + assert format_quote_liquidity(817.6, 11) == "817.6/11" + assert format_quote_liquidity(15.6, None) == "15.6" + assert format_quote_liquidity(None, 10) is None + + def test_expiry_breakeven_from_ask(): from lib.options.options_pricing_lib import expiry_breakeven_from_ask