"""OKX 单笔期权本位模式与币本位 USDT 预算.""" from __future__ import annotations import os from typing import Any MODE_USDC = "usdc" MODE_COIN = "coin" def _env_bool(name: str, default: bool = False) -> bool: v = (os.getenv(name) or "").strip().lower() if not v: return default return v in ("1", "true", "yes", "on", "y") def _env_float(name: str, default: float) -> float: try: return float(os.getenv(name) or default) except (TypeError, ValueError): return float(default) def normalize_options_margin_mode(raw: Any = None) -> str: """返回 usdc | coin;未配置时默认币本位.""" if raw is None: raw = os.getenv("OKX_OPTIONS_MARGIN_MODE") v = str(raw or MODE_COIN).strip().lower() if v in ("usdc", "usdc_margin", "usd_margin", "u本位", "u"): return MODE_USDC if v in ("coin", "coin_margin", "crypto", "crypto_margin", "币本位"): return MODE_COIN # 空串或未知值:默认币本位 if not v: return MODE_COIN return MODE_COIN def is_coin_margin_mode(raw: Any = None) -> bool: return normalize_options_margin_mode(raw) == MODE_COIN def inst_family_for_underlying(underlying: str, *, margin_mode: str | None = None) -> str: u = (underlying or "ETH").strip().upper() or "ETH" mode = normalize_options_margin_mode(margin_mode) if mode == MODE_COIN: return f"{u}-USD" return f"{u}-USD_UM" def margin_mode_from_inst_id(inst_id: str) -> str: inst = (inst_id or "").strip().upper() if not inst: return normalize_options_margin_mode() if "_UM" in inst: return MODE_USDC # ETH-USD-260701-2500-C / BTC-USD-... if "-USD-" in inst and "_UM" not in inst: return MODE_COIN return normalize_options_margin_mode() def premium_ccy_for_mode(margin_mode: str, underlying: str = "ETH") -> str: if normalize_options_margin_mode(margin_mode) == MODE_COIN: return (underlying or "ETH").strip().upper() or "ETH" return "USDC" def spot_quote_inst_id(underlying: str) -> str: """现货市价买卖: ETH-USDT / BTC-USDT.""" u = (underlying or "ETH").strip().upper() or "ETH" return f"{u}-USDT" def compute_coin_budget_usdt( trading_usdt: float, *, compound: bool | None = None, buffer: float | None = None, fixed_budget_usdt: float | None = None, max_enabled: bool | None = None, max_usdt: float | None = None, ) -> dict[str, Any]: """ 币本位单笔 USDT 预算. 复利开: trading_usdt × buffer; 复利关: fixed × buffer. 上限开: min(..., max_usdt). """ bal = max(0.0, float(trading_usdt or 0)) use_compound = _env_bool("OKX_OPTIONS_COIN_COMPOUND", True) if compound is None else bool(compound) buf = float(buffer) if buffer is not None else _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95) if buf <= 0: buf = 0.95 fixed = ( float(fixed_budget_usdt) if fixed_budget_usdt is not None else _env_float("OKX_OPTIONS_COIN_BUDGET_USDT", 10.0) ) if use_compound: raw = bal * buf source = "compound" else: raw = max(0.0, fixed) * buf source = "fixed" capped = False max_on = ( _env_bool("OKX_OPTIONS_COIN_MAX_USDT_ENABLED", False) if max_enabled is None else bool(max_enabled) ) max_n = ( float(max_usdt) if max_usdt is not None else _env_float("OKX_OPTIONS_COIN_MAX_USDT", 50.0) ) budget = raw if max_on and max_n > 0 and budget > max_n: budget = max_n capped = True return { "ok": budget > 0, "budget_usdt": round(budget, 8), "raw_usdt": round(raw, 8), "trading_usdt": round(bal, 8), "buffer": buf, "compound": use_compound, "source": source, "max_enabled": max_on, "max_usdt": max_n if max_on else None, "capped_by_max": capped, "msg": "" if budget > 0 else "交易账户 USDT 不足,无法计算币本位预算", } def normalize_coin_spot_buy_buffer(raw: Any = None) -> float: """ 现货买入相对权利金的倍数缓冲. env OKX_OPTIONS_COIN_SPOT_BUY_BUFFER 默认 1.10(=多买 10%). 也可写 0.10 表示 +10%. """ if raw is None: v = _env_float("OKX_OPTIONS_COIN_SPOT_BUY_BUFFER", 1.10) else: try: v = float(raw) except (TypeError, ValueError): v = 1.10 if v <= 0: return 1.10 if v < 1.0: return 1.0 + v return v def plan_coin_open_by_budget( *, quote_per_unit: float, ct_mult: float, min_sz: int, budget_usdt: float, index_px: float, ask_sz: float | None = None, spot_buy_buffer: float | None = None, target_sheets: int | None = None, ) -> dict[str, Any]: """ 先按预算/卖一估最大可开张数,再按「权利金 × 现货缓冲」算应买现货 USDT. 不全额把预算换成币. """ import math from lib.exchange.okx_options_lib import cap_option_buy_sheets_to_ask_depth ask = float(quote_per_unit or 0) mult = float(ct_mult or 0.01) need = max(1, int(min_sz or 1)) budget = max(0.0, float(budget_usdt or 0)) idx = float(index_px or 0) buf = normalize_coin_spot_buy_buffer(spot_buy_buffer) if ask <= 0 or mult <= 0: return {"ok": False, "msg": "卖一价无效", "sheets": 0, "buy_usdt": 0.0} if idx <= 0: return {"ok": False, "msg": "缺少指数价,无法估算买币 USDT", "sheets": 0, "buy_usdt": 0.0} if budget <= 0: return {"ok": False, "msg": "USDT 预算无效", "sheets": 0, "buy_usdt": 0.0} per_sheet_coin = ask * mult # 每张开仓需买的币(含缓冲)及其约合 USDT per_sheet_buy_coin = per_sheet_coin * buf per_sheet_usdt = per_sheet_buy_coin * idx if per_sheet_usdt <= 0: return {"ok": False, "msg": "无法计算单张买币成本", "sheets": 0, "buy_usdt": 0.0} max_by_budget = int(math.floor((budget / per_sheet_usdt) + 1e-12)) if target_sheets is not None: try: want = int(target_sheets) except (TypeError, ValueError): want = 0 if want < need: return { "ok": False, "msg": f"指定张数无效(需≥{need})", "sheets": 0, "buy_usdt": 0.0, "max_by_budget": max_by_budget, } sheets = min(want, max_by_budget) if sheets < want: return { "ok": False, "msg": ( f"预算约可开 {max_by_budget} 张(含现货缓冲×{buf:g})," f"不足指定 {want} 张" ), "sheets": 0, "buy_usdt": 0.0, "max_by_budget": max_by_budget, "spot_buy_buffer": buf, } else: sheets = max_by_budget capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=need) ask_depth_capped = False if capped is None: return { "ok": False, "msg": cap_msg or "卖一深度不足", "sheets": 0, "buy_usdt": 0.0, "spot_buy_buffer": buf, } if int(capped) < sheets: sheets = int(capped) ask_depth_capped = True if sheets < need: return { "ok": False, "msg": ( f"预算不足,无法买入 {need} 张" f"(单张约需 {per_sheet_usdt:.4f} USDT,含现货缓冲×{buf:g})" ), "sheets": sheets, "buy_usdt": 0.0, "per_sheet_usdt": round(per_sheet_usdt, 8), "spot_buy_buffer": buf, "max_by_budget": max_by_budget, } premium_coin = sheets * per_sheet_coin buy_coin = premium_coin * buf buy_usdt = min(budget, buy_coin * idx) # 再保险:向下对齐,避免浮点导致略超预算 buy_usdt = min(budget, round(buy_usdt, 8)) out = { "ok": True, "msg": "" if not ask_depth_capped else (cap_msg or f"已按卖一深度限制为 {sheets} 张"), "sheets": sheets, "eth_amount": round(sheets * mult, 8), "coin_premium": round(premium_coin, 8), "total_premium": round(premium_coin, 8), "per_sheet_coin": per_sheet_coin, "buy_coin": round(buy_coin, 8), "buy_usdt": round(buy_usdt, 8), "budget_usdt": round(budget, 8), "spot_buy_buffer": buf, "index_px": idx, "max_by_budget": max_by_budget, "ask_depth_capped": ask_depth_capped, "est_note": ( f"按最大可开 {sheets} 张×卖一权利金×现货缓冲{buf:g}估买币;" f"不全额兑换预算" ), } if target_sheets is not None: out["est_note"] = ( f"指定 {sheets} 张×卖一权利金×现货缓冲{buf:g}估买币;不全额兑换" ) out["target_sheets"] = int(target_sheets) return out def calc_sheets_from_coin_balance( *, quote_per_unit: float, ct_mult: float, min_sz: int, coin_available: float, ) -> dict[str, Any]: """用可用标的币尽量开满(权利金以币计).""" import math ask = float(quote_per_unit or 0) mult = float(ct_mult or 0.01) need = max(1, int(min_sz or 1)) coin = max(0.0, float(coin_available or 0)) # 留一点手续费/精度缓冲,避免算满张后下单 51008 coin_eff = coin * 0.97 if ask <= 0 or mult <= 0: return {"ok": False, "msg": "卖一价无效", "sheets": 0, "coin_premium": 0.0} per_sheet = ask * mult if per_sheet <= 0: return {"ok": False, "msg": "无法计算单张权利金(币)", "sheets": 0, "coin_premium": 0.0} sheets = int(math.floor((coin_eff / per_sheet) + 1e-12)) if sheets < need: return { "ok": False, "msg": f"可用币不足,无法买入 {need} 张(单张约 {per_sheet:.8g} 币,可用 {coin:g})", "sheets": sheets, "coin_premium": round(sheets * per_sheet, 8), "per_sheet_coin": per_sheet, } prem = sheets * per_sheet return { "ok": True, "msg": "", "sheets": sheets, "coin_premium": round(prem, 8), "per_sheet_coin": per_sheet, "eth_amount": round(sheets * mult, 8), }