Add OKX three-way trade mode for options vs hedge.

Env OKX_TRADE_MODE selects standalone options, perp hedge, or OO hedge; hide the other module UI and use group or position limits per mode.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-30 08:28:50 +08:00
parent f9c2a63cbc
commit 83ce50b24e
11 changed files with 417 additions and 69 deletions
+7 -3
View File
@@ -111,7 +111,10 @@ OKX_OPTIONS_API_PASSPHRASE=
OKX_OPTIONS_ACCOUNT_LABEL=主账户·期权
OKX_OPTIONS_TRADE_BUDGET_USDC=10
OKX_OPTIONS_BUDGET_BUFFER=0.95
# 期权同时持仓上限(笔,按交易所合约笔数);0=不限制;期期需≥2(或 0);同合约加仓不占新笔数;热更
# 交易模式三选一(热更):options=单独期权 / perp_options=永期对冲 / options_options=期期对冲
# 选单独期权时隐藏对冲导航与对冲配置;选对冲时不可单独开期权,仓位按「对冲组数上限」
OKX_TRADE_MODE=options
# 仅单独期权模式:期权同时持仓上限(笔);0=不限制;同合约加仓不占新笔数;热更
OKX_OPTIONS_MAX_ACTIVE_POSITIONS=0
OKX_OPTIONS_DEFAULT_UNDERLY=ETH
# 期权链仅显示卖一深度≥1张的合约(估算卖一/无深度不显示);false 则显示全部
@@ -128,10 +131,10 @@ OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12
# =============================================================================
# 对冲计划(仅 OKX;前端 env「对冲计划」;详见 docs/对冲计划开发方案.md)
# 对冲计划(仅 OKX;由 OKX_TRADE_MODE 控制是否启用;详见 docs/对冲计划开发方案.md)
# =============================================================================
# 以下三项已由 OKX_TRADE_MODE 取代,保留兼容旧部署(未配置 TRADE_MODE 时仍可读)
HEDGE_PLAN_ENABLED=false
# 页面 Tab 显示(默认全部显示,可单独关闭;不影响已有进行中/历史计划)
HEDGE_PLAN_SHOW_PERP_OPTIONS=true
HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true
HEDGE_PLAN_LIVE_ORDER=false
@@ -149,6 +152,7 @@ HEDGE_PLAN_OO_BIAS_RATIO=0.7
HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true
# 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效
HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL=true
# 对冲组数上限(默认 1;opening/active/partial 计入);仅永期/期期模式生效;热更
MAX_ACTIVE_HEDGE_PLANS=1
HEDGE_PLAN_MONITOR_POLL_SECONDS=15
# 半腿失败自动平期权;若 MANUAL_COMPLETE_ON_PARTIAL=true 则运行时强制无效(建议一并写成 false)
+12 -7
View File
@@ -6714,6 +6714,10 @@ def render_main_page(page="trade", embed_mode=None):
from lib.instance.instance_display_prefs_lib import display_prefs_template_context
_display_ctx = display_prefs_template_context(get_db)
from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode
_okx_trade_mode = get_okx_trade_mode()
_hedge_mode_on = _okx_trade_mode in ("perp_options", "options_options")
template_ctx = dict(
page=page,
key=key_list,
@@ -6803,12 +6807,12 @@ def render_main_page(page="trade", embed_mode=None):
exchange_display=EXCHANGE_DISPLAY_NAME,
options_enabled=OKX_OPTIONS_ENABLED,
options_nav_visible=True,
hedge_plan_enabled=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
hedge_plan_nav_visible=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
hedge_plan_show_perp_options=os.getenv("HEDGE_PLAN_SHOW_PERP_OPTIONS", "true").lower()
in ("1", "true", "yes", "on"),
hedge_plan_show_options_options=os.getenv("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "true").lower()
in ("1", "true", "yes", "on"),
okx_trade_mode=_okx_trade_mode,
options_open_allowed=_okx_trade_mode == "options",
hedge_plan_enabled=_hedge_mode_on,
hedge_plan_nav_visible=_hedge_mode_on,
hedge_plan_show_perp_options=_okx_trade_mode == "perp_options",
hedge_plan_show_options_options=_okx_trade_mode == "options_options",
hedge_plan_oo_close_mode_enabled=os.getenv("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", "true").lower()
in ("1", "true", "yes", "on"),
hedge_plan_budget_buffer=float(os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"),
@@ -9110,6 +9114,7 @@ def _dashboard_enrich_orders(items):
return enrich_order_items_with_marks(items, get_price=get_price)
from lib.hedge_plan.okx_trade_mode_lib import hedge_module_enabled
from lib.instance.instance_dashboard_register import register_instance_dashboard_routes
register_instance_dashboard_routes(
@@ -9118,7 +9123,7 @@ register_instance_dashboard_routes(
get_db=get_db,
fetch_options_positions=_dashboard_fetch_options_positions,
enrich_orders=_dashboard_enrich_orders,
hedge_enabled=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
hedge_enabled=hedge_module_enabled,
)
+7
View File
@@ -91,6 +91,8 @@ HOT_RELOAD_EXACT = frozenset({
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS",
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED",
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS",
"OKX_TRADE_MODE",
"MAX_ACTIVE_HEDGE_PLANS",
"HEDGE_PLAN_LIVE_ORDER",
"HEDGE_PLAN_OPEN_ORDER",
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
@@ -145,6 +147,11 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
("budget", "预算金额"),
("sheets", "张数"),
),
"OKX_TRADE_MODE": (
("options", "单独期权"),
("perp_options", "永期对冲"),
("options_options", "期期对冲"),
),
}
_SELECT_ALIASES: dict[str, dict[str, str]] = {
+127 -52
View File
@@ -119,6 +119,18 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
},
]
_MODE_SECTION: dict[str, Any] = {
"title": "期权/对冲模式",
"exchanges": frozenset({"okx"}),
"fields": [
(
"OKX_TRADE_MODE",
"交易模式",
"三选一:单独期权 / 永期对冲 / 期期对冲.选单独期权时隐藏对冲导航与对冲配置;选对冲时不可单独开期权",
),
],
}
_OPTIONS_SECTION: dict[str, Any] = {
"title": "期权账户",
"exchanges": frozenset({"okx"}),
@@ -133,7 +145,7 @@ _OPTIONS_SECTION: dict[str, Any] = {
(
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS",
"期权持仓上限(笔)",
"默认 0=不限制;按交易所期权合约笔数计数.期期对冲一次需 2 笔,上限设 1 时无法开期期(须≥2 或 0).同合约加仓不占新笔数.",
"仅「单独期权」模式生效;默认 0=不限制;按交易所期权合约笔数计数,同合约加仓不占新笔数",
),
("OKX_OPTIONS_DEFAULT_UNDERLY", "默认标的", "如 ETH"),
(
@@ -144,55 +156,69 @@ _OPTIONS_SECTION: dict[str, Any] = {
],
}
# 对冲公共字段(不含已由 OKX_TRADE_MODE 取代的 ENABLED/SHOW/MUTUAL)
_HEDGE_COMMON_FIELDS: list[tuple[str, str, str]] = [
("HEDGE_PLAN_LIVE_ORDER", "允许对冲真实下单", "再与实盘 LIVE_TRADING_ENABLED 同开才可启动"),
(
"MAX_ACTIVE_HEDGE_PLANS",
"对冲组数上限",
"默认 1;同时进行中的对冲计划组数(opening/active/partial),可改",
),
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
(
"HEDGE_PLAN_BUDGET_BUFFER",
"对冲预算缓冲比例",
"默认 0.95;仅对冲计划;与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立",
),
(
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
"半腿失败改手动补开",
"默认 true;开启时半腿失败不自动平,计划挂 partial,页面可补开;并强制关闭下方自动平",
),
(
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
"半腿失败时自动平期权",
"默认 true;若上方「半腿失败改手动补开」开启则本项强制无效",
),
]
_HEDGE_PO_FIELDS: list[tuple[str, str, str]] = [
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"),
]
_HEDGE_OO_FIELDS: list[tuple[str, str, str]] = [
("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "期期只平盈利腿", "达目标价只平盈利方"),
(
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
"期期平仓模式(方案C)",
"默认 true;开启后页面可选「到期平/全平」;关闭则固定到期平",
),
(
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
"期期做多做空拆分口径",
"默认预算金额;budget=按权利金预算分两腿;sheets=先算同张数再按比例拆",
),
(
"HEDGE_PLAN_OO_BIAS_RATIO",
"期期做多做空主腿占比",
"默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
),
]
# 兼容旧测试/全量字段列表(写 env 时仍允许这些键,但 UI 按模式过滤)
_HEDGE_PLAN_SECTION: dict[str, Any] = {
"title": "对冲计划",
"exchanges": frozenset({"okx"}),
"fields": [
("HEDGE_PLAN_ENABLED", "启用对冲计划", "关闭则隐藏导航且不可开仓"),
("HEDGE_PLAN_SHOW_PERP_OPTIONS", "显示永期对冲", "默认 true;关闭后隐藏永期 Tab,不可测算/开仓"),
("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "显示期期对冲", "默认 true;关闭后隐藏期期 Tab,不可测算/开仓"),
("HEDGE_PLAN_LIVE_ORDER", "允许对冲真实下单", "再与实盘 LIVE_TRADING_ENABLED 同开才可启动永期"),
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"),
("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "期期只平盈利腿", "达目标价只平盈利方"),
(
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
"期期平仓模式(方案C)",
"默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平",
),
(
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
"期期做多做空拆分口径",
"默认预算金额;budget=按权利金预算按比例分两腿;sheets=先算同张数总张数(2n)再按比例拆",
),
(
"HEDGE_PLAN_OO_BIAS_RATIO",
"期期做多做空主腿占比",
"默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
),
(
"HEDGE_PLAN_BUDGET_BUFFER",
"对冲预算缓冲比例",
"默认 0.95;仅对冲计划(期期可用预算=交易户×本比例);与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立",
),
(
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
"对冲与期权互斥门控",
"默认 true;开启时:有对冲计划则不可单独开期权,有单独期权则不可启动对冲;关闭后两边可同时开",
),
(
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
"半腿失败改手动补开",
"默认 true;开启时半腿失败不自动平,计划挂 partial,页面可补开永续/腿B;并强制关闭下方自动平",
),
("MAX_ACTIVE_HEDGE_PLANS", "最大同时活跃计划数", "建议 1"),
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
(
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
"半腿失败时自动平期权",
"默认 true;若上方「半腿失败改手动补开」开启则本项强制无效(不会自动平)",
),
("HEDGE_PLAN_ENABLED", "启用对冲计划", "已由「交易模式」取代,一般无需再改"),
("HEDGE_PLAN_SHOW_PERP_OPTIONS", "显示永期对冲", "已由「交易模式」取代"),
("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "显示期期对冲", "已由「交易模式」取代"),
("HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", "对冲与期权互斥门控", "已由「交易模式」三选一取代"),
*_HEDGE_COMMON_FIELDS,
*_HEDGE_PO_FIELDS,
*_HEDGE_OO_FIELDS,
],
}
@@ -212,6 +238,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED": "true",
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0",
"OKX_TRADE_MODE": "options",
"MAX_ACTIVE_HEDGE_PLANS": "1",
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
"HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
@@ -293,24 +321,69 @@ def _build_field(
return out
def ui_sections_for_exchange(exchange_key: str) -> list[dict[str, Any]]:
def _okx_mode_for_env_ui() -> str:
try:
from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode
return get_okx_trade_mode()
except Exception:
return "options"
def _options_fields_for_mode(mode: str) -> list[tuple[str, str, str]]:
fields = list(_OPTIONS_SECTION["fields"])
if mode != "options":
fields = [f for f in fields if f[0] != "OKX_OPTIONS_MAX_ACTIVE_POSITIONS"]
return fields
def _hedge_fields_for_mode(mode: str) -> list[tuple[str, str, str]]:
if mode == "perp_options":
return [*_HEDGE_COMMON_FIELDS, *_HEDGE_PO_FIELDS]
if mode == "options_options":
return [*_HEDGE_COMMON_FIELDS, *_HEDGE_OO_FIELDS]
return []
def ui_sections_for_exchange(
exchange_key: str,
*,
mode: str | None = None,
) -> list[dict[str, Any]]:
ex = (exchange_key or "").strip().lower()
sections: list[dict[str, Any]] = []
live_fields = _EXCHANGE_LIVE_FIELDS.get(ex, _EXCHANGE_LIVE_FIELDS["okx"])
sections.append({"title": "交易所与实盘", "fields": live_fields})
sections.extend(_SHARED_SECTIONS)
if ex in _OPTIONS_SECTION.get("exchanges", frozenset()):
sections.append(_OPTIONS_SECTION)
if ex in _HEDGE_PLAN_SECTION.get("exchanges", frozenset()):
sections.append(_HEDGE_PLAN_SECTION)
if ex in _MODE_SECTION.get("exchanges", frozenset()):
from lib.hedge_plan.okx_trade_mode_lib import normalize_okx_trade_mode
m = normalize_okx_trade_mode(mode) if mode else ""
if not m:
m = _okx_mode_for_env_ui()
sections.append(_MODE_SECTION)
sections.append({"title": "期权账户", "fields": _options_fields_for_mode(m)})
hedge_fields = _hedge_fields_for_mode(m)
if hedge_fields:
title = "对冲计划·永期" if m == "perp_options" else "对冲计划·期期"
sections.append({"title": title, "fields": hedge_fields})
return sections
def ui_allowed_keys(exchange_key: str) -> frozenset[str]:
"""可写键=当前模式可见字段 + 模式切换键 + 遗留对冲开关(兼容旧脚本写入)."""
keys: set[str] = set()
for sec in ui_sections_for_exchange(exchange_key):
for item in sec["fields"]:
keys.add(item[0])
ex = (exchange_key or "").strip().lower()
if ex == "okx":
keys.add("OKX_TRADE_MODE")
# 允许写入遗留键,避免旧自动化/手改失败;页面不再展示
for item in _HEDGE_PLAN_SECTION["fields"]:
keys.add(item[0])
for item in _OPTIONS_SECTION["fields"]:
keys.add(item[0])
return frozenset(keys)
@@ -323,7 +396,9 @@ def build_env_ui_payload(
env_lines = read_env_lines(env_path)
values = env_get_all(env_lines)
groups: list[dict[str, Any]] = []
for sec in ui_sections_for_exchange(exchange_key):
for sec in ui_sections_for_exchange(
exchange_key, mode=values.get("OKX_TRADE_MODE") or ""
):
fields = [
_build_field(key, label, note, schema, values)
for key, label, note in sec["fields"]
+9 -3
View File
@@ -108,15 +108,21 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
def _hedge_enabled() -> bool:
return _env_bool("HEDGE_PLAN_ENABLED", False)
from lib.hedge_plan.okx_trade_mode_lib import hedge_module_enabled
return hedge_module_enabled()
def _show_perp_options() -> bool:
return _env_bool("HEDGE_PLAN_SHOW_PERP_OPTIONS", True)
from lib.hedge_plan.okx_trade_mode_lib import show_perp_options
return show_perp_options()
def _show_options_options() -> bool:
return _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True)
from lib.hedge_plan.okx_trade_mode_lib import show_options_options
return show_options_options()
def _oo_close_mode_enabled() -> bool:
+103
View File
@@ -0,0 +1,103 @@
"""OKX 期权/对冲三选一模式(env: OKX_TRADE_MODE).
options 仅单独期权(隐藏对冲导航与对冲 env 配置)
perp_options 仅永期对冲(不可单独开期权;对冲组数上限 MAX_ACTIVE_HEDGE_PLANS)
options_options 仅期期对冲(同上)
"""
from __future__ import annotations
import os
from typing import Optional
MODE_OPTIONS = "options"
MODE_PERP = "perp_options"
MODE_OO = "options_options"
VALID_MODES = frozenset({MODE_OPTIONS, MODE_PERP, MODE_OO})
_ALIASES = {
"option": MODE_OPTIONS,
"standalone": MODE_OPTIONS,
"期权": MODE_OPTIONS,
"单独期权": MODE_OPTIONS,
"po": MODE_PERP,
"perp": MODE_PERP,
"永期": MODE_PERP,
"永期对冲": MODE_PERP,
"oo": MODE_OO,
"期期": MODE_OO,
"期期对冲": MODE_OO,
}
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None or str(raw).strip() == "":
return default
return str(raw).strip().lower() in ("1", "true", "yes", "on")
def normalize_okx_trade_mode(raw: Optional[str]) -> str:
s = str(raw or "").strip().lower()
if s in VALID_MODES:
return s
if s in _ALIASES:
return _ALIASES[s]
return ""
def legacy_infer_okx_trade_mode() -> str:
"""未配置 OKX_TRADE_MODE 时,按旧开关推断,避免已有部署行为突变."""
if not _env_bool("HEDGE_PLAN_ENABLED", False):
return MODE_OPTIONS
show_po = _env_bool("HEDGE_PLAN_SHOW_PERP_OPTIONS", True)
show_oo = _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True)
if show_po and not show_oo:
return MODE_PERP
if show_oo and not show_po:
return MODE_OO
if show_po:
return MODE_PERP
if show_oo:
return MODE_OO
return MODE_OPTIONS
def get_okx_trade_mode() -> str:
m = normalize_okx_trade_mode(os.getenv("OKX_TRADE_MODE"))
if m:
return m
return legacy_infer_okx_trade_mode()
def hedge_module_enabled() -> bool:
return get_okx_trade_mode() in (MODE_PERP, MODE_OO)
def show_perp_options() -> bool:
return get_okx_trade_mode() == MODE_PERP
def show_options_options() -> bool:
return get_okx_trade_mode() == MODE_OO
def standalone_options_open_allowed() -> bool:
return get_okx_trade_mode() == MODE_OPTIONS
def mode_label(mode: Optional[str] = None) -> str:
m = mode or get_okx_trade_mode()
return {
MODE_OPTIONS: "单独期权",
MODE_PERP: "永期对冲",
MODE_OO: "期期对冲",
}.get(m, m or "")
def block_standalone_open_by_mode_msg() -> Optional[str]:
if standalone_options_open_allowed():
return None
return (
f"当前交易模式为「{mode_label()}」,不可单独开期权;"
"请在 env「交易模式」切换为「单独期权」"
)
+10 -2
View File
@@ -12,19 +12,27 @@ def register_instance_dashboard_routes(
login_required: Callable,
get_db: Callable,
fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
hedge_enabled: bool = False,
hedge_enabled: bool | Callable[[], bool] = False,
enrich_orders: Optional[Callable[[list[dict[str, Any]]], list[dict[str, Any]]]] = None,
) -> None:
from lib.instance.instance_dashboard_cache import instance_dashboard_store
from lib.instance.instance_dashboard_lib import build_instance_dashboard_payload
def _hedge_on() -> bool:
if callable(hedge_enabled):
try:
return bool(hedge_enabled())
except Exception:
return False
return bool(hedge_enabled)
def _build() -> dict[str, Any]:
conn = get_db()
try:
payload = build_instance_dashboard_payload(
conn,
fetch_options_positions=fetch_options_positions,
hedge_enabled=bool(hedge_enabled),
hedge_enabled=_hedge_on(),
)
if callable(enrich_orders) and payload.get("ok") and isinstance(payload.get("orders"), dict):
items = list(payload["orders"].get("items") or [])
@@ -73,6 +73,14 @@ def option_position_limit_block_msg(
- 期期两腿应一次传入两个 inst_id,在开仓前预检,避免上限=1 时开出半边仓
- 拉持仓失败:拒绝开仓(避免绕过上限)
"""
try:
from lib.hedge_plan.okx_trade_mode_lib import standalone_options_open_allowed
# 对冲模式用 MAX_ACTIVE_HEDGE_PLANS 管「组数」,不占用期权笔数上限
if max_active is None and not standalone_options_open_allowed():
return None
except Exception:
pass
mx = options_max_active_positions() if max_active is None else int(max_active)
if mx <= 0:
return None
+34
View File
@@ -456,6 +456,32 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
pass
ask = q.get("ask")
ask_sz = q.get("ask_sz")
try:
from lib.hedge_plan.okx_trade_mode_lib import block_standalone_open_by_mode_msg
mode_block = block_standalone_open_by_mode_msg()
if mode_block:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": mode_block,
"quote_per_unit": ask,
"premium_per_sheet": None,
"sizing": {
"ok": False,
"msg": mode_block,
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
"available_usdc": available_usdc,
"budget_full_usdc": budget if mode == "budget_full" else None,
}
)
except Exception:
pass
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
if not can_open:
# 合约可报价,但不可开仓:返回参考标记价供展示
@@ -566,6 +592,14 @@ 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.okx_trade_mode_lib import block_standalone_open_by_mode_msg
mode_block = block_standalone_open_by_mode_msg()
if mode_block:
return jsonify({"ok": False, "msg": mode_block, "can_open": False})
except Exception:
pass
try:
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
+5 -2
View File
@@ -6,10 +6,13 @@
{% if not options_enabled %}
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>.</div>
{% endif %}
{% if options_enabled and options_open_allowed is defined and not options_open_allowed %}
<div class="flash" style="margin-bottom:12px">当前交易模式为对冲(永期/期期),不可单独开期权;持仓可在此查看/平仓.切换请到 env「交易模式」.</div>
{% endif %}
<div class="options-dual-grid">
<div class="card options-order-card">
<h2>期权下单</h2>
<div class="card options-order-card"{% if options_open_allowed is defined and not options_open_allowed %} style="opacity:.72"{% endif %}>
<h2>期权下单{% if options_open_allowed is defined and not options_open_allowed %} <small class="muted">(对冲模式已禁用开仓)</small>{% endif %}</h2>
<details class="opt-close-rule opt-open-rule">
<summary>开仓规则说明</summary>
<div class="opt-close-rule-body">
+95
View File
@@ -0,0 +1,95 @@
"""OKX_TRADE_MODE 三选一."""
import os
import unittest
from unittest.mock import patch
from lib.hedge_plan.okx_trade_mode_lib import (
MODE_OO,
MODE_OPTIONS,
MODE_PERP,
block_standalone_open_by_mode_msg,
get_okx_trade_mode,
hedge_module_enabled,
show_options_options,
show_perp_options,
standalone_options_open_allowed,
)
class OkxTradeModeTests(unittest.TestCase):
def test_explicit_modes(self):
for mode, hedge, po, oo in (
(MODE_OPTIONS, False, False, False),
(MODE_PERP, True, True, False),
(MODE_OO, True, False, True),
):
with patch.dict(os.environ, {"OKX_TRADE_MODE": mode}, clear=False):
self.assertEqual(get_okx_trade_mode(), mode)
self.assertEqual(hedge_module_enabled(), hedge)
self.assertEqual(show_perp_options(), po)
self.assertEqual(show_options_options(), oo)
self.assertEqual(standalone_options_open_allowed(), mode == MODE_OPTIONS)
def test_legacy_infer_options(self):
with patch.dict(
os.environ,
{"HEDGE_PLAN_ENABLED": "false"},
clear=False,
):
os.environ.pop("OKX_TRADE_MODE", None)
self.assertEqual(get_okx_trade_mode(), MODE_OPTIONS)
def test_legacy_infer_perp(self):
with patch.dict(
os.environ,
{
"HEDGE_PLAN_ENABLED": "true",
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "false",
},
clear=False,
):
os.environ.pop("OKX_TRADE_MODE", None)
self.assertEqual(get_okx_trade_mode(), MODE_PERP)
def test_block_standalone_in_hedge_mode(self):
with patch.dict(os.environ, {"OKX_TRADE_MODE": MODE_OO}):
msg = block_standalone_open_by_mode_msg()
self.assertIsNotNone(msg)
self.assertIn("期期", msg or "")
class EnvUiModeSectionsTests(unittest.TestCase):
def test_options_mode_hides_hedge_section(self):
from lib.env.env_ui_manifest import ui_sections_for_exchange
titles = [s["title"] for s in ui_sections_for_exchange("okx", mode="options")]
self.assertIn("期权/对冲模式", titles)
self.assertIn("期权账户", titles)
self.assertTrue(all("对冲" not in t for t in titles if t != "期权/对冲模式"))
def test_perp_mode_shows_po_fields(self):
from lib.env.env_ui_manifest import ui_sections_for_exchange
secs = {s["title"]: s["fields"] for s in ui_sections_for_exchange("okx", mode="perp_options")}
self.assertIn("对冲计划·永期", secs)
keys = {f[0] for f in secs["对冲计划·永期"]}
self.assertIn("MAX_ACTIVE_HEDGE_PLANS", keys)
self.assertIn("HEDGE_PLAN_OPEN_ORDER", keys)
self.assertNotIn("HEDGE_PLAN_OO_BIAS_RATIO", keys)
opt_keys = {f[0] for f in secs["期权账户"]}
self.assertNotIn("OKX_OPTIONS_MAX_ACTIVE_POSITIONS", opt_keys)
def test_oo_mode_shows_oo_fields(self):
from lib.env.env_ui_manifest import ui_sections_for_exchange
secs = {s["title"]: s["fields"] for s in ui_sections_for_exchange("okx", mode="options_options")}
self.assertIn("对冲计划·期期", secs)
keys = {f[0] for f in secs["对冲计划·期期"]}
self.assertIn("HEDGE_PLAN_OO_BIAS_RATIO", keys)
self.assertNotIn("HEDGE_PLAN_OPEN_ORDER", keys)
if __name__ == "__main__":
unittest.main()