78fd046fb6
Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
2.9 KiB
Python
98 lines
2.9 KiB
Python
"""币安期权 / 永续符号解析 → 中性合约行。"""
|
|
|
|
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<under>[A-Z0-9]+)-(?P<ymd>\d{6})-(?P<strike>\d+(?:\.\d+)?)-(?P<side>[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
|