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:
@@ -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 {
|
||||
|
||||
@@ -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 || "报价失败";
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="options-dual-grid">
|
||||
<div class="card options-order-card">
|
||||
<h2>期权下单</h2>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 0.01。链展示近 <span id="opt-chain-dte">14</span> 日到期,标注实值/虚值。资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」。</p>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user