37f787e6a9
Support premium multiplier (coin default 1.05) or net-PnL USDT threshold with index conversion. Co-authored-by: Cursor <cursoragent@cursor.com>
326 lines
10 KiB
Python
326 lines
10 KiB
Python
"""期权按买盘平仓门控:权利金倍数或净盈亏(USDT)达标并持续 hold 秒后才允许目标平仓."""
|
|
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
|
|
|
|
|
|
def _close_gate_mode() -> str:
|
|
raw = (os.getenv("OKX_OPTIONS_CLOSE_GATE_MODE") or "premium").strip().lower()
|
|
return raw if raw in ("premium", "net_pnl") else "premium"
|
|
|
|
|
|
def _close_recycle_mult(premium_ccy: str) -> float:
|
|
ccy = (premium_ccy or "USDC").strip().upper()
|
|
if ccy in ("ETH", "BTC"):
|
|
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN", 1.05)
|
|
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0)
|
|
|
|
|
|
def _close_hold_seconds() -> float:
|
|
return _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0)
|
|
|
|
|
|
def _close_net_pnl_min_usdt() -> float:
|
|
return _env_float("OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT", 0.0)
|
|
|
|
|
|
# 兼容旧引用
|
|
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 -> gate state
|
|
_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:
|
|
"""标记同仓已通过门控,续批平仓只验流动性."""
|
|
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 _fmt_gate_amt(v: float, *, ccy: str) -> str:
|
|
unit = (ccy or "USDC").strip().upper() or "USDC"
|
|
if unit in ("ETH", "BTC"):
|
|
txt = f"{float(v):.8f}".rstrip("0").rstrip(".")
|
|
return txt or "0"
|
|
return f"{float(v):.4f}"
|
|
|
|
|
|
def _fmt_usdt(v: float) -> str:
|
|
sign = "+" if v >= 0 else ""
|
|
return f"{sign}{float(v):.2f}"
|
|
|
|
|
|
def _net_pnl_usdt(
|
|
*,
|
|
recv: float | None,
|
|
prem: float | None,
|
|
premium_ccy: str,
|
|
index_px: float | None,
|
|
) -> float | None:
|
|
if recv is None or prem is None:
|
|
return None
|
|
net = float(recv) - float(prem)
|
|
ccy = (premium_ccy or "USDC").strip().upper() or "USDC"
|
|
if ccy in ("ETH", "BTC"):
|
|
idx = _safe_float(index_px)
|
|
if idx is None or idx <= 0:
|
|
return None
|
|
return net * float(idx)
|
|
return net
|
|
|
|
|
|
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,
|
|
premium_ccy: str | None = None,
|
|
index_px: float | None = None,
|
|
gate_mode: str | None = None,
|
|
net_pnl_min_usdt: float | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
根据当前买盘可回收金额刷新门控.
|
|
premium: 可回收 ≥ 权利金×倍数(币本位默认×1.05,USDC 默认×2)
|
|
net_pnl: 净盈亏(买一可回收−权利金)折 USDT ≥ 阈值(默认 0U)
|
|
条件不满足时重置计时;满足时从首次满足起累计持续时间.
|
|
"""
|
|
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())
|
|
mode = (gate_mode or _close_gate_mode()).strip().lower()
|
|
if mode not in ("premium", "net_pnl"):
|
|
mode = "premium"
|
|
hold = float(hold_seconds if hold_seconds is not None else _close_hold_seconds())
|
|
if hold < 0:
|
|
hold = 0.0
|
|
|
|
with _lock:
|
|
prev = _gates.get(inst) or {}
|
|
prev_ccy = prev.get("premium_ccy")
|
|
ccy = (premium_ccy or prev_ccy or "USDC").strip().upper() or "USDC"
|
|
if ccy not in ("ETH", "BTC", "USDC"):
|
|
ccy = "USDC"
|
|
need_decimals = 8 if ccy in ("ETH", "BTC") else 4
|
|
|
|
prem = _safe_float(premium_paid)
|
|
recv = _safe_float(recycle_usdc)
|
|
mult = float(min_mult if min_mult is not None else _close_recycle_mult(ccy))
|
|
if mult <= 0:
|
|
mult = _close_recycle_mult(ccy)
|
|
min_pnl_u = float(net_pnl_min_usdt if net_pnl_min_usdt is not None else _close_net_pnl_min_usdt())
|
|
idx = _safe_float(index_px)
|
|
if idx is None:
|
|
idx = _safe_float(prev.get("index_px"))
|
|
|
|
need = round(prem * mult, need_decimals) if prem is not None and prem > 0 else None
|
|
net_pnl_u = _net_pnl_usdt(recv=recv, prem=prem, premium_ccy=ccy, index_px=idx)
|
|
|
|
if mode == "net_pnl":
|
|
if prem is None or prem <= 0:
|
|
condition_ok = False
|
|
elif recv is None:
|
|
condition_ok = False
|
|
elif net_pnl_u is None:
|
|
condition_ok = False
|
|
else:
|
|
condition_ok = bool(net_pnl_u + 1e-9 >= min_pnl_u)
|
|
else:
|
|
condition_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 {}
|
|
prev_mode = prev.get("gate_mode")
|
|
ok_since = prev.get("ok_since")
|
|
if prev_mode != mode:
|
|
ok_since = None
|
|
if condition_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(condition_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,
|
|
"premium_ccy": ccy,
|
|
"gate_mode": mode,
|
|
"net_pnl_usdt": net_pnl_u,
|
|
"net_pnl_min_usdt": min_pnl_u,
|
|
"index_px": idx,
|
|
}
|
|
_gates[inst] = state
|
|
|
|
remain = max(0.0, hold - held) if condition_ok and not ready else None
|
|
if mode == "net_pnl":
|
|
if prem is None or prem <= 0:
|
|
msg = "缺少权利金,无法校验平仓门控"
|
|
elif recv is None:
|
|
msg = "暂无有效买盘可回收金额"
|
|
elif net_pnl_u is None:
|
|
msg = "缺少指数价,无法将币本位净盈亏折算为 USDT"
|
|
elif not condition_ok:
|
|
msg = (
|
|
f"净盈亏 {_fmt_usdt(net_pnl_u)} U(估) < {min_pnl_u:g} U,"
|
|
f"目标平仓门控未过"
|
|
)
|
|
elif not ready:
|
|
msg = (
|
|
f"净盈亏 {_fmt_usdt(net_pnl_u)} U(估) ≥ {min_pnl_u:g} U,"
|
|
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
|
|
)
|
|
else:
|
|
msg = f"净盈亏 ≥ {min_pnl_u:g} U 且持续≥{hold:.0f}s,目标触达后可按买一平仓"
|
|
elif prem is None or prem <= 0:
|
|
msg = "缺少权利金,无法校验平仓门控"
|
|
elif recv is None:
|
|
msg = "暂无有效买盘可回收金额"
|
|
elif not condition_ok:
|
|
msg = (
|
|
f"可回收 {_fmt_gate_amt(recv, ccy=ccy)} {ccy} < 权利金×{mult:g}"
|
|
f"({_fmt_gate_amt(need, ccy=ccy)}),目标平仓门控未过"
|
|
)
|
|
elif not ready:
|
|
msg = (
|
|
f"可回收已达×{mult:g}({_fmt_gate_amt(recv, ccy=ccy)}/{_fmt_gate_amt(need, ccy=ccy)} {ccy}),"
|
|
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": condition_ok,
|
|
"recycle_usdc": recv,
|
|
"premium_paid": prem,
|
|
"need_recycle_usdc": need,
|
|
"premium_ccy": ccy,
|
|
"min_mult": mult,
|
|
"hold_seconds": hold,
|
|
"held_seconds": round(held, 1) if condition_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,
|
|
"gate_mode": mode,
|
|
"net_pnl_usdt": net_pnl_u,
|
|
"net_pnl_min_usdt": min_pnl_u,
|
|
"index_px": idx,
|
|
}
|
|
|
|
|
|
def check_close_gate(
|
|
inst_id: str,
|
|
*,
|
|
recycle_usdc: float | None = None,
|
|
premium_paid: float | None = None,
|
|
premium_ccy: str | None = None,
|
|
index_px: 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 or premium_ccy is None or index_px 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")
|
|
if premium_ccy is None:
|
|
premium_ccy = prev.get("premium_ccy")
|
|
if index_px is None:
|
|
index_px = prev.get("index_px")
|
|
return update_close_gate(
|
|
inst,
|
|
recycle_usdc=recycle_usdc,
|
|
premium_paid=premium_paid,
|
|
premium_ccy=premium_ccy,
|
|
index_px=index_px,
|
|
)
|
|
with _lock:
|
|
prev = _gates.get(inst)
|
|
if not prev:
|
|
return update_close_gate(
|
|
inst,
|
|
recycle_usdc=recycle_usdc,
|
|
premium_paid=premium_paid,
|
|
premium_ccy=premium_ccy,
|
|
index_px=index_px,
|
|
)
|
|
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"),
|
|
premium_ccy=premium_ccy if premium_ccy is not None else prev.get("premium_ccy"),
|
|
index_px=index_px if index_px is not None else prev.get("index_px"),
|
|
)
|