From 4e14c92385b3a122110a1b2408d96d58be9c3b1d Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 22:20:08 +0800 Subject: [PATCH] =?UTF-8?q?fix=E6=9C=9F=E6=9D=83=E9=93=BE=20Delta=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E4=BB=8E=20opt-summary=20=E6=8B=89=E5=8F=96?= =?UTF-8?q?=E3=80=82?= 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 | 38 ++++++++++++++++++++++++++---- tests/test_options_delta_fields.py | 19 ++++++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 2c18450..557e505 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -799,6 +799,37 @@ def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, return out +def fetch_option_summaries(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]: + """GET /api/v5/public/opt-summary — 含 delta / deltaBS 等希腊字母. + 市场 tickers 不含 Delta,期权链展示必须走本接口. + """ + out: dict[str, dict[str, Any]] = {} + try: + rows = ex.public_get_public_opt_summary({"instFamily": inst_family}).get("data") or [] + for r in rows: + if isinstance(r, dict) and r.get("instId"): + out[str(r["instId"])] = r + except Exception: + pass + return out + + +def option_delta_from_summary( + summary: dict[str, Any] | None = None, + *, + ticker: dict[str, Any] | None = None, +) -> float | None: + """优先 Black-Scholes 单位 Delta(deltaBS),其次 delta;兼容 ticker 偶发字段.""" + for src in (summary, ticker): + if not isinstance(src, dict): + continue + for key in ("deltaBS", "delta", "deltaBs", "delta_bs"): + v = _safe_float(src.get(key)) + if v is not None: + return v + return None + + def build_option_chain( ex: ccxt.okx, underlying: str, @@ -847,6 +878,7 @@ def build_option_chain( except Exception as e2: instruments_err = str(e2) or e2.__class__.__name__ tickers = fetch_option_tickers(ex, family) + summaries = fetch_option_summaries(ex, family) expiries: dict[str, list[dict[str, Any]]] = {} skipped_no_index = 0 for meta in instruments: @@ -872,6 +904,7 @@ def build_option_chain( continue inst_id = str(meta.get("instId") or "") t = tickers.get(inst_id) or {} + s = summaries.get(inst_id) or {} q = _resolve_chain_quote( ticker=t, meta=meta, @@ -884,10 +917,7 @@ def build_option_chain( mark = q["mark_px"] ask_sz = q["ask_sz"] bid_sz = q["bid_sz"] - # 行情 ticker:delta;兼容个别字段名 - delta = _safe_float(t.get("delta")) - if delta is None: - delta = _safe_float(t.get("deltaBS")) + delta = option_delta_from_summary(s, ticker=t) expiry_be = expiry_breakeven_from_ask( opt_type=opt_type, strike=strike, diff --git a/tests/test_options_delta_fields.py b/tests/test_options_delta_fields.py index 67e59ff..13f561f 100644 --- a/tests/test_options_delta_fields.py +++ b/tests/test_options_delta_fields.py @@ -1,10 +1,27 @@ # -*- coding: utf-8 -*- import unittest -from lib.exchange.okx_options_lib import format_position_row +from lib.exchange.okx_options_lib import format_position_row, option_delta_from_summary class OptionsDeltaFieldsTests(unittest.TestCase): + def test_delta_prefers_summary_delta_bs(self): + self.assertAlmostEqual( + option_delta_from_summary({"deltaBS": "0.55", "delta": "12.3"}), + 0.55, + places=4, + ) + + def test_delta_falls_back_to_ticker(self): + self.assertAlmostEqual( + option_delta_from_summary({}, ticker={"delta": "-0.22"}), + -0.22, + places=4, + ) + + def test_delta_none_when_missing(self): + self.assertIsNone(option_delta_from_summary({}, ticker={})) + def test_format_position_exposes_delta_bs(self): row = format_position_row( {