Add pre-buy expiry breakeven columns to options chain table.

Show estimated expiry balance and distance from index in the chain list and order panel using ask price before opening a position.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 16:21:22 +08:00
parent 3570a6900e
commit 99f13817f8
6 changed files with 96 additions and 8 deletions
+12
View File
@@ -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 {
+28 -2
View File
@@ -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 = '<tr><td colspan="6" class="muted">请选择到期日</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="muted">请选择到期日</td></tr>';
state.selectedInst = null;
return;
}
@@ -168,7 +183,7 @@
return c.opt_type === state.optType;
});
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">该到期日暂无报价</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="muted">该到期日暂无报价</td></tr>';
state.selectedInst = null;
return;
}
@@ -184,6 +199,8 @@
"<td><code>" + c.inst_id + "</code></td>" +
"<td>" + fmt(c.ask, 4) + "</td>" +
"<td>" + fmt(c.bid, 4) + "</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">' +
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button> ' +
'<button type="button" class="btn-primary opt-buy-btn" data-inst="' + c.inst_id + '">买入</button>' +
@@ -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 || "报价失败";
+30 -4
View File
@@ -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:
+12
View File
@@ -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,
+6 -2
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> 日到期,标注实值/虚值。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。</p>
<p class="muted options-hint">报价单位为每 1 ETH/BTC1 张 = 0.01。链展示近 <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>
@@ -26,11 +26,13 @@
<th>合约</th>
<th>卖一</th>
<th>买一</th>
<th>到期平衡</th>
<th>距平衡</th>
<th>操作</th>
</tr>
</thead>
<tbody id="opt-strike-tbody">
<tr><td colspan="6" class="muted">请选择到期日</td></tr>
<tr><td colspan="8" class="muted">请选择到期日</td></tr>
</tbody>
</table>
</div>
@@ -43,6 +45,8 @@
<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>
<div><span class="k">到期平衡</span><span id="opt-order-expiry-be" class="v"></span></div>
<div><span class="k">距平衡</span><span id="opt-order-dist-be" class="v"></span></div>
</div>
<div class="form-row options-order-mode-row">
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
+8
View File
@@ -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,