"""OKX 合约 ID / 到期解析(交易所专属)。""" from __future__ import annotations import re from datetime import datetime, timezone from typing import Any _DATE_RE = re.compile(r"^\d{6}$") def safe_float(v: Any) -> float | None: if v is None or v == "": return None try: return float(v) except (TypeError, ValueError): return None def parse_option_inst_id(inst_id: str) -> tuple[str | None, float | None, str | None]: """ETH-USD_UM-YYMMDD-STRIKE-C → (YYMMDD, strike, C|P).""" parts = (inst_id or "").strip().split("-") if len(parts) < 5: return None, None, None ymd = parts[-3] strike = safe_float(parts[-2]) opt = parts[-1].upper() if not _DATE_RE.fullmatch(ymd) or strike is None or opt not in ("C", "P"): return None, None, None return ymd, strike, opt def expiry_ms_from_ymd(ymd: str) -> int: """OKX 期权到期:当日 08:00 UTC = 上海 16:00。""" yy, mm, dd = int(ymd[0:2]), int(ymd[2:4]), int(ymd[4:6]) dt = datetime(2000 + yy, mm, dd, 8, 0, 0, tzinfo=timezone.utc) return int(dt.timestamp() * 1000) def rows_to_option_contracts(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: """ 归一化为策略层可用的中性结构: {inst_id, expiry_ymd, strike, side, ct_mult} """ out: list[dict[str, Any]] = [] for row in rows: if not isinstance(row, dict): continue state = str(row.get("state") or "live").lower() if state and state != "live": continue inst_id = str(row.get("instId") or "") y, stk, opt = parse_option_inst_id(inst_id) if y is None or stk is None or opt is None: exp = safe_float(row.get("expTime")) if exp: ms = int(exp) if exp > 10_000_000_000 else int(exp * 1000) y = datetime.fromtimestamp(ms / 1000, tz=timezone.utc).strftime("%y%m%d") stk = safe_float(row.get("stk")) opt_raw = str(row.get("optType") or "").upper() opt = opt_raw if opt_raw in ("C", "P") else None if not inst_id or not y or stk is None or opt not in ("C", "P"): continue ct = safe_float(row.get("ctMult")) out.append( { "inst_id": inst_id, "expiry_ymd": y, "expiry_ms": expiry_ms_from_ymd(y), "strike": float(stk), "side": opt, "ct_mult": float(ct) if ct and ct > 0 else None, } ) return out