From a7bec5e121bbc2bbfa218c7d520961c8b46a60ce Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 20 Aug 2026 16:21:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B8=81=E6=9C=AC=E4=BD=8D?= =?UTF-8?q?=E6=9C=9F=E6=9D=83=E6=AE=8B=E6=A1=A3=E5=88=A4=E5=AE=9A=EF=BC=9A?= =?UTF-8?q?=E5=86=85=E5=9C=A8=E4=BB=B7=E5=80=BC=E6=8C=89=E5=B8=81=E6=8A=A5?= =?UTF-8?q?=E4=BB=B7(S-K)/S=EF=BC=8C=E9=81=BF=E5=85=8D=E4=B8=8E=E7=BE=8E?= =?UTF-8?q?=E5=85=83=E7=82=B9=E5=B7=AE=E6=B7=B7=E6=AF=94=E8=AF=AF=E6=9D=80?= =?UTF-8?q?=E6=9C=89=E6=95=88=E4=B9=B0=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/exchange/okx_options_lib.py | 17 +++---- lib/options/options_close_exec_lib.py | 8 ++- lib/options/options_positions_lib.py | 2 + lib/options/options_pricing_lib.py | 71 ++++++++++++++++++++++++--- lib/options/options_target_lib.py | 8 ++- tests/test_options_pricing.py | 29 +++++++++++ 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index acd878d..4c313b7 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -13,6 +13,7 @@ import ccxt from lib.options.options_pricing_lib import ( expiry_breakeven_from_ask, idx_distance_to_be, + intrinsic_px_per_unit, is_shallow_itm, option_moneyness, option_moneyness_label, @@ -251,15 +252,6 @@ def tick_sz_and_ct_mult( return tick_sz, ct_mult or 0.01 -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], @@ -267,6 +259,7 @@ def _resolve_chain_quote( opt_type: str, strike: float, index_px: float, + inst_id: str | None = None, ) -> dict[str, Any]: """链列表报价:卖一缺失时用标记价/内在价值估算(深度实值常见无卖一).""" tick_sz = meta.get("tickSz") @@ -276,12 +269,13 @@ def _resolve_chain_quote( ask_sz = _safe_float(ticker.get("askSz")) bid_sz = _safe_float(ticker.get("bidSz")) ask_estimated = False + iid = (inst_id or str(meta.get("instId") or "")).strip() 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) + intrinsic = intrinsic_px_per_unit(opt_type, strike, index_px, inst_id=iid or None) if intrinsic is not None and intrinsic > 0: ask = round_option_px(intrinsic, tick_sz, "buy") ask_estimated = True @@ -289,7 +283,7 @@ def _resolve_chain_quote( 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) + intrinsic = intrinsic_px_per_unit(opt_type, strike, index_px, inst_id=iid or None) if intrinsic is not None and intrinsic > 0: bid = round_option_px(intrinsic, tick_sz, "sell") @@ -878,6 +872,7 @@ def build_option_chain( opt_type=opt_type, strike=strike, index_px=idx, + inst_id=inst_id, ) ask = q["ask"] bid = q["bid"] diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index 6939bd3..edd161f 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -58,7 +58,13 @@ def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = if strike is None: strike = ps idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px")) - return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx) + return close_ref_prices( + mark_px=mark, + opt_type=str(opt_type or ""), + strike=strike, + index_px=idx, + inst_id=inst_id, + ) def _avail_sheets(pos: dict[str, Any]) -> int: diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py index c442411..a3d6a1d 100644 --- a/lib/options/options_positions_lib.py +++ b/lib/options/options_positions_lib.py @@ -40,6 +40,8 @@ def attach_close_preview( row.get("opt_type") or row.get("optType"), _safe_float(row.get("strike") or row.get("stk")), _safe_float(row.get("idx_px") or row.get("idxPx")), + inst_id=inst_id, + margin_mode=row.get("margin_mode"), ) # 与实盘一致:只按买一估算本轮可平 preview = estimate_close_by_bids( diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py index 82551fe..d591f16 100644 --- a/lib/options/options_pricing_lib.py +++ b/lib/options/options_pricing_lib.py @@ -64,7 +64,46 @@ def _safe_px(v: Any) -> float | None: return x if x > 0 else None -def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: float | None) -> float | None: +def _quote_in_coin_from_context( + *, + quote_in_coin: bool | None = None, + inst_id: str | None = None, + margin_mode: str | None = None, +) -> bool: + """币本位(ETH-USD/BTC-USD)权利金按币报价;USDC(USD_UM)按美元点差.""" + if quote_in_coin is not None: + return bool(quote_in_coin) + if inst_id: + try: + from lib.options.options_margin_mode_lib import MODE_COIN, margin_mode_from_inst_id + + return margin_mode_from_inst_id(inst_id) == MODE_COIN + except Exception: + pass + if margin_mode is not None: + try: + from lib.options.options_margin_mode_lib import is_coin_margin_mode + + return is_coin_margin_mode(margin_mode) + except Exception: + return str(margin_mode).strip().lower() in ("coin", "coin_margin", "crypto") + return False + + +def intrinsic_px_per_unit( + opt_type: str | None, + strike: float | None, + index_px: float | None, + *, + quote_in_coin: bool | None = None, + inst_id: str | None = None, + margin_mode: str | None = None, +) -> float | None: + """ + 与盘口同单位的内在价值(每 1 标的). + - USDC / USD_UM: 美元点差 max(0, S−K) / max(0, K−S) + - 币本位 ETH-USD / BTC-USD: 币报价 max(0, S−K)/S / max(0, K−S)/S + """ o = (opt_type or "").strip().upper() if strike is None or index_px is None: return None @@ -74,10 +113,18 @@ def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: except (TypeError, ValueError): return None if o == "C" and idx > k: - return idx - k - if o == "P" and idx < k: - return k - idx - return None + points = idx - k + elif o == "P" and idx < k: + points = k - idx + else: + return None + if _quote_in_coin_from_context( + quote_in_coin=quote_in_coin, inst_id=inst_id, margin_mode=margin_mode + ): + if idx <= 0: + return None + return points / idx + return points def is_stub_bid_px( @@ -128,9 +175,19 @@ def close_ref_prices( opt_type: str | None = None, strike: float | None = None, index_px: float | None = None, + quote_in_coin: bool | None = None, + inst_id: str | None = None, + margin_mode: str | None = None, ) -> tuple[float | None, float | None]: - """返回 (mark_px, intrinsic_px) 供残档判断.""" - return _safe_px(mark_px), intrinsic_px_per_unit(opt_type, strike, index_px) + """返回 (mark_px, intrinsic_px) 供残档判断;intrinsic 与盘口同单位.""" + return _safe_px(mark_px), intrinsic_px_per_unit( + opt_type, + strike, + index_px, + quote_in_coin=quote_in_coin, + inst_id=inst_id, + margin_mode=margin_mode, + ) def filter_bids_for_close( diff --git a/lib/options/options_target_lib.py b/lib/options/options_target_lib.py index ecbb8c6..fe84344 100644 --- a/lib/options/options_target_lib.py +++ b/lib/options/options_target_lib.py @@ -33,7 +33,13 @@ def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = if strike is None: strike = ps idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px")) - return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx) + return close_ref_prices( + mark_px=mark, + opt_type=str(opt_type or ""), + strike=strike, + index_px=idx, + inst_id=inst_id, + ) def ensure_target_tables(conn: sqlite3.Connection) -> None: diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py index 28ff4eb..4b85102 100644 --- a/tests/test_options_pricing.py +++ b/tests/test_options_pricing.py @@ -267,6 +267,35 @@ def test_stub_bid_blocks_auto_close_estimate(): assert good["covered_sheets"] == 10 +def test_intrinsic_px_coin_vs_usdc_units(): + from lib.options.options_pricing_lib import intrinsic_px_per_unit, is_stub_bid_px + + # USDC / 默认:美元点差 + assert intrinsic_px_per_unit("C", 2250, 2274) == 24.0 + assert intrinsic_px_per_unit("C", 2250, 2274, margin_mode="usdc") == 24.0 + + # 币本位:与盘口同单位的币报价 (S−K)/S + coin_iv = intrinsic_px_per_unit("C", 2250, 2274, quote_in_coin=True) + assert coin_iv is not None + assert abs(coin_iv - 24.0 / 2274.0) < 1e-12 + assert abs( + intrinsic_px_per_unit("C", 2250, 2274, inst_id="ETH-USD-260822-2250-C") - 24.0 / 2274.0 + ) < 1e-12 + # USD_UM 仍为点差 + assert intrinsic_px_per_unit("C", 2250, 2274, inst_id="ETH-USD_UM-260822-2250-C") == 24.0 + + # 复现线上误杀:把点差当内在价值会把正常买一判残档 + wrong_stub, _ = is_stub_bid_px(0.023, mark_px=0.0241, intrinsic_px=23.58) + assert wrong_stub is True + # 币报价内在价值后,买一贴近标记价应有效 + ok_stub, _ = is_stub_bid_px(0.023, mark_px=0.0241, intrinsic_px=coin_iv) + assert ok_stub is False + + put_iv = intrinsic_px_per_unit("P", 2300, 2274, quote_in_coin=True) + assert put_iv is not None + assert abs(put_iv - 26.0 / 2274.0) < 1e-12 + + def test_expiry_breakeven_from_ask(): from lib.options.options_pricing_lib import expiry_breakeven_from_ask