From e1c14977e6d27147486e1f3a3b611cfb4586049d Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 7 Jul 2026 08:58:31 +0800 Subject: [PATCH] fix: OKX option quote requires instFamily with instId Co-authored-by: Cursor --- lib/exchange/okx_options_lib.py | 110 +++++++++++++++++++++----------- tests/test_options_pricing.py | 7 +- 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 391413b..43d75f3 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -90,6 +90,35 @@ def _pos_side_from_position(pos: dict[str, Any] | None) -> str | None: return "net" +def inst_family_from_inst_id(inst_id: str) -> str | None: + """从 instId 解析 instFamily,如 ETH-USD_UM-260707-1790-C → ETH-USD_UM。""" + parts = (inst_id or "").strip().split("-") + if len(parts) < 4: + return None + return "-".join(parts[:-3]) + + +def fetch_option_instrument_meta(ex: ccxt.okx, inst_id: str) -> dict[str, Any] | None: + family = inst_family_from_inst_id(inst_id) + if not family: + return None + try: + rows = ex.public_get_public_instruments( + {"instType": "OPTION", "instFamily": family, "instId": inst_id} + ).get("data") or [] + if rows and isinstance(rows[0], dict): + return rows[0] + rows = ex.public_get_public_instruments( + {"instType": "OPTION", "instFamily": family} + ).get("data") or [] + for r in rows: + if isinstance(r, dict) and str(r.get("instId")) == inst_id: + return r + except Exception: + return None + return None + + def _extract_ccy_balance(balance: dict[str, Any], ccy: str) -> float | None: ccy = (ccy or "").upper() if not isinstance(balance, dict): @@ -244,45 +273,48 @@ def build_option_chain( def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]: - meta_rows = ex.public_get_public_instruments( - {"instType": "OPTION", "instId": inst_id} - ).get("data") or [] - if not meta_rows: - return {"ok": False, "msg": "合约不存在"} - meta = meta_rows[0] - t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or [] - t = t_rows[0] if t_rows else {} - ask = _safe_float(t.get("askPx")) - bid = _safe_float(t.get("bidPx")) - if ask is None or bid is None: - book_bid, book_ask = _fetch_book_bid_ask(ex, inst_id) - if ask is None: - ask = book_ask - if bid is None: - bid = book_bid - mark = _safe_float(t.get("markPx")) - tick_sz = meta.get("tickSz") - if ask is None and mark is not None: - ask = round_option_px(mark, tick_sz, "buy") - if bid is None and mark is not None: - bid = round_option_px(mark, tick_sz, "sell") - uly = str(meta.get("uly") or "") - idx = fetch_index_price(ex, uly) - return { - "ok": True, - "inst_id": inst_id, - "meta": meta, - "ask": ask, - "bid": bid, - "mark": mark, - "index_px": idx, - "ct_mult": _safe_float(meta.get("ctMult")) or 0.01, - "min_sz": int(_safe_float(meta.get("minSz")) or 1), - "tick_sz": meta.get("tickSz"), - "strike": _safe_float(meta.get("stk")), - "opt_type": meta.get("optType"), - "exp_time": meta.get("expTime"), - } + inst_id = (inst_id or "").strip() + if not inst_id: + return {"ok": False, "msg": "缺少 inst_id"} + try: + meta = fetch_option_instrument_meta(ex, inst_id) + if not meta: + return {"ok": False, "msg": "合约不存在"} + t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or [] + t = t_rows[0] if t_rows else {} + ask = _safe_float(t.get("askPx")) + bid = _safe_float(t.get("bidPx")) + if ask is None or bid is None: + book_bid, book_ask = _fetch_book_bid_ask(ex, inst_id) + if ask is None: + ask = book_ask + if bid is None: + bid = book_bid + mark = _safe_float(t.get("markPx")) + tick_sz = meta.get("tickSz") + if ask is None and mark is not None: + ask = round_option_px(mark, tick_sz, "buy") + if bid is None and mark is not None: + bid = round_option_px(mark, tick_sz, "sell") + uly = str(meta.get("uly") or "") + idx = fetch_index_price(ex, uly) + return { + "ok": True, + "inst_id": inst_id, + "meta": meta, + "ask": ask, + "bid": bid, + "mark": mark, + "index_px": idx, + "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"), + "exp_time": meta.get("expTime"), + } + except Exception as e: + return {"ok": False, "msg": str(e)} def place_option_limit_order( diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py index d110956..8ee4fa1 100644 --- a/tests/test_options_pricing.py +++ b/tests/test_options_pricing.py @@ -5,7 +5,12 @@ from lib.options.options_pricing_lib import ( sheets_from_eth_amount, total_premium, ) -from lib.exchange.okx_options_lib import format_option_px, round_option_px +from lib.exchange.okx_options_lib import format_option_px, inst_family_from_inst_id, round_option_px + + +def test_inst_family_from_inst_id(): + assert inst_family_from_inst_id("ETH-USD_UM-260707-1790-C") == "ETH-USD_UM" + assert inst_family_from_inst_id("BTC-USD-260925-60000-C") == "BTC-USD" def test_round_option_px():