"""OKX USDⓈ 期权:张数与权利金计算.""" from __future__ import annotations import math from typing import Any def ct_mult_from_meta(meta: dict[str, Any] | None) -> float: if not meta: return 0.01 try: return float(meta.get("ctMult") or 0.01) except (TypeError, ValueError): return 0.01 def min_sz_from_meta(meta: dict[str, Any] | None) -> int: if not meta: return 1 try: return max(1, int(float(meta.get("minSz") or 1))) except (TypeError, ValueError): return 1 def premium_per_sheet(quote_per_unit: float, ct_mult: float = 0.01) -> float: """报价为每 1 ETH/BTC;每张权利金 = 报价 × ctMult.""" return float(quote_per_unit) * float(ct_mult) def format_quote_liquidity(px: float | None, sz: float | None, *, px_decimals: int = 4) -> str | None: """盘口展示:价格/张数,如 17.2/150.""" if px is None: return None try: price = f"{float(px):.{px_decimals}f}".rstrip("0").rstrip(".") except (TypeError, ValueError): return None if sz is None: return price try: s = float(sz) size = str(int(s)) if abs(s - int(s)) < 1e-9 else str(s).rstrip("0").rstrip(".") except (TypeError, ValueError): return price return f"{price}/{size}" def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.01) -> float: return float(quote_per_unit) * float(eth_amount) def sheets_from_eth_amount(eth_amount: float, ct_mult: float = 0.01) -> int: if eth_amount <= 0 or ct_mult <= 0: return 0 return int(math.floor(eth_amount / ct_mult + 1e-12)) def eth_amount_from_sheets(sheets: int, ct_mult: float = 0.01) -> float: return round(int(sheets) * float(ct_mult), 8) def calc_order_size( *, quote_per_unit: float, ct_mult: float, min_sz: int, budget_usdc: float | None = None, budget_buffer: float = 0.95, eth_amount: float | None = None, sheets: int | None = None, budget_cap: float | None = None, ) -> dict[str, Any]: """ 返回 sheets, eth_amount, total_premium. mode: budget_full / eth_amount / sheets. """ if quote_per_unit <= 0: return {"ok": False, "msg": "卖一价无效", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} if sheets is not None and int(sheets) > 0: sheets = int(sheets) elif eth_amount is not None and eth_amount > 0: sheets = sheets_from_eth_amount(eth_amount, ct_mult) elif budget_usdc is not None and budget_usdc > 0: eff = float(budget_usdc) * float(budget_buffer) per_sheet = premium_per_sheet(quote_per_unit, ct_mult) if per_sheet <= 0: return {"ok": False, "msg": "无法计算单张权利金", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} sheets = int(math.floor(eff / per_sheet)) else: return {"ok": False, "msg": "请指定预算,币数量或张数", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} if sheets < min_sz: per = premium_per_sheet(quote_per_unit, ct_mult) return { "ok": False, "msg": f"预算不足,无法买入 {min_sz} 张(单张约 {per:.4f} USDC)", "sheets": sheets, "eth_amount": eth_amount_from_sheets(sheets, ct_mult), "total_premium": total_premium(quote_per_unit, eth_amount_from_sheets(sheets, ct_mult)), } eth = eth_amount_from_sheets(sheets, ct_mult) prem = total_premium(quote_per_unit, eth) if budget_cap is not None and prem > float(budget_cap) + 1e-9: return { "ok": False, "msg": f"权利金 {prem:.4f} 超过单笔上限 {budget_cap} USDC", "sheets": sheets, "eth_amount": eth, "total_premium": prem, } return {"ok": True, "msg": "", "sheets": sheets, "eth_amount": eth, "total_premium": prem} def is_shallow_itm( *, opt_type: str, strike: float, index_px: float, max_dist_usd: float, ) -> bool: o = (opt_type or "").upper() if o == "C": if strike >= index_px: return False return (index_px - strike) <= max_dist_usd if o == "P": if strike <= index_px: return False return (strike - index_px) <= max_dist_usd return False def option_moneyness(*, opt_type: str, strike: float, index_px: float) -> str: """返回 itm / otm / atm.""" o = (opt_type or "").upper() if strike is None or index_px is None or index_px <= 0: return "unknown" atm_band = max(index_px * 0.002, 2.0) if abs(strike - index_px) <= atm_band: return "atm" if o == "C": return "itm" if strike < index_px else "otm" if o == "P": return "itm" if strike > index_px else "otm" return "unknown" def option_moneyness_label(moneyness: str) -> str: return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "") def expiry_breakeven_from_ask( *, opt_type: str, strike: float | None, ask_px: float | None, mark_px: float | None = None, ) -> float | None: """买入前预估到期平衡:权利金按卖一;无卖一时回退标记价.""" prem = ask_px if ask_px is not None and ask_px > 0 else mark_px return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem) def expiry_breakeven_px( *, opt_type: str, strike: float | None, avg_px: float | None, be_px_api: float | None = None, ) -> float | None: """到期平衡点:持有至到期时标的指数盈亏为 0 的价格.优先 OKX bePx.""" if be_px_api is not None and be_px_api > 0: return round(float(be_px_api), 2) if strike is None or avg_px is None: return None o = (opt_type or "").upper() if o == "C": return round(strike + avg_px, 2) if o == "P": return round(strike - avg_px, 2) return None def close_breakeven_idx( *, opt_type: str, idx_px: float | None, mark_px: float | None, avg_px: float | None, delta_pa: float | None = None, pos: float = 0, ct_mult: float = 0.01, ) -> float | None: """ 平掉回本:标的指数达到该价位时,按标记价平仓近似盈亏为 0. 优先用 deltaPA 线性外推,否则用时间价值近似(适合短期轻度实值). """ if idx_px is None or mark_px is None or avg_px is None: return None eth_amt = abs(float(pos)) * float(ct_mult) if eth_amt > 1e-12 and delta_pa is not None and abs(float(delta_pa)) > 1e-12: slope = float(delta_pa) / eth_amt return round(float(idx_px) + (float(avg_px) - float(mark_px)) / slope, 2) o = (opt_type or "").upper() if o == "C": return round(float(idx_px) + float(avg_px) - float(mark_px), 2) if o == "P": return round(float(idx_px) + float(mark_px) - float(avg_px), 2) return None def idx_distance_to_be(idx_px: float | None, be_px: float | None) -> float | None: """指数距平衡点(正=指数需上涨才到平衡点).""" if idx_px is None or be_px is None: return None return round(float(be_px) - float(idx_px), 2) def format_options_breakeven_line( *, expiry_be_px: float | None, close_be_px: float | None, idx_px: float | None = None, ) -> str: """持仓摘要行:到期平衡 / 平掉回本.""" parts: list[str] = [] if expiry_be_px is not None: parts.append(f"到期平衡{expiry_be_px:.0f}") if close_be_px is not None: parts.append(f"平掉回本{close_be_px:.0f}") if idx_px is not None and parts: return " ".join(parts) + f"(指数{idx_px:.0f})" return " ".join(parts) def estimate_expiry_profit_at_index( *, opt_type: str, strike: float | None, target_idx: float | None, entry_px: float | None, eth_amount: float | None, ) -> float | None: """到期测算:目标指数价下按内在价值减权利金(每 1 币报价 × 币量).""" if strike is None or target_idx is None or entry_px is None or eth_amount is None: return None if eth_amount <= 0: return None o = (opt_type or "").upper() if o == "C": intrinsic = max(0.0, float(target_idx) - float(strike)) elif o == "P": intrinsic = max(0.0, float(strike) - float(target_idx)) else: return None return round((intrinsic - float(entry_px)) * float(eth_amount), 4) def equivalent_contract_leverage( *, index_px: float | None, eth_amount: float | None, total_premium: float | None, ) -> float | None: """名义价值 / 权利金,近似相当于永续合约杠杆倍数(测算用).""" if index_px is None or eth_amount is None or total_premium is None: return None if eth_amount <= 0 or total_premium <= 0: return None return round(float(index_px) * float(eth_amount) / float(total_premium), 1)