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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Vendored
+7
@@ -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]] = {
|
||||
|
||||
Vendored
+127
-52
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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「交易模式」切换为「单独期权」"
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user