78fd046fb6
Co-authored-by: Cursor <cursoragent@cursor.com>
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
"""OKX 合约 ID / 到期解析(交易所专属)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from ..expiry import expiry_ms_from_ymd
|
|
|
|
_DATE_RE = re.compile(r"^\d{6}$")
|
|
|
|
__all__ = [
|
|
"expiry_ms_from_ymd",
|
|
"parse_option_inst_id",
|
|
"rows_to_option_contracts",
|
|
"safe_float",
|
|
]
|
|
|
|
|
|
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 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)
|
|
exp_ms = None
|
|
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")
|
|
exp_ms = ms
|
|
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
|
|
if exp_ms is None:
|
|
exp_ms = expiry_ms_from_ymd(y)
|
|
ct = safe_float(row.get("ctMult"))
|
|
out.append(
|
|
{
|
|
"inst_id": inst_id,
|
|
"expiry_ymd": y,
|
|
"expiry_ms": int(exp_ms),
|
|
"strike": float(stk),
|
|
"side": opt,
|
|
"ct_mult": float(ct) if ct and ct > 0 else None,
|
|
}
|
|
)
|
|
return out
|