Estimate missing option ask from mark or intrinsic on chain list.

Deep ITM contracts often have no ask on the book; show mark-based estimates with a tilde and restore breakeven calculations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 17:50:25 +08:00
parent 25879dd007
commit 7f225c7407
4 changed files with 89 additions and 10 deletions
+68 -5
View File
@@ -115,6 +115,61 @@ def format_option_px(px: float, tick_sz: Any) -> str:
return f"{px:.{decimals}f}".rstrip("0").rstrip(".") or "0"
def _intrinsic_px_per_unit(opt_type: str, strike: float, index_px: float) -> float | None:
o = (opt_type or "").upper()
if o == "C" and index_px > strike:
return float(index_px) - float(strike)
if o == "P" and index_px < strike:
return float(strike) - float(index_px)
return None
def _resolve_chain_quote(
*,
ticker: dict[str, Any],
meta: dict[str, Any],
opt_type: str,
strike: float,
index_px: float,
) -> dict[str, Any]:
"""链列表报价:卖一缺失时用标记价/内在价值估算(深度实值常见无卖一)。"""
tick_sz = meta.get("tickSz")
ask = _safe_float(ticker.get("askPx"))
bid = _safe_float(ticker.get("bidPx"))
mark = _safe_float(ticker.get("markPx"))
ask_sz = _safe_float(ticker.get("askSz"))
bid_sz = _safe_float(ticker.get("bidSz"))
ask_estimated = False
if ask is None and mark is not None and mark > 0:
ask = round_option_px(mark, tick_sz, "buy")
ask_estimated = True
if ask is None:
intrinsic = _intrinsic_px_per_unit(opt_type, strike, index_px)
if intrinsic is not None and intrinsic > 0:
ask = round_option_px(intrinsic, tick_sz, "buy")
ask_estimated = True
if bid is None and mark is not None and mark > 0:
bid = round_option_px(mark, tick_sz, "sell")
if bid is None:
intrinsic = _intrinsic_px_per_unit(opt_type, strike, index_px)
if intrinsic is not None and intrinsic > 0:
bid = round_option_px(intrinsic, tick_sz, "sell")
if ask_estimated:
ask_sz = None
return {
"ask": ask,
"bid": bid,
"ask_sz": ask_sz,
"bid_sz": bid_sz,
"mark_px": mark,
"ask_estimated": ask_estimated,
}
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
@@ -344,11 +399,18 @@ def build_option_chain(
continue
inst_id = str(meta.get("instId") or "")
t = tickers.get(inst_id) or {}
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"))
q = _resolve_chain_quote(
ticker=t,
meta=meta,
opt_type=opt_type,
strike=strike,
index_px=idx,
)
ask = q["ask"]
bid = q["bid"]
mark = q["mark_px"]
ask_sz = q["ask_sz"]
bid_sz = q["bid_sz"]
if ask is None and bid is None and mark is None:
continue
expiry_be = expiry_breakeven_from_ask(
@@ -370,6 +432,7 @@ def build_option_chain(
"ask_sz": ask_sz,
"bid_sz": bid_sz,
"mark_px": mark,
"ask_estimated": q["ask_estimated"],
"expiry_be_px": expiry_be,
"dist_expiry_be": idx_distance_to_be(idx, expiry_be),
"moneyness": mny,