"""对冲计划虚实值选约与校验. 永期(perp_options):期权腿仅允许实值或平值(禁虚值). 期期(options_options):两腿仅允许平值或虚值(禁实值). """ from __future__ import annotations import os from typing import Any, Optional def _env_float(name: str, default: float) -> float: try: return float(os.getenv(name) or default) except (TypeError, ValueError): return float(default) def itm_max_dist_usd() -> float: """过深实值上限(USD).优先对冲专用,否则回退期权页.""" raw = (os.getenv("HEDGE_PLAN_ITM_MAX_DIST_USD") or "").strip() if raw: try: return max(0.0, float(raw)) except ValueError: pass return max(0.0, _env_float("OKX_OPTIONS_ITM_MAX_DIST_USD", 30.0)) def min_option_hours() -> float: return max(0.0, _env_float("HEDGE_PLAN_MIN_OPTION_HOURS", 8.0)) def min_option_leverage() -> float: """指数/卖一 最低杠杆门槛;0=不启用.""" return max(0.0, _env_float("HEDGE_PLAN_MIN_OPTION_LEVERAGE", 0.0)) def _sf(v: Any) -> Optional[float]: if v is None or v == "": return None try: return float(v) except (TypeError, ValueError): return None def normalize_opt_type(opt_type: Any, inst_id: str = "") -> str: o = str(opt_type or "").strip().upper() if o in ("C", "CALL"): return "C" if o in ("P", "PUT"): return "P" inst = str(inst_id or "").upper() if inst.endswith("-C") or inst.endswith("-CALL"): return "C" if inst.endswith("-P") or inst.endswith("-PUT"): return "P" return "" def classify_moneyness(*, opt_type: str, strike: float, index_px: float) -> str: """itm / atm / otm / unknown.与 options_pricing_lib.option_moneyness 同口径.""" from lib.options.options_pricing_lib import option_moneyness return option_moneyness(opt_type=opt_type, strike=strike, index_px=index_px) def is_itm_or_atm(*, opt_type: str, strike: float, index_px: float) -> bool: """Call: K<=S(+atm 带);Put: K>=S.用 classify 结果含 atm/itm.""" m = classify_moneyness(opt_type=opt_type, strike=strike, index_px=index_px) if m in ("itm", "atm"): return True # 几何兜底(与 eth_hedge_sim 一致),避免 atm 带边界漏判 o = normalize_opt_type(opt_type) k = float(strike) s = float(index_px) if o == "C": return k <= s + 1e-9 if o == "P": return k >= s - 1e-9 return False def is_atm_or_otm(*, opt_type: str, strike: float, index_px: float) -> bool: m = classify_moneyness(opt_type=opt_type, strike=strike, index_px=index_px) if m in ("atm", "otm"): return True o = normalize_opt_type(opt_type) k = float(strike) s = float(index_px) if o == "C": return k >= s - 1e-9 # 平值带内或虚值 if o == "P": return k <= s + 1e-9 return False def itm_depth_usd(*, opt_type: str, strike: float, index_px: float) -> float: o = normalize_opt_type(opt_type) k = float(strike) s = float(index_px) if o == "C" and k < s: return s - k if o == "P" and k > s: return k - s return 0.0 def parse_strike_from_inst(inst_id: str) -> Optional[float]: """从 OKX 合约名解析行权价: ETH-USD-260731-1800-P.""" parts = str(inst_id or "").strip().upper().split("-") if len(parts) < 5: return None return _sf(parts[-2]) def pick_itm_or_atm_contract( contracts: list[dict[str, Any]], *, opt_type: str, index_px: float, itm_max_dist: Optional[float] = None, ) -> Optional[dict[str, Any]]: """在合约列表中选距标的最近的实值/平值腿.""" want = normalize_opt_type(opt_type) if not want or index_px <= 0: return None max_dist = itm_max_dist if itm_max_dist is not None else itm_max_dist_usd() cands: list[tuple[float, float, dict[str, Any]]] = [] for c in contracts or []: if normalize_opt_type(c.get("opt_type"), str(c.get("inst_id") or "")) != want: continue k = _sf(c.get("strike")) if k is None: continue if not is_itm_or_atm(opt_type=want, strike=k, index_px=index_px): continue depth = itm_depth_usd(opt_type=want, strike=k, index_px=index_px) if max_dist > 0 and depth > max_dist: continue cands.append((abs(k - index_px), k, c)) if not cands: return None cands.sort(key=lambda x: (x[0], x[1])) return cands[0][2] def pick_atm_or_otm_contract( contracts: list[dict[str, Any]], *, opt_type: str, index_px: float, prefer: str = "atm", ) -> Optional[dict[str, Any]]: """选平值或虚值腿.prefer=atm 取距标的最近;prefer=otm 取最近虚值(不含实值).""" want = normalize_opt_type(opt_type) if not want or index_px <= 0: return None prefer_l = (prefer or "atm").strip().lower() cands: list[tuple[float, float, dict[str, Any]]] = [] for c in contracts or []: if normalize_opt_type(c.get("opt_type"), str(c.get("inst_id") or "")) != want: continue k = _sf(c.get("strike")) if k is None: continue if not is_atm_or_otm(opt_type=want, strike=k, index_px=index_px): continue m = classify_moneyness(opt_type=want, strike=k, index_px=index_px) if prefer_l == "otm" and m != "otm": continue if prefer_l == "atm" and m == "otm": # 仍可入选,但排序靠后(先 atm) cands.append((1_000_000 + abs(k - index_px), k, c)) else: cands.append((abs(k - index_px), k, c)) if not cands: return None cands.sort(key=lambda x: (x[0], x[1])) return cands[0][2] def recommend_oo_legs( contracts: list[dict[str, Any]], *, index_px: float, template: str = "atm_straddle", ) -> Optional[tuple[dict[str, Any], dict[str, Any]]]: """期期推荐两腿.atm_straddle=最近平值 Call+Put;double_otm=最近虚值 Call+Put.""" tpl = (template or "atm_straddle").strip().lower() prefer = "otm" if tpl in ("double_otm", "otm_otm", "otm") else "atm" call = pick_atm_or_otm_contract( contracts, opt_type="C", index_px=index_px, prefer=prefer ) put = pick_atm_or_otm_contract( contracts, opt_type="P", index_px=index_px, prefer=prefer ) if not call or not put: return None if str(call.get("inst_id") or "") == str(put.get("inst_id") or ""): return None return call, put def validate_po_option_moneyness( *, opt_type: str, strike: Any, index_px: Any, ask: Any = None, hours_to_expiry: Any = None, ) -> Optional[str]: """永期保险腿校验;返回错误文案或 None.""" o = normalize_opt_type(opt_type) k = _sf(strike) s = _sf(index_px) if o not in ("C", "P"): return "期权类型无效" if k is None or s is None or s <= 0: return "行权价或指数无效,无法校验虚实值" if not is_itm_or_atm(opt_type=o, strike=k, index_px=s): return "永期保险腿须为实值或平值,不可选虚值" max_dist = itm_max_dist_usd() depth = itm_depth_usd(opt_type=o, strike=k, index_px=s) if max_dist > 0 and depth > max_dist: return f"实值过深(距现价 {depth:.1f}U > {max_dist:.0f}U),请换更接近平值的档" min_h = min_option_hours() h = _sf(hours_to_expiry) if min_h > 0 and h is not None and h < min_h: return f"剩余到期约 {h:.1f}h,低于最低 {min_h:.0f}h" min_lev = min_option_leverage() a = _sf(ask) if min_lev > 0 and a is not None and a > 0: lev = s / a if lev < min_lev: return f"期权杠杆 S/ask≈{lev:.0f} 低于门槛 {min_lev:.0f}" return None def validate_oo_leg_moneyness( *, opt_type: str, strike: Any, index_px: Any, role: str = "腿", ) -> Optional[str]: o = normalize_opt_type(opt_type) k = _sf(strike) s = _sf(index_px) if o not in ("C", "P"): return f"{role}期权类型无效" if k is None or s is None or s <= 0: return f"{role}行权价或指数无效,无法校验虚实值" m = classify_moneyness(opt_type=o, strike=k, index_px=s) if m == "itm": return f"{role}须为平值或虚值,不可选实值" if not is_atm_or_otm(opt_type=o, strike=k, index_px=s): return f"{role}须为平值或虚值" return None def validate_oo_legs_moneyness( leg_a: dict[str, Any], leg_b: dict[str, Any], *, index_px: Any, ) -> Optional[str]: err = validate_oo_leg_moneyness( opt_type=leg_a.get("opt_type"), strike=leg_a.get("strike"), index_px=index_px, role="腿A", ) if err: return err err = validate_oo_leg_moneyness( opt_type=leg_b.get("opt_type"), strike=leg_b.get("strike"), index_px=index_px, role="腿B", ) if err: return err return None