Show bid/ask liquidity as price per sheet in options chain.

Display OKX askSz and bidSz beside top-of-book prices in the chain table and order panel using price/sheets format.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 16:28:08 +08:00
parent 99f13817f8
commit 2cfc1a490f
6 changed files with 76 additions and 12 deletions
+4
View File
@@ -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;
}
+14 -3
View File
@@ -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 @@
"<td>" + c.strike + "</td>" +
"<td>" + moneynessBadge(c) + "</td>" +
"<td><code>" + c.inst_id + "</code></td>" +
"<td>" + fmt(c.ask, 4) + "</td>" +
"<td>" + fmt(c.bid, 4) + "</td>" +
"<td class=\"opt-px-sz\">" + fmtPxSz(c.ask, c.ask_sz) + "</td>" +
"<td class=\"opt-px-sz\">" + fmtPxSz(c.bid, c.bid_sz) + "</td>" +
"<td>" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "</td>" +
'<td class="' + distBeClass(c.dist_expiry_be) + '">' + fmtDist(c.dist_expiry_be) + "</td>" +
'<td class="opt-row-actions">' +
@@ -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" : "—";
+26 -5
View File
@@ -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,
+18
View File
@@ -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)
+5 -4
View File
@@ -7,7 +7,7 @@
<div class="options-dual-grid">
<div class="card options-order-card">
<h2>期权下单</h2>
<p class="muted options-hint">报价单位为每 1 ETH/BTC1 张 = 0.01。链展示近 <span id="opt-chain-dte">14</span> 日到期,标注实值/虚值;<strong>到期平衡</strong>按卖一预估(无卖一按标记价)。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。</p>
<p class="muted options-hint">报价单位为每 1 ETH/BTC1 张 = 0.01。卖一/买一列为 <strong>价格/张数</strong>链展示近 <span id="opt-chain-dte">14</span> 日到期,标注实值/虚值;<strong>到期平衡</strong>按卖一预估(无卖一按标记价)。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。</p>
<div class="form-row options-chain-toolbar">
<button type="button" class="btn-secondary opt-uly-btn active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary opt-uly-btn" data-uly="BTC">BTC</button>
@@ -24,8 +24,8 @@
<th>行权价</th>
<th>类型</th>
<th>合约</th>
<th>卖一</th>
<th>买一</th>
<th>卖一/张</th>
<th>买一/张</th>
<th>到期平衡</th>
<th>距平衡</th>
<th>操作</th>
@@ -41,7 +41,8 @@
<h3 class="opt-order-title">下单</h3>
<div id="opt-order-inst" class="options-order-inst"></div>
<div class="options-order-grid">
<div><span class="k">卖一(每1币)</span><span id="opt-order-ask" class="v"></span></div>
<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">预估权利金</span><span id="opt-order-premium" class="v"></span></div>
+9
View File
@@ -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