Add mutual-exclusion gate between hedge plans and standalone options.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -135,6 +135,8 @@ HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
|
||||
HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true
|
||||
# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平
|
||||
HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true
|
||||
# 对冲与单独期权互斥(默认 true):有对冲计划不可单独开期权;有单独期权不可启动对冲;false=可同时开
|
||||
HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true
|
||||
MAX_ACTIVE_HEDGE_PLANS=1
|
||||
HEDGE_PLAN_MONITOR_POLL_SECONDS=15
|
||||
HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=true
|
||||
|
||||
Vendored
+1
@@ -91,6 +91,7 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
||||
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
|
||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||
"MAX_ACTIVE_HEDGE_PLANS",
|
||||
"HEDGE_PLAN_MONITOR_POLL_SECONDS",
|
||||
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
|
||||
|
||||
Vendored
+6
@@ -146,6 +146,11 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
|
||||
"期期平仓模式(方案C)",
|
||||
"默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平",
|
||||
),
|
||||
(
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||
"对冲与期权互斥门控",
|
||||
"默认 true;开启时:有对冲计划则不可单独开期权,有单独期权则不可启动对冲;关闭后两边可同时开",
|
||||
),
|
||||
("MAX_ACTIVE_HEDGE_PLANS", "最大同时活跃计划数", "建议 1"),
|
||||
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
|
||||
("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", "半腿失败时自动平期权", ""),
|
||||
@@ -163,6 +168,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""对冲计划与单独期权开仓互斥门控.
|
||||
|
||||
默认开启:有进行中对冲计划时禁止单独开期权;有纯期权持仓时禁止启动对冲计划.
|
||||
关闭 HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE 后两边可同时开.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
|
||||
def _env_bool(key: str, default: bool = False) -> bool:
|
||||
v = (os.getenv(key) or "").strip().lower()
|
||||
if not v:
|
||||
return default
|
||||
return v in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def mutual_exclusive_enabled() -> bool:
|
||||
return _env_bool("HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", True)
|
||||
|
||||
|
||||
def block_standalone_option_open_msg(conn: Any) -> Optional[str]:
|
||||
"""若应拦截单独开期权,返回中文原因;否则 None."""
|
||||
if not mutual_exclusive_enabled():
|
||||
return None
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables
|
||||
|
||||
init_hedge_plan_tables(conn)
|
||||
if count_active_plans(conn) > 0:
|
||||
return "存在进行中对冲计划,禁止单独开期权(可在 env「对冲与期权互斥门控」关闭)"
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _pos_nonzero(raw: dict[str, Any]) -> bool:
|
||||
try:
|
||||
return abs(float(raw.get("pos") or 0)) > 1e-12
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def has_standalone_option_position(conn: Any, raw_positions: list[dict[str, Any]] | None) -> bool:
|
||||
"""交易所期权持仓中,是否存在未挂在进行中对冲计划腿上的仓位."""
|
||||
if not raw_positions:
|
||||
return False
|
||||
from lib.instance.instance_dashboard_lib import _resolve_options_source
|
||||
|
||||
for p in raw_positions:
|
||||
if not isinstance(p, dict) or not _pos_nonzero(p):
|
||||
continue
|
||||
inst = str(p.get("instId") or p.get("inst_id") or "").strip()
|
||||
if not inst:
|
||||
continue
|
||||
source, _, _ = _resolve_options_source(conn, inst)
|
||||
if source == "option":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def block_hedge_plan_start_msg(
|
||||
conn: Any,
|
||||
*,
|
||||
fetch_positions: Optional[Callable[[Any], Any]] = None,
|
||||
exchange: Any = None,
|
||||
raw_positions: list[dict[str, Any]] | None = None,
|
||||
) -> Optional[str]:
|
||||
"""若应拦截启动对冲计划,返回中文原因;否则 None."""
|
||||
if not mutual_exclusive_enabled():
|
||||
return None
|
||||
rows = raw_positions
|
||||
if rows is None:
|
||||
if fetch_positions is None or exchange is None:
|
||||
return None
|
||||
try:
|
||||
rows = fetch_positions(exchange) or []
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
if has_standalone_option_position(conn, rows):
|
||||
return "存在单独期权持仓,禁止启动对冲计划(可在 env「对冲与期权互斥门控」关闭)"
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
@@ -476,6 +476,8 @@ def gate_status(
|
||||
max_active: int = 1,
|
||||
show_perp_options: bool = True,
|
||||
show_options_options: bool = True,
|
||||
mutual_exclusive: bool = True,
|
||||
has_standalone_option: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
from lib.trade.position_sizing_lib import is_full_margin_mode
|
||||
|
||||
@@ -505,6 +507,9 @@ def gate_status(
|
||||
if active_count >= max(1, int(max_active or 1)):
|
||||
can_start = False
|
||||
reasons.append(f"活跃计划已达上限({max_active})")
|
||||
if mutual_exclusive and has_standalone_option:
|
||||
can_start = False
|
||||
reasons.append("存在单独期权持仓,禁止启动对冲计划(互斥门控)")
|
||||
if pt == "perp_options":
|
||||
if not full:
|
||||
can_start = False
|
||||
@@ -531,6 +536,8 @@ def gate_status(
|
||||
"max_active": max_active,
|
||||
"show_perp_options": bool(show_perp_options),
|
||||
"show_options_options": bool(show_options_options),
|
||||
"mutual_exclusive": bool(mutual_exclusive),
|
||||
"has_standalone_option": bool(has_standalone_option),
|
||||
"can_preview": can_preview,
|
||||
"can_start": can_start,
|
||||
"reasons": reasons,
|
||||
|
||||
@@ -143,18 +143,35 @@ def _max_active() -> int:
|
||||
|
||||
def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
|
||||
active = 0
|
||||
has_standalone = False
|
||||
mutual = True
|
||||
try:
|
||||
from lib.hedge_plan.hedge_options_exclusive_lib import (
|
||||
has_standalone_option_position,
|
||||
mutual_exclusive_enabled,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables
|
||||
|
||||
mutual = mutual_exclusive_enabled()
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
active = count_active_plans(conn)
|
||||
if mutual:
|
||||
try:
|
||||
from lib.exchange.okx_options_lib import fetch_option_positions
|
||||
|
||||
ex = cfg.get("exchange_options") or cfg.get("exchange")
|
||||
raw = fetch_option_positions(ex) if ex is not None else []
|
||||
has_standalone = has_standalone_option_position(conn, raw or [])
|
||||
except Exception:
|
||||
has_standalone = False
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
active = 0
|
||||
has_standalone = False
|
||||
return gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
@@ -166,6 +183,8 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
|
||||
max_active=_max_active(),
|
||||
show_perp_options=_show_perp_options(),
|
||||
show_options_options=_show_options_options(),
|
||||
mutual_exclusive=mutual,
|
||||
has_standalone_option=has_standalone,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -509,6 +509,18 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ex, err = _require_options_ex(cfg)
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": err})
|
||||
try:
|
||||
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
|
||||
|
||||
conn_gate = cfg["get_db"]()
|
||||
try:
|
||||
block_msg = block_standalone_option_open_msg(conn_gate)
|
||||
finally:
|
||||
conn_gate.close()
|
||||
if block_msg:
|
||||
return jsonify({"ok": False, "msg": block_msg})
|
||||
except Exception:
|
||||
pass
|
||||
data = request.get_json(silent=True) or {}
|
||||
inst_id = (data.get("inst_id") or "").strip()
|
||||
mode = (data.get("mode") or "budget_full").strip()
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""对冲计划与单独期权互斥门控."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from lib.hedge_plan.hedge_options_exclusive_lib import (
|
||||
block_hedge_plan_start_msg,
|
||||
block_standalone_option_open_msg,
|
||||
has_standalone_option_position,
|
||||
mutual_exclusive_enabled,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_calc_lib import gate_status
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
|
||||
|
||||
|
||||
class HedgeOptionsExclusiveTests(unittest.TestCase):
|
||||
def test_mutual_default_true(self):
|
||||
with mock.patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop("HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", None)
|
||||
self.assertTrue(mutual_exclusive_enabled())
|
||||
|
||||
def test_mutual_can_disable(self):
|
||||
with mock.patch.dict(os.environ, {"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "false"}):
|
||||
self.assertFalse(mutual_exclusive_enabled())
|
||||
|
||||
def test_block_open_when_active_plan(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_hedge_plan_tables(conn)
|
||||
conn.execute(
|
||||
"INSERT INTO hedge_plans (plan_type, underlying, status) "
|
||||
"VALUES ('options_options', 'ETH', 'active')"
|
||||
)
|
||||
conn.commit()
|
||||
with mock.patch.dict(os.environ, {"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true"}):
|
||||
msg = block_standalone_option_open_msg(conn)
|
||||
self.assertIsNotNone(msg)
|
||||
self.assertIn("对冲计划", msg or "")
|
||||
with mock.patch.dict(os.environ, {"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "false"}):
|
||||
self.assertIsNone(block_standalone_option_open_msg(conn))
|
||||
conn.close()
|
||||
|
||||
def test_standalone_position_detection(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_hedge_plan_tables(conn)
|
||||
raw = [{"instId": "ETH-USD_UM-260719-1890-C", "pos": "1"}]
|
||||
with mock.patch(
|
||||
"lib.instance.instance_dashboard_lib._resolve_options_source",
|
||||
return_value=("option", "纯期权", None),
|
||||
):
|
||||
self.assertTrue(has_standalone_option_position(conn, raw))
|
||||
with mock.patch(
|
||||
"lib.instance.instance_dashboard_lib._resolve_options_source",
|
||||
return_value=("options_options", "期期对冲", 2),
|
||||
):
|
||||
self.assertFalse(has_standalone_option_position(conn, raw))
|
||||
conn.close()
|
||||
|
||||
def test_block_hedge_start_msg(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_hedge_plan_tables(conn)
|
||||
raw = [{"instId": "ETH-USD_UM-260719-1890-C", "pos": "2"}]
|
||||
with mock.patch.dict(os.environ, {"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true"}):
|
||||
with mock.patch(
|
||||
"lib.instance.instance_dashboard_lib._resolve_options_source",
|
||||
return_value=("option", "纯期权", None),
|
||||
):
|
||||
msg = block_hedge_plan_start_msg(conn, raw_positions=raw)
|
||||
self.assertIsNotNone(msg)
|
||||
self.assertIn("单独期权", msg or "")
|
||||
conn.close()
|
||||
|
||||
def test_gate_status_blocks_start(self):
|
||||
g = gate_status(
|
||||
hedge_enabled=True,
|
||||
sizing_mode="full_margin",
|
||||
plan_type="options_options",
|
||||
options_enabled=True,
|
||||
live_order=True,
|
||||
mutual_exclusive=True,
|
||||
has_standalone_option=True,
|
||||
)
|
||||
self.assertFalse(g["can_start"])
|
||||
self.assertTrue(any("单独期权" in r for r in g["reasons"]))
|
||||
g2 = gate_status(
|
||||
hedge_enabled=True,
|
||||
sizing_mode="full_margin",
|
||||
plan_type="options_options",
|
||||
options_enabled=True,
|
||||
live_order=True,
|
||||
mutual_exclusive=False,
|
||||
has_standalone_option=True,
|
||||
)
|
||||
self.assertTrue(g2["can_start"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user