"""期权按买盘平仓门控:可回收需 ≥ N×权利金,并持续持有一段时间后才允许平仓.""" from __future__ import annotations import os import threading import time from typing import Any def _env_float(key: str, default: float) -> float: try: return float(os.getenv(key, str(default))) except (TypeError, ValueError): return default # 可回收 ≥ 权利金 × 倍数,且该状态持续满 hold_seconds 才允许按买盘平仓 CLOSE_RECYCLE_MIN_MULT = _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0) CLOSE_RECYCLE_HOLD_SECONDS = _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0) _lock = threading.Lock() # inst_id -> {"ok_since": float|None, "recycle": float, "premium": float, "updated": float} _gates: dict[str, dict[str, Any]] = {} 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 clear_close_gate(inst_id: str | None = None) -> None: with _lock: if inst_id: _gates.pop(str(inst_id).strip(), None) else: _gates.clear() def mark_close_gate_passed(inst_id: str) -> None: """标记同仓已通过 2× 门控,续批平仓只验流动性.""" inst = (inst_id or "").strip() if not inst: return with _lock: st = _gates.get(inst) or {} st["passed"] = True st["updated"] = time.time() _gates[inst] = st def is_close_gate_passed(inst_id: str) -> bool: inst = (inst_id or "").strip() if not inst: return False with _lock: return bool((_gates.get(inst) or {}).get("passed")) def update_close_gate( inst_id: str, *, recycle_usdc: float | None, premium_paid: float | None, now: float | None = None, min_mult: float | None = None, hold_seconds: float | None = None, ) -> dict[str, Any]: """ 根据当前买盘可回收金额刷新门控. 条件不满足时重置计时;满足时从首次满足起累计持续时间. """ inst = (inst_id or "").strip() if not inst: return { "ok": False, "ready": False, "recycle_ok": False, "msg": "缺少合约", } ts = float(now if now is not None else time.time()) mult = float(min_mult if min_mult is not None else CLOSE_RECYCLE_MIN_MULT) hold = float(hold_seconds if hold_seconds is not None else CLOSE_RECYCLE_HOLD_SECONDS) if mult <= 0: mult = 2.0 if hold < 0: hold = 0.0 prem = _safe_float(premium_paid) recv = _safe_float(recycle_usdc) need = round(prem * mult, 4) if prem is not None and prem > 0 else None recycle_ok = bool( prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need ) with _lock: prev = _gates.get(inst) or {} ok_since = prev.get("ok_since") if recycle_ok: if ok_since is None: ok_since = ts else: ok_since = None held = (ts - float(ok_since)) if ok_since is not None else 0.0 ready = bool(recycle_ok and held + 1e-9 >= hold) prev_passed = bool(prev.get("passed")) passed = prev_passed or ready state = { "ok_since": ok_since, "recycle": recv, "premium": prem, "need": need, "updated": ts, "min_mult": mult, "hold_seconds": hold, "passed": passed, } _gates[inst] = state remain = max(0.0, hold - held) if recycle_ok and not ready else None if prem is None or prem <= 0: msg = "缺少权利金,无法校验平仓门控" elif recv is None: msg = "暂无有效买盘可回收金额" elif not recycle_ok: msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),目标平仓门控未过" elif not ready: msg = ( f"可回收已达×{mult:g}({recv:.4f}/{need:.4f})," f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过" ) else: msg = f"可回收已达×{mult:g}且持续≥{hold:.0f}s,目标触达后可按买一平仓" auto_blocked = not (ready or passed) return { "ok": True, "ready": ready, "passed": passed, "recycle_ok": recycle_ok, "recycle_usdc": recv, "premium_paid": prem, "need_recycle_usdc": need, "min_mult": mult, "hold_seconds": hold, "held_seconds": round(held, 1) if recycle_ok else 0.0, "remain_seconds": round(remain, 1) if remain is not None else None, "ok_since": ok_since, "msg": msg, "auto_close_blocked": auto_blocked, "close_gate_blocked": auto_blocked, } def check_close_gate( inst_id: str, *, recycle_usdc: float | None = None, premium_paid: float | None = None, refresh: bool = True, ) -> dict[str, Any]: """检查是否允许平仓;默认先用最新回收/权利金刷新.""" inst = (inst_id or "").strip() if refresh: if recycle_usdc is None or premium_paid is None: with _lock: prev = _gates.get(inst) or {} if recycle_usdc is None: recycle_usdc = prev.get("recycle") if premium_paid is None: premium_paid = prev.get("premium") return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) with _lock: prev = _gates.get(inst) if not prev: return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) return update_close_gate( inst, recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"), premium_paid=premium_paid if premium_paid is not None else prev.get("premium"), )