"""币安期权 / 永续符号解析 → 中性合约行。""" from __future__ import annotations import re from typing import Any from ..expiry import expiry_ms_from_ymd, ymd_from_expiry_ms _OPT_RE = re.compile( r"^(?P[A-Z0-9]+)-(?P\d{6})-(?P\d+(?:\.\d+)?)-(?P[CP])$", re.IGNORECASE, ) 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_symbol(symbol: str) -> tuple[str | None, float | None, str | None]: """ETH-250726-1860-C → (YYMMDD, strike, C|P).""" m = _OPT_RE.match((symbol or "").strip()) if not m: return None, None, None ymd = m.group("ymd") strike = safe_float(m.group("strike")) side = m.group("side").upper() return ymd, strike, side def is_option_symbol(symbol: str) -> bool: y, s, o = parse_option_symbol(symbol) return y is not None and s is not None and o in ("C", "P") def rows_to_option_contracts(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: """ 归一化: {inst_id, expiry_ymd, expiry_ms, strike, side, ct_mult} """ out: list[dict[str, Any]] = [] for row in rows: if not isinstance(row, dict): continue status = str(row.get("status") or "TRADING").upper() if status and status not in ("TRADING", "LIVE", ""): continue inst_id = str(row.get("symbol") or row.get("inst_id") or "") y, stk, opt = parse_option_symbol(inst_id) exp_ms = None raw_exp = row.get("expiryDate") or row.get("expiration") or row.get("expiry_ms") if raw_exp is not None: try: exp_ms = int(float(raw_exp)) if exp_ms < 10_000_000_000: # seconds exp_ms *= 1000 except (TypeError, ValueError): exp_ms = None if y is None and exp_ms is not None: y = ymd_from_expiry_ms(exp_ms) if stk is None: stk = safe_float(row.get("strikePrice") or row.get("strike")) if opt is None: side_raw = str(row.get("side") or row.get("optionSide") or "").upper() if side_raw in ("CALL", "C"): opt = "C" elif side_raw in ("PUT", "P"): opt = "P" if not inst_id or not y or stk is None or opt not in ("C", "P"): continue if exp_ms is None: try: exp_ms = expiry_ms_from_ymd(y) except ValueError: continue unit = safe_float(row.get("unit") or row.get("ct_mult")) out.append( { "inst_id": inst_id, "expiry_ymd": y, "expiry_ms": int(exp_ms), "strike": float(stk), "side": opt, "ct_mult": float(unit) if unit and unit > 0 else 1.0, } ) return out