目标平仓门控改为 USDT 口径:env 可配权利金倍数与净盈亏阈值,币本位默认×1.05
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -143,6 +143,13 @@ OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0
|
|||||||
OKX_OPTIONS_POLL_SECONDS=15
|
OKX_OPTIONS_POLL_SECONDS=15
|
||||||
OKX_OPTIONS_TD_MODE=isolated
|
OKX_OPTIONS_TD_MODE=isolated
|
||||||
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
|
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
|
||||||
|
# 目标平仓门控(目标触达后自动平才校验;比较口径均为 USDT 估值)
|
||||||
|
# OKX_OPTIONS_CLOSE_GATE_MODE=premium
|
||||||
|
# OKX_OPTIONS_CLOSE_RECYCLE_MULT=
|
||||||
|
# OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN=1.05
|
||||||
|
# OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC=2
|
||||||
|
# OKX_OPTIONS_CLOSE_NET_PNL_MIN_U=0
|
||||||
|
# OKX_OPTIONS_CLOSE_HOLD_SECONDS=120
|
||||||
# 对冲买期权等成交超时(秒);超时撤未成交部分,未完全成交则开仓失败
|
# 对冲买期权等成交超时(秒);超时撤未成交部分,未完全成交则开仓失败
|
||||||
OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12
|
OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12
|
||||||
|
|
||||||
|
|||||||
@@ -1101,7 +1101,7 @@
|
|||||||
const gate = preview.close_gate || {};
|
const gate = preview.close_gate || {};
|
||||||
// 2× 只是目标平仓门控,本身不会自动平;手动买一平不拦截
|
// 2× 只是目标平仓门控,本身不会自动平;手动买一平不拦截
|
||||||
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
||||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "可回收需≥2×权利金并持续2分钟");
|
return "目标门控: " + (preview.close_gate_msg || gate.msg || "门控未过(见 env 目标平仓门控)");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
}
|
}
|
||||||
const gate = preview.close_gate || {};
|
const gate = preview.close_gate || {};
|
||||||
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
||||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "可回收需≥2×权利金并持续2分钟");
|
return "目标门控: " + (preview.close_gate_msg || gate.msg || "门控未过(见 env 目标平仓门控)");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+30
@@ -215,6 +215,36 @@ _OPTIONS_SECTION: dict[str, Any] = {
|
|||||||
"链上仅显示有卖一",
|
"链上仅显示有卖一",
|
||||||
"默认 true;开启后隐藏无卖一深度或深度不足1张的合约(含标记价估算行)",
|
"默认 true;开启后隐藏无卖一深度或深度不足1张的合约(含标记价估算行)",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
||||||
|
"目标平仓门控模式",
|
||||||
|
"premium=可回收(U)≥权利金(U)×倍数;net_pnl=净盈亏(U)大于阈值。币本位按指数换算为U",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||||
|
"门控权利金倍数(全局)",
|
||||||
|
"可选;填写则覆盖下方分本位默认值",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||||
|
"门控权利金倍数(币本位)",
|
||||||
|
"默认 1.05;premium 模式下 recyclable(U)≥premium(U)×本值",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC",
|
||||||
|
"门控权利金倍数(USDC)",
|
||||||
|
"默认 2;premium 模式下 recyclable(U)≥premium(U)×本值",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_NET_PNL_MIN_U",
|
||||||
|
"门控净盈亏下限(U)",
|
||||||
|
"net_pnl 模式;净盈亏(估)须大于本值,如 0 或 1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OKX_OPTIONS_CLOSE_HOLD_SECONDS",
|
||||||
|
"门控持续秒数",
|
||||||
|
"达标后须持续本秒数才通过,默认 120",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def close_option_by_bid1(
|
|||||||
- 限价 = 校验通过时锁定的买一价
|
- 限价 = 校验通过时锁定的买一价
|
||||||
- 永不市价
|
- 永不市价
|
||||||
- 始终校验有效流动性(残档买一禁止)
|
- 始终校验有效流动性(残档买一禁止)
|
||||||
- require_recycle_gate=True 时:首次还需可回收≥2×权利金并持续 hold 秒;
|
- require_recycle_gate=True 时:首次还需目标门控(权利金×倍数或净盈亏,U 口径)并持续 hold 秒;
|
||||||
一旦通过后对同仓续批只验流动性
|
一旦通过后对同仓续批只验流动性
|
||||||
"""
|
"""
|
||||||
from lib.exchange.okx_options_lib import (
|
from lib.exchange.okx_options_lib import (
|
||||||
@@ -145,6 +145,8 @@ def close_option_by_bid1(
|
|||||||
premium_paid = _open_premium_paid(cfg, inst_id)
|
premium_paid = _open_premium_paid(cfg, inst_id)
|
||||||
if premium_paid is None:
|
if premium_paid is None:
|
||||||
premium_paid = _safe_float(pos.get("premium_paid"))
|
premium_paid = _safe_float(pos.get("premium_paid"))
|
||||||
|
premium_ccy = str(q.get("premium_ccy") or pos.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
||||||
|
index_px = _safe_float(pos.get("idxPx")) or _safe_float(q.get("index_px"))
|
||||||
|
|
||||||
# 已有未成交卖平单:等成交,不撤不重挂
|
# 已有未成交卖平单:等成交,不撤不重挂
|
||||||
try:
|
try:
|
||||||
@@ -194,7 +196,13 @@ def close_option_by_bid1(
|
|||||||
)
|
)
|
||||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||||
# 不撤他人挂单:仅拒绝本轮下单
|
# 不撤他人挂单:仅拒绝本轮下单
|
||||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
update_close_gate(
|
||||||
|
inst_id,
|
||||||
|
recycle_usdc=None,
|
||||||
|
premium_paid=premium_paid,
|
||||||
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止平仓",
|
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止平仓",
|
||||||
@@ -208,7 +216,13 @@ def close_option_by_bid1(
|
|||||||
bid_px = _safe_float(q.get("bid"))
|
bid_px = _safe_float(q.get("bid"))
|
||||||
stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||||
if stub or bid_px is None or bid_px <= 0:
|
if stub or bid_px is None or bid_px <= 0:
|
||||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
update_close_gate(
|
||||||
|
inst_id,
|
||||||
|
recycle_usdc=None,
|
||||||
|
premium_paid=premium_paid,
|
||||||
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": stub_reason or "暂无买一,无法限价平仓",
|
"msg": stub_reason or "暂无买一,无法限价平仓",
|
||||||
@@ -231,7 +245,13 @@ def close_option_by_bid1(
|
|||||||
|
|
||||||
stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||||
if stub_lv:
|
if stub_lv:
|
||||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
update_close_gate(
|
||||||
|
inst_id,
|
||||||
|
recycle_usdc=None,
|
||||||
|
premium_paid=premium_paid,
|
||||||
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": stub_lv_reason or "暂无有效买盘,禁止平仓",
|
"msg": stub_lv_reason or "暂无有效买盘,禁止平仓",
|
||||||
@@ -245,6 +265,8 @@ def close_option_by_bid1(
|
|||||||
inst_id,
|
inst_id,
|
||||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||||
premium_paid=premium_paid,
|
premium_paid=premium_paid,
|
||||||
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
)
|
)
|
||||||
if require_recycle_gate and not is_close_gate_passed(inst_id) and not gate.get("ready"):
|
if require_recycle_gate and not is_close_gate_passed(inst_id) and not gate.get("ready"):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""期权按买盘平仓门控:可回收需 ≥ N×权利金,并持续持有一段时间后才允许平仓."""
|
"""期权按买盘平仓门控:可回收/净盈亏换算为 USDT 后校验,并持续 hold 秒才允许平仓."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -14,10 +14,44 @@ def _env_float(key: str, default: float) -> float:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
# 可回收 ≥ 权利金 × 倍数,且该状态持续满 hold_seconds 才允许按买盘平仓
|
def _env_optional_float(key: str) -> float | None:
|
||||||
CLOSE_RECYCLE_MIN_MULT = _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0)
|
raw = os.getenv(key)
|
||||||
|
if raw is None or str(raw).strip() == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
CLOSE_RECYCLE_HOLD_SECONDS = _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0)
|
CLOSE_RECYCLE_HOLD_SECONDS = _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0)
|
||||||
|
|
||||||
|
|
||||||
|
def close_net_pnl_min_u() -> float:
|
||||||
|
return _env_float("OKX_OPTIONS_CLOSE_NET_PNL_MIN_U", 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def close_gate_mode() -> str:
|
||||||
|
m = (os.getenv("OKX_OPTIONS_CLOSE_GATE_MODE") or "premium").strip().lower()
|
||||||
|
return m if m in ("premium", "net_pnl") else "premium"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_close_recycle_mult(premium_ccy: str | None, min_mult: float | None = None) -> float:
|
||||||
|
if min_mult is not None:
|
||||||
|
m = float(min_mult)
|
||||||
|
return m if m > 0 else 1.05
|
||||||
|
global_mult = _env_optional_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT")
|
||||||
|
if global_mult is not None and global_mult > 0:
|
||||||
|
return global_mult
|
||||||
|
ccy = (premium_ccy or "USDC").strip().upper() or "USDC"
|
||||||
|
if ccy in ("ETH", "BTC"):
|
||||||
|
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN", 1.05)
|
||||||
|
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC", 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
# 兼容旧引用
|
||||||
|
CLOSE_RECYCLE_MIN_MULT = resolve_close_recycle_mult("USDC")
|
||||||
|
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
# inst_id -> {"ok_since": float|None, "recycle": float, "premium": float, "updated": float}
|
# inst_id -> {"ok_since": float|None, "recycle": float, "premium": float, "updated": float}
|
||||||
_gates: dict[str, dict[str, Any]] = {}
|
_gates: dict[str, dict[str, Any]] = {}
|
||||||
@@ -32,6 +66,39 @@ def _safe_float(v: Any) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_premium_ccy(premium_ccy: str | None) -> str:
|
||||||
|
ccy = (premium_ccy or "USDC").strip().upper() or "USDC"
|
||||||
|
if ccy not in ("ETH", "BTC", "USDC"):
|
||||||
|
ccy = "USDC"
|
||||||
|
return ccy
|
||||||
|
|
||||||
|
|
||||||
|
def _to_usdt(amount: float | None, ccy: str, index_px: float | None) -> float | None:
|
||||||
|
if amount is None:
|
||||||
|
return None
|
||||||
|
unit = _normalize_premium_ccy(ccy)
|
||||||
|
if unit in ("ETH", "BTC"):
|
||||||
|
idx = _safe_float(index_px)
|
||||||
|
if idx is None or idx <= 0:
|
||||||
|
return None
|
||||||
|
return float(amount) * float(idx)
|
||||||
|
return float(amount)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_gate_amt(v: float, *, ccy: str) -> str:
|
||||||
|
unit = _normalize_premium_ccy(ccy)
|
||||||
|
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 | None) -> str:
|
||||||
|
if v is None:
|
||||||
|
return "—"
|
||||||
|
return f"{float(v):.2f}"
|
||||||
|
|
||||||
|
|
||||||
def clear_close_gate(inst_id: str | None = None) -> None:
|
def clear_close_gate(inst_id: str | None = None) -> None:
|
||||||
with _lock:
|
with _lock:
|
||||||
if inst_id:
|
if inst_id:
|
||||||
@@ -41,7 +108,7 @@ def clear_close_gate(inst_id: str | None = None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def mark_close_gate_passed(inst_id: str) -> None:
|
def mark_close_gate_passed(inst_id: str) -> None:
|
||||||
"""标记同仓已通过 2× 门控,续批平仓只验流动性."""
|
"""标记同仓已通过门控,续批平仓只验流动性."""
|
||||||
inst = (inst_id or "").strip()
|
inst = (inst_id or "").strip()
|
||||||
if not inst:
|
if not inst:
|
||||||
return
|
return
|
||||||
@@ -60,14 +127,6 @@ def is_close_gate_passed(inst_id: str) -> bool:
|
|||||||
return bool((_gates.get(inst) or {}).get("passed"))
|
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 update_close_gate(
|
def update_close_gate(
|
||||||
inst_id: str,
|
inst_id: str,
|
||||||
*,
|
*,
|
||||||
@@ -77,10 +136,12 @@ def update_close_gate(
|
|||||||
min_mult: float | None = None,
|
min_mult: float | None = None,
|
||||||
hold_seconds: float | None = None,
|
hold_seconds: float | None = None,
|
||||||
premium_ccy: str | None = None,
|
premium_ccy: str | None = None,
|
||||||
|
index_px: float | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
根据当前买盘可回收金额刷新门控.
|
根据当前买盘可回收金额刷新门控(比较口径均为 USDT 估值).
|
||||||
条件不满足时重置计时;满足时从首次满足起累计持续时间.
|
premium 模式:可回收(U) ≥ 权利金(U) × 倍数
|
||||||
|
net_pnl 模式:净盈亏(U) > OKX_OPTIONS_CLOSE_NET_PNL_MIN_U
|
||||||
"""
|
"""
|
||||||
inst = (inst_id or "").strip()
|
inst = (inst_id or "").strip()
|
||||||
if not inst:
|
if not inst:
|
||||||
@@ -91,25 +152,37 @@ def update_close_gate(
|
|||||||
"msg": "缺少合约",
|
"msg": "缺少合约",
|
||||||
}
|
}
|
||||||
ts = float(now if now is not None else time.time())
|
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)
|
mode = close_gate_mode()
|
||||||
hold = float(hold_seconds if hold_seconds is not None else CLOSE_RECYCLE_HOLD_SECONDS)
|
hold = float(hold_seconds if hold_seconds is not None else CLOSE_RECYCLE_HOLD_SECONDS)
|
||||||
if mult <= 0:
|
|
||||||
mult = 2.0
|
|
||||||
if hold < 0:
|
if hold < 0:
|
||||||
hold = 0.0
|
hold = 0.0
|
||||||
|
|
||||||
with _lock:
|
with _lock:
|
||||||
prev_ccy = (_gates.get(inst) or {}).get("premium_ccy")
|
prev_ccy = (_gates.get(inst) or {}).get("premium_ccy")
|
||||||
ccy = (premium_ccy or prev_ccy or "USDC").strip().upper() or "USDC"
|
ccy = _normalize_premium_ccy(premium_ccy or prev_ccy)
|
||||||
if ccy not in ("ETH", "BTC", "USDC"):
|
mult = resolve_close_recycle_mult(ccy, min_mult)
|
||||||
ccy = "USDC"
|
min_pnl_u = close_net_pnl_min_u()
|
||||||
need_decimals = 8 if ccy in ("ETH", "BTC") else 4
|
|
||||||
|
|
||||||
prem = _safe_float(premium_paid)
|
prem = _safe_float(premium_paid)
|
||||||
recv = _safe_float(recycle_usdc)
|
recv = _safe_float(recycle_usdc)
|
||||||
need = round(prem * mult, need_decimals) if prem is not None and prem > 0 else None
|
recv_u = _to_usdt(recv, ccy, index_px)
|
||||||
|
prem_u = _to_usdt(prem, ccy, index_px)
|
||||||
|
net_u = round(recv_u - prem_u, 4) if recv_u is not None and prem_u is not None else None
|
||||||
|
need_u = round(prem_u * mult, 4) if prem_u is not None and prem_u > 0 and mode == "premium" else None
|
||||||
|
|
||||||
|
missing_index = ccy in ("ETH", "BTC") and (index_px is None or _safe_float(index_px) is None or _safe_float(index_px) <= 0)
|
||||||
|
|
||||||
|
if missing_index and prem is not None and prem > 0 and recv is not None:
|
||||||
|
recycle_ok = False
|
||||||
|
elif mode == "net_pnl":
|
||||||
|
recycle_ok = bool(net_u is not None and net_u > min_pnl_u + 1e-9)
|
||||||
|
else:
|
||||||
recycle_ok = bool(
|
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
|
prem_u is not None
|
||||||
|
and prem_u > 0
|
||||||
|
and recv_u is not None
|
||||||
|
and need_u is not None
|
||||||
|
and recv_u + 1e-9 >= need_u
|
||||||
)
|
)
|
||||||
|
|
||||||
with _lock:
|
with _lock:
|
||||||
@@ -128,12 +201,19 @@ def update_close_gate(
|
|||||||
"ok_since": ok_since,
|
"ok_since": ok_since,
|
||||||
"recycle": recv,
|
"recycle": recv,
|
||||||
"premium": prem,
|
"premium": prem,
|
||||||
"need": need,
|
"need": need_u,
|
||||||
"updated": ts,
|
"updated": ts,
|
||||||
"min_mult": mult,
|
"min_mult": mult,
|
||||||
"hold_seconds": hold,
|
"hold_seconds": hold,
|
||||||
"passed": passed,
|
"passed": passed,
|
||||||
"premium_ccy": ccy,
|
"premium_ccy": ccy,
|
||||||
|
"gate_mode": mode,
|
||||||
|
"index_px": _safe_float(index_px),
|
||||||
|
"recycle_usdt": recv_u,
|
||||||
|
"premium_usdt": prem_u,
|
||||||
|
"need_recycle_usdt": need_u,
|
||||||
|
"net_pnl_usdt": net_u,
|
||||||
|
"net_pnl_min_u": min_pnl_u if mode == "net_pnl" else None,
|
||||||
}
|
}
|
||||||
_gates[inst] = state
|
_gates[inst] = state
|
||||||
|
|
||||||
@@ -142,18 +222,40 @@ def update_close_gate(
|
|||||||
msg = "缺少权利金,无法校验平仓门控"
|
msg = "缺少权利金,无法校验平仓门控"
|
||||||
elif recv is None:
|
elif recv is None:
|
||||||
msg = "暂无有效买盘可回收金额"
|
msg = "暂无有效买盘可回收金额"
|
||||||
elif not recycle_ok:
|
elif missing_index:
|
||||||
|
msg = "缺少指数价,无法按 USDT 校验目标平仓门控"
|
||||||
|
elif mode == "net_pnl":
|
||||||
|
if not recycle_ok:
|
||||||
msg = (
|
msg = (
|
||||||
f"可回收 {_fmt_gate_amt(recv, ccy=ccy)} {ccy} < 权利金×{mult:g}"
|
f"净盈亏 {_fmt_usdt(net_u)}U(估) ≤ {_fmt_usdt(min_pnl_u)}U,"
|
||||||
f"({_fmt_gate_amt(need, ccy=ccy)}),目标平仓门控未过"
|
f"目标平仓门控未过"
|
||||||
)
|
)
|
||||||
elif not ready:
|
elif not ready:
|
||||||
msg = (
|
msg = (
|
||||||
f"可回收已达×{mult:g}({_fmt_gate_amt(recv, ccy=ccy)}/{_fmt_gate_amt(need, ccy=ccy)} {ccy}),"
|
f"净盈亏 {_fmt_usdt(net_u)}U(估) 已>{_fmt_usdt(min_pnl_u)}U,"
|
||||||
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
|
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = f"可回收已达×{mult:g}且持续≥{hold:.0f}s,目标触达后可按买一平仓"
|
msg = (
|
||||||
|
f"净盈亏 {_fmt_usdt(net_u)}U(估) 已>{_fmt_usdt(min_pnl_u)}U"
|
||||||
|
f"且持续≥{hold:.0f}s,目标触达后可按买一平仓"
|
||||||
|
)
|
||||||
|
elif not recycle_ok:
|
||||||
|
msg = (
|
||||||
|
f"可回收 {_fmt_usdt(recv_u)}U(估) < 权利金×{mult:g}"
|
||||||
|
f"({_fmt_usdt(need_u)}U),目标平仓门控未过"
|
||||||
|
)
|
||||||
|
elif not ready:
|
||||||
|
msg = (
|
||||||
|
f"可回收 {_fmt_usdt(recv_u)}U(估) 已达×{mult:g}"
|
||||||
|
f"({_fmt_usdt(recv_u)}/{_fmt_usdt(need_u)}U),"
|
||||||
|
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = (
|
||||||
|
f"可回收 {_fmt_usdt(recv_u)}U(估) 已达×{mult:g}且持续≥{hold:.0f}s,"
|
||||||
|
f"目标触达后可按买一平仓"
|
||||||
|
)
|
||||||
|
|
||||||
auto_blocked = not (ready or passed)
|
auto_blocked = not (ready or passed)
|
||||||
return {
|
return {
|
||||||
@@ -163,7 +265,7 @@ def update_close_gate(
|
|||||||
"recycle_ok": recycle_ok,
|
"recycle_ok": recycle_ok,
|
||||||
"recycle_usdc": recv,
|
"recycle_usdc": recv,
|
||||||
"premium_paid": prem,
|
"premium_paid": prem,
|
||||||
"need_recycle_usdc": need,
|
"need_recycle_usdc": need_u,
|
||||||
"premium_ccy": ccy,
|
"premium_ccy": ccy,
|
||||||
"min_mult": mult,
|
"min_mult": mult,
|
||||||
"hold_seconds": hold,
|
"hold_seconds": hold,
|
||||||
@@ -171,6 +273,13 @@ def update_close_gate(
|
|||||||
"remain_seconds": round(remain, 1) if remain is not None else None,
|
"remain_seconds": round(remain, 1) if remain is not None else None,
|
||||||
"ok_since": ok_since,
|
"ok_since": ok_since,
|
||||||
"msg": msg,
|
"msg": msg,
|
||||||
|
"gate_mode": mode,
|
||||||
|
"index_px": _safe_float(index_px),
|
||||||
|
"recycle_usdt": recv_u,
|
||||||
|
"premium_usdt": prem_u,
|
||||||
|
"need_recycle_usdt": need_u,
|
||||||
|
"net_pnl_usdt": net_u,
|
||||||
|
"net_pnl_min_u": min_pnl_u if mode == "net_pnl" else None,
|
||||||
"auto_close_blocked": auto_blocked,
|
"auto_close_blocked": auto_blocked,
|
||||||
"close_gate_blocked": auto_blocked,
|
"close_gate_blocked": auto_blocked,
|
||||||
}
|
}
|
||||||
@@ -182,12 +291,13 @@ def check_close_gate(
|
|||||||
recycle_usdc: float | None = None,
|
recycle_usdc: float | None = None,
|
||||||
premium_paid: float | None = None,
|
premium_paid: float | None = None,
|
||||||
premium_ccy: str | None = None,
|
premium_ccy: str | None = None,
|
||||||
|
index_px: float | None = None,
|
||||||
refresh: bool = True,
|
refresh: bool = True,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""检查是否允许平仓;默认先用最新回收/权利金刷新."""
|
"""检查是否允许平仓;默认先用最新回收/权利金刷新."""
|
||||||
inst = (inst_id or "").strip()
|
inst = (inst_id or "").strip()
|
||||||
if refresh:
|
if refresh:
|
||||||
if recycle_usdc is None or premium_paid is None or premium_ccy is None:
|
if recycle_usdc is None or premium_paid is None or premium_ccy is None or index_px is None:
|
||||||
with _lock:
|
with _lock:
|
||||||
prev = _gates.get(inst) or {}
|
prev = _gates.get(inst) or {}
|
||||||
if recycle_usdc is None:
|
if recycle_usdc is None:
|
||||||
@@ -196,11 +306,14 @@ def check_close_gate(
|
|||||||
premium_paid = prev.get("premium")
|
premium_paid = prev.get("premium")
|
||||||
if premium_ccy is None:
|
if premium_ccy is None:
|
||||||
premium_ccy = prev.get("premium_ccy")
|
premium_ccy = prev.get("premium_ccy")
|
||||||
|
if index_px is None:
|
||||||
|
index_px = prev.get("index_px")
|
||||||
return update_close_gate(
|
return update_close_gate(
|
||||||
inst,
|
inst,
|
||||||
recycle_usdc=recycle_usdc,
|
recycle_usdc=recycle_usdc,
|
||||||
premium_paid=premium_paid,
|
premium_paid=premium_paid,
|
||||||
premium_ccy=premium_ccy,
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
)
|
)
|
||||||
with _lock:
|
with _lock:
|
||||||
prev = _gates.get(inst)
|
prev = _gates.get(inst)
|
||||||
@@ -210,10 +323,12 @@ def check_close_gate(
|
|||||||
recycle_usdc=recycle_usdc,
|
recycle_usdc=recycle_usdc,
|
||||||
premium_paid=premium_paid,
|
premium_paid=premium_paid,
|
||||||
premium_ccy=premium_ccy,
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
)
|
)
|
||||||
return update_close_gate(
|
return update_close_gate(
|
||||||
inst,
|
inst,
|
||||||
recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"),
|
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_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"),
|
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"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,10 +54,15 @@ def attach_close_preview(
|
|||||||
max_levels=1,
|
max_levels=1,
|
||||||
)
|
)
|
||||||
premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
||||||
# 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要)
|
index_px = _safe_float(row.get("idx_px") or row.get("idxPx"))
|
||||||
|
# 残档时不累计门控;有效买一时刷新计时(仅自动平仓需要)
|
||||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||||
gate = update_close_gate(
|
gate = update_close_gate(
|
||||||
inst_id, recycle_usdc=None, premium_paid=paid, premium_ccy=premium_ccy
|
inst_id,
|
||||||
|
recycle_usdc=None,
|
||||||
|
premium_paid=paid,
|
||||||
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
)
|
)
|
||||||
preview["close_gate"] = gate
|
preview["close_gate"] = gate
|
||||||
preview["close_gate_blocked"] = True
|
preview["close_gate_blocked"] = True
|
||||||
@@ -70,6 +75,7 @@ def attach_close_preview(
|
|||||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||||
premium_paid=paid,
|
premium_paid=paid,
|
||||||
premium_ccy=premium_ccy,
|
premium_ccy=premium_ccy,
|
||||||
|
index_px=index_px,
|
||||||
)
|
)
|
||||||
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
|
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
|
||||||
preview["close_gate"] = gate
|
preview["close_gate"] = gate
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""期权平仓门控:可回收≥2×权利金且持续持有."""
|
"""期权平仓门控:USDT 口径(权利金×倍数 / 净盈亏阈值)且持续持有."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from lib.options.options_close_gate_lib import (
|
from lib.options.options_close_gate_lib import (
|
||||||
@@ -14,43 +15,169 @@ from lib.options.options_close_gate_lib import (
|
|||||||
class OptionsCloseGateTests(unittest.TestCase):
|
class OptionsCloseGateTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
clear_close_gate()
|
clear_close_gate()
|
||||||
|
self._env_backup = {
|
||||||
|
k: os.environ.get(k)
|
||||||
|
for k in (
|
||||||
|
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||||
|
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC",
|
||||||
|
"OKX_OPTIONS_CLOSE_NET_PNL_MIN_U",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for k in self._env_backup:
|
||||||
|
os.environ.pop(k, None)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
clear_close_gate()
|
clear_close_gate()
|
||||||
|
for k, v in self._env_backup.items():
|
||||||
|
if v is None:
|
||||||
|
os.environ.pop(k, None)
|
||||||
|
else:
|
||||||
|
os.environ[k] = v
|
||||||
|
|
||||||
def test_below_2x_not_ready(self):
|
def test_usdc_below_2x_not_ready(self):
|
||||||
g = update_close_gate("ETH-X", recycle_usdc=15.0, premium_paid=10.0, now=1000.0)
|
g = update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=15.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
self.assertFalse(g["recycle_ok"])
|
self.assertFalse(g["recycle_ok"])
|
||||||
self.assertFalse(g["ready"])
|
self.assertFalse(g["ready"])
|
||||||
|
self.assertIn("U(估)", g["msg"])
|
||||||
|
|
||||||
def test_meets_2x_needs_hold(self):
|
def test_usdc_meets_2x_needs_hold(self):
|
||||||
g1 = update_close_gate("ETH-X", recycle_usdc=20.0, premium_paid=10.0, now=1000.0)
|
g1 = update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=20.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
self.assertTrue(g1["recycle_ok"])
|
self.assertTrue(g1["recycle_ok"])
|
||||||
self.assertFalse(g1["ready"])
|
self.assertFalse(g1["ready"])
|
||||||
self.assertAlmostEqual(g1["remain_seconds"], 120.0)
|
self.assertAlmostEqual(g1["remain_seconds"], 120.0)
|
||||||
|
|
||||||
g2 = update_close_gate("ETH-X", recycle_usdc=21.0, premium_paid=10.0, now=1120.0)
|
g2 = update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=21.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1120.0,
|
||||||
|
)
|
||||||
self.assertTrue(g2["ready"])
|
self.assertTrue(g2["ready"])
|
||||||
self.assertGreaterEqual(g2["held_seconds"], 120.0)
|
|
||||||
|
def test_coin_premium_gate_uses_usdt_and_default_105(self):
|
||||||
|
# 0.0384 ETH * 2500 = 96U; ×1.05 = 100.8U
|
||||||
|
g = update_close_gate(
|
||||||
|
"ETH-P",
|
||||||
|
recycle_usdc=0.036,
|
||||||
|
premium_paid=0.0384,
|
||||||
|
premium_ccy="ETH",
|
||||||
|
index_px=2500.0,
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
self.assertFalse(g["recycle_ok"])
|
||||||
|
self.assertAlmostEqual(g["premium_usdt"], 96.0)
|
||||||
|
self.assertAlmostEqual(g["need_recycle_usdt"], 100.8)
|
||||||
|
self.assertIn("U(估)", g["msg"])
|
||||||
|
|
||||||
|
g_ok = update_close_gate(
|
||||||
|
"ETH-P",
|
||||||
|
recycle_usdc=0.041,
|
||||||
|
premium_paid=0.0384,
|
||||||
|
premium_ccy="ETH",
|
||||||
|
index_px=2500.0,
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
self.assertTrue(g_ok["recycle_ok"])
|
||||||
|
self.assertAlmostEqual(g_ok["recycle_usdt"], 102.5)
|
||||||
|
|
||||||
|
def test_net_pnl_gate_mode(self):
|
||||||
|
os.environ["OKX_OPTIONS_CLOSE_GATE_MODE"] = "net_pnl"
|
||||||
|
os.environ["OKX_OPTIONS_CLOSE_NET_PNL_MIN_U"] = "1"
|
||||||
|
g = update_close_gate(
|
||||||
|
"ETH-N",
|
||||||
|
recycle_usdc=0.039,
|
||||||
|
premium_paid=0.0384,
|
||||||
|
premium_ccy="ETH",
|
||||||
|
index_px=2500.0,
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
self.assertTrue(g["recycle_ok"])
|
||||||
|
self.assertAlmostEqual(g["net_pnl_usdt"], 1.5)
|
||||||
|
|
||||||
|
g2 = update_close_gate(
|
||||||
|
"ETH-N2",
|
||||||
|
recycle_usdc=0.0385,
|
||||||
|
premium_paid=0.0384,
|
||||||
|
premium_ccy="ETH",
|
||||||
|
index_px=2500.0,
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
self.assertFalse(g2["recycle_ok"])
|
||||||
|
|
||||||
def test_break_resets_timer(self):
|
def test_break_resets_timer(self):
|
||||||
update_close_gate("ETH-X", recycle_usdc=20.0, premium_paid=10.0, now=1000.0)
|
update_close_gate(
|
||||||
update_close_gate("ETH-X", recycle_usdc=21.0, premium_paid=10.0, now=1100.0)
|
"ETH-X",
|
||||||
g_break = update_close_gate("ETH-X", recycle_usdc=12.0, premium_paid=10.0, now=1110.0)
|
recycle_usdc=20.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=21.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1100.0,
|
||||||
|
)
|
||||||
|
g_break = update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=12.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1110.0,
|
||||||
|
)
|
||||||
self.assertFalse(g_break["recycle_ok"])
|
self.assertFalse(g_break["recycle_ok"])
|
||||||
g_again = update_close_gate("ETH-X", recycle_usdc=22.0, premium_paid=10.0, now=1111.0)
|
g_again = update_close_gate(
|
||||||
|
"ETH-X",
|
||||||
|
recycle_usdc=22.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1111.0,
|
||||||
|
)
|
||||||
self.assertTrue(g_again["recycle_ok"])
|
self.assertTrue(g_again["recycle_ok"])
|
||||||
self.assertFalse(g_again["ready"])
|
self.assertFalse(g_again["ready"])
|
||||||
self.assertAlmostEqual(g_again["held_seconds"], 0.0)
|
self.assertAlmostEqual(g_again["held_seconds"], 0.0)
|
||||||
|
|
||||||
def test_passed_latches_after_ready(self):
|
def test_passed_latches_after_ready(self):
|
||||||
update_close_gate("ETH-Y", recycle_usdc=20.0, premium_paid=10.0, now=1000.0)
|
update_close_gate(
|
||||||
g_ready = update_close_gate("ETH-Y", recycle_usdc=21.0, premium_paid=10.0, now=1120.0)
|
"ETH-Y",
|
||||||
|
recycle_usdc=20.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1000.0,
|
||||||
|
)
|
||||||
|
g_ready = update_close_gate(
|
||||||
|
"ETH-Y",
|
||||||
|
recycle_usdc=21.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1120.0,
|
||||||
|
)
|
||||||
self.assertTrue(g_ready["ready"])
|
self.assertTrue(g_ready["ready"])
|
||||||
self.assertTrue(g_ready["passed"])
|
self.assertTrue(g_ready["passed"])
|
||||||
self.assertTrue(is_close_gate_passed("ETH-Y"))
|
self.assertTrue(is_close_gate_passed("ETH-Y"))
|
||||||
# 后续回收跌破 2×:计时重置,但 passed 仍保留供续批只验流动性
|
g_drop = update_close_gate(
|
||||||
g_drop = update_close_gate("ETH-Y", recycle_usdc=5.0, premium_paid=10.0, now=1130.0)
|
"ETH-Y",
|
||||||
|
recycle_usdc=5.0,
|
||||||
|
premium_paid=10.0,
|
||||||
|
premium_ccy="USDC",
|
||||||
|
now=1130.0,
|
||||||
|
)
|
||||||
self.assertFalse(g_drop["recycle_ok"])
|
self.assertFalse(g_drop["recycle_ok"])
|
||||||
self.assertTrue(g_drop["passed"])
|
self.assertTrue(g_drop["passed"])
|
||||||
self.assertFalse(g_drop["auto_close_blocked"])
|
self.assertFalse(g_drop["auto_close_blocked"])
|
||||||
|
|||||||
Reference in New Issue
Block a user