From 20e0c2cb9fe7dc2bdc2ade9397e49cd89bdaaa3b Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 9 Aug 2026 09:05:32 +0800 Subject: [PATCH] refactor(hedge): env-driven option-primary UI with grouped params Move mode switch to HEDGE_PLAN_OPTION_PRIMARY, split left into capital/select/exit groups, and show perp quote above options with leverage auto-match. Co-authored-by: Cursor --- crypto_monitor_okx/.env.example | 2 + crypto_monitor_okx/app.py | 2 + docs/对冲计划-以期权为主.md | 10 +- lib/common/static/hedge_plan.js | 112 +++++----- lib/common/static/instance_theme.css | 83 +++++++- lib/env/env_schema.py | 5 + lib/env/env_ui_manifest.py | 6 + .../templates/hedge_plan_panel.html | 191 +++++++++++------- 8 files changed, 281 insertions(+), 130 deletions(-) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 2cd4ee2..0fbe210 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -141,6 +141,8 @@ HEDGE_PLAN_ENABLED=false HEDGE_PLAN_SHOW_PERP_OPTIONS=true HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true HEDGE_PLAN_LIVE_ORDER=false +# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换) +HEDGE_PLAN_OPTION_PRIMARY=true HEDGE_PLAN_OPEN_ORDER=options_first HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index e367a31..b1d8bca 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6870,6 +6870,8 @@ def render_main_page(page="trade", embed_mode=None): 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_option_primary=os.getenv("HEDGE_PLAN_OPTION_PRIMARY", "true").lower() + in ("1", "true", "yes", "on"), hedge_plan_budget_buffer=float(os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"), options_trade_budget=OKX_OPTIONS_TRADE_BUDGET_USDC, options_budget_buffer=float(os.getenv("OKX_OPTIONS_BUDGET_BUFFER") or "0.95"), diff --git a/docs/对冲计划-以期权为主.md b/docs/对冲计划-以期权为主.md index 30948bc..1f1f719 100644 --- a/docs/对冲计划-以期权为主.md +++ b/docs/对冲计划-以期权为主.md @@ -1,14 +1,16 @@ # 对冲计划 · 永期「以期权为主」 -> 实现日:2026-08-09 · 在现有永期**保险模式**上增加计划级开关,不新增 `OKX_TRADE_MODE`。 +> 实现日:2026-08-09 · 在现有永期**保险模式**上增加子模式,不新增 `OKX_TRADE_MODE`。 +> 模式由 env `HEDGE_PLAN_OPTION_PRIMARY` 切换(默认 `true`),页面标题前显示标识,不可在页内切换。 ## 1. 模式对照 -| | 保险模式(开关关) | 以期权为主(开关开) | +| | 保险模式(`OPTION_PRIMARY=false`) | 以期权为主(`true`) | |--|------------------|-------------------| | UI 做多 | 永续多 + 买 Put | 买 Call + 永续空 | | UI 做空 | 永续空 + 买 Call | 买 Put + 永续多 | -| 左卡 | 开仓价 / 张数 / TP / SL | 权利金 / 杠杆 / 比例 / 到期h / 间隔 / 目标点数 | +| 左卡 | 开仓价 / 张数 / TP / SL | 资金与杠杆 / 选约条件 / 出场条件 三组 | +| 右卡 | 上永续行情 · 下期权链 | 同上;期权表含杠杆(指数÷卖一),按类型自动匹配 | | 选约 | 仅实值/平值 | 实/平/虚 + 间隔 + 杠杆门 | | 开仓 | 受 `HEDGE_PLAN_OPEN_ORDER` | **强制先期权**,成交后**立即市价**开永续(**不挂**交易所 TP/SL) | | 出场 | 交易所 TP/SL | 相对 K 的点数目标分叉 | @@ -60,7 +62,7 @@ contracts = perp_eth / contract_size | `hedge_plan_orders_lib.py` | 路径、开平永续、启动前定仓刷新 | | `hedge_plan_monitor_lib.py` | `_tick_po_option_primary*` | | `hedge_plan_register.py` / `hedge_plan_db.py` | preview/start/persist;`options-chain?option_primary&min_hours&strike_interval` | -| `hedge_plan.js` + `hedge_plan_panel.html` | 开关与左右卡 | +| `hedge_plan.js` + `hedge_plan_panel.html` | env 模式标识、左三组参数、右上永续/右下期权 | ## 6. 测试 diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 7abe93b..2f96525 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -37,7 +37,7 @@ ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0", ooCloseMode: "close_all", direction: "long", - optionPrimary: true, + optionPrimary: root.getAttribute("data-option-primary") !== "0", opLevTouched: false, opRatioTouched: false, tradingUsdc: null, @@ -283,14 +283,9 @@ function syncOptionPrimaryUI() { const on = isOptionPrimary(); - document.querySelectorAll(".hp-po-mode").forEach(function (b) { - const v = b.getAttribute("data-option-primary") === "1"; - b.classList.toggle("is-selected", v === on); - b.classList.toggle("active", v === on); - }); const ins = $("hp-po-fields-insurance"); const op = $("hp-po-fields-option-primary"); - // 保险模式只显示开仓价/张数/止盈止损;以期权为主只显示权利金等执行参数 + // 保险模式只显示开仓价/张数/止盈止损;以期权为主显示三组参数(env 切换,页内不可改) if (ins) { ins.classList.toggle("hidden", on); if (on) ins.setAttribute("hidden", "hidden"); @@ -303,48 +298,39 @@ else op.removeAttribute("hidden"); op.style.display = on ? "" : "none"; } - const otmBtn = document.querySelector(".hp-money-otm"); - if (otmBtn) { + const insMoney = $("hp-po-ins-money"); + if (insMoney) { + if (on) { + insMoney.setAttribute("hidden", "hidden"); + insMoney.classList.add("hidden"); + } else { + insMoney.removeAttribute("hidden"); + insMoney.classList.remove("hidden"); + } + } + document.querySelectorAll(".hp-money-otm").forEach(function (otmBtn) { otmBtn.classList.toggle("hidden", !on); if (!on) otmBtn.setAttribute("hidden", "hidden"); else otmBtn.removeAttribute("hidden"); - } + }); if (!on && state.moneyFilter === "otm") { state.moneyFilter = "itm"; syncMoneyUI(); } if (on) applyOpDefaultsFromMoney(false); - const title = $("hp-po-card-title"); - if (title) title.textContent = on ? "执行参数" : "永续"; - const rightQ = $("hp-po-perp-quote-right"); - if (rightQ) { - rightQ.classList.toggle("hidden", !on); - if (!on) rightQ.setAttribute("hidden", "hidden"); - else rightQ.removeAttribute("hidden"); + const badge = $("hp-po-mode-badge"); + if (badge) { + badge.textContent = on ? "以期权为主" : "保险模式"; + badge.classList.toggle("is-insurance", !on); } + const title = $("hp-po-card-title"); + if (title) title.textContent = "执行参数"; const dirLong = document.querySelector('.hp-po-dir[data-dir="long"]'); const dirShort = document.querySelector('.hp-po-dir[data-dir="short"]'); if (dirLong) dirLong.title = on ? "做多=买Call+永续空" : "做多永续"; if (dirShort) dirShort.title = on ? "做空=买Put+永续多" : "做空永续"; } - function setOptionPrimary(on, forceReload) { - const next = !!on; - const changed = next !== isOptionPrimary(); - state.optionPrimary = next; - if (changed) { - state.opLevTouched = false; - state.opRatioTouched = false; - state.selected = null; - if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; - } - syncOptionPrimaryUI(); - if (!changed && !forceReload) return; - void loadMarket().then(function () { - return loadChain(); - }); - } - function hoursFromExpMs(expMs) { const n = Number(expMs); if (!n || Number.isNaN(n)) return null; @@ -440,6 +426,37 @@ return (Math.round((k / a) * 10) / 10).toFixed(1) + "×"; } + /** 永期期权行情杠杆:指数÷卖一(与选约杠杆门一致). */ + function calcIndexAskLeverage(ask) { + const idx = indexPx(); + const a = Number(ask); + if (!(idx > 0) || !(a > 0)) return "—"; + return (Math.round((idx / a) * 10) / 10).toFixed(1) + "×"; + } + + function clearPoSelection() { + state.selected = null; + if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; + updatePremiumLine(); + updatePerpPnlHint(); + } + + /** 以期权为主:按类型/间隔/杠杆自动匹配最近合约. */ + function autoMatchPoOption(force) { + if (!isOptionPrimary()) return; + const want = optTypeForDirection(getDirection()); + const sel = state.selected; + const stillOk = + !force && + sel && + String(sel.opt_type || "").toUpperCase() === want && + matchesMoneyFilter(sel); + if (stillOk) return; + const c = pickClosestItmAtm(currentContracts("hp-exp-select"), want); + if (c) pickContract(c); + else clearPoSelection(); + } + function findAtmStrikeFromRows(rows, idx) { if (!rows.length) return null; if (idx == null || Number.isNaN(Number(idx))) return rows[0].strike; @@ -913,10 +930,6 @@ : ""); const q = $("hp-perp-quote"); if (q) q.innerHTML = quoteHtml; - const rightQ = $("hp-po-perp-quote-right"); - if (rightQ && isOptionPrimary()) { - rightQ.innerHTML = "永续行情 · " + quoteHtml; - } const contractsInput = $("hp-contracts"); if (contractsInput) { const step = amtPrec <= 0 ? "1" : String(Math.pow(10, -amtPrec)); @@ -1130,7 +1143,8 @@ const prevInst = state.selected && state.selected.inst_id; tbody.innerHTML = ""; if (!exp) { - tbody.innerHTML = '请选择到期日'; + tbody.innerHTML = '请选择到期日'; + if (isOptionPrimary()) clearPoSelection(); return; } const sameType = (exp.contracts || []).filter(function (c) { @@ -1158,7 +1172,8 @@ (idx ? " · 指数 " + fmt(idx, 2) : "") + " · 可换到期或点刷新链"; } - tbody.innerHTML = '' + hint + ""; + tbody.innerHTML = '' + hint + ""; + if (isOptionPrimary()) clearPoSelection(); return; } list.forEach(function (c) { @@ -1171,6 +1186,8 @@ c.strike + "" + moneynessBadge(c) + + '' + + calcIndexAskLeverage(c.ask) + '' + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + '' + @@ -1191,6 +1208,7 @@ pickContract(c); }); }); + autoMatchPoOption(false); } function updatePremiumLine() { @@ -1729,7 +1747,7 @@ b.addEventListener("click", function () { const m = b.getAttribute("data-money") || "itm"; if (m === "otm" && !isOptionPrimary()) { - alert("永期保险腿仅允许实值或平值;请先打开「以期权为主」"); + alert("永期保险腿仅允许实值或平值;请在 env 将 HEDGE_PLAN_OPTION_PRIMARY=true"); return; } state.moneyFilter = m === "otm" ? "otm" : m === "atm" ? "atm" : "itm"; @@ -1738,11 +1756,6 @@ renderListStrikes(); }); }); - document.querySelectorAll(".hp-po-mode").forEach(function (b) { - b.addEventListener("click", function () { - setOptionPrimary(b.getAttribute("data-option-primary") === "1", true); - }); - }); if ($("hp-opt-leverage")) { $("hp-opt-leverage").addEventListener("input", function () { state.opLevTouched = true; @@ -1789,6 +1802,13 @@ }); if ($("hp-recommend-opt")) { $("hp-recommend-opt").addEventListener("click", function () { + if (isOptionPrimary()) { + autoMatchPoOption(true); + if (!state.selected) { + alert("当前选约条件下无匹配合约,请调整类型/间隔/杠杆或换到期"); + } + return; + } const want = optTypeForDirection(getDirection()); const c = pickClosestItmAtm(currentContracts("hp-exp-select"), want); if (!c) { diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 3749967..ba843ba 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3470,6 +3470,34 @@ html[data-theme="light"] .opt-be-dist-down { font-size: 0.74rem; line-height: 1.4; } +.hedge-plan-page-wrap .hp-po-mode-badge { + display: inline-block; + margin-right: 6px; + padding: 2px 8px; + border-radius: 6px; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.02em; + color: #c4b5fd; + background: rgba(139, 92, 246, 0.22); + border: 1px solid rgba(167, 139, 250, 0.35); + vertical-align: middle; +} +.hedge-plan-page-wrap .hp-po-mode-badge.is-insurance { + color: #93c5fd; + background: rgba(59, 130, 246, 0.18); + border-color: rgba(96, 165, 250, 0.35); +} +.hedge-plan-page-wrap .hp-po-section { + margin: 8px 0 10px; +} +.hedge-plan-page-wrap .hp-po-section-title { + margin: 0 0 6px; + font-size: 0.8rem; + font-weight: 650; + color: #c5cdd9; + letter-spacing: 0.02em; +} .hedge-plan-page-wrap .hp-po-fields { display: grid; grid-template-columns: 1fr 1fr; @@ -3480,11 +3508,53 @@ html[data-theme="light"] .opt-be-dist-down { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.06); } +.hedge-plan-page-wrap .hp-po-fields--section { + margin: 0; +} +.hedge-plan-page-wrap .hp-po-field--type { + grid-column: 1 / -1; +} +.hedge-plan-page-wrap .hp-po-type-seg { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.hedge-plan-page-wrap .hp-po-right-stack { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + min-height: 0; +} +.hedge-plan-page-wrap .hp-po-quote-head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} +.hedge-plan-page-wrap .hp-po-ins-money { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; +} +.hedge-plan-page-wrap .hp-po-ins-money[hidden], +.hedge-plan-page-wrap .hp-po-ins-money.hidden { + display: none !important; +} /* display:grid 会盖掉 [hidden];模式切换必须显式 none */ .hedge-plan-page-wrap .hp-po-fields.hidden, -.hedge-plan-page-wrap .hp-po-fields[hidden] { +.hedge-plan-page-wrap .hp-po-fields[hidden], +.hedge-plan-page-wrap #hp-po-fields-option-primary.hidden, +.hedge-plan-page-wrap #hp-po-fields-option-primary[hidden] { display: none !important; } +.hedge-plan-page-wrap .hp-strike-table-wrap--6 { + max-height: 280px; + min-height: 200px; + overflow-y: auto; + margin-top: 4px; + flex: 1 1 auto; +} .hedge-plan-page-wrap .hp-po-field { display: flex; flex-direction: column; @@ -3731,6 +3801,7 @@ html[data-theme="light"] .opt-be-dist-down { margin-top: 4px; flex: 0 0 auto; } +.hedge-plan-page-wrap .opt-chain-lev, .hedge-plan-page-wrap .hp-oo-table-wrap .opt-chain-lev { text-align: center; white-space: nowrap; @@ -3747,12 +3818,20 @@ html[data-theme="light"] .opt-be-dist-down { align-items: stretch; margin-bottom: 28px; } -.hedge-plan-page-wrap .options-dual-grid > .card { +.hedge-plan-page-wrap .options-dual-grid > .card, +.hedge-plan-page-wrap .hp-po-right-stack > .card { height: 100%; min-height: 0; display: flex; flex-direction: column; } +.hedge-plan-page-wrap .hp-po-right-stack > .hp-po-perp-quote-card { + height: auto; + flex: 0 0 auto; +} +.hedge-plan-page-wrap .hp-po-right-stack > .hp-opt-card { + flex: 1 1 auto; +} .hedge-plan-page-wrap .hp-action-row { margin-top: 10px; gap: 8px; diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index 2375aa9..ec90c54 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -96,6 +96,7 @@ HOT_RELOAD_EXACT = frozenset({ "OKX_TRADE_MODE", "MAX_ACTIVE_HEDGE_PLANS", "HEDGE_PLAN_LIVE_ORDER", + "HEDGE_PLAN_OPTION_PRIMARY", "HEDGE_PLAN_OPEN_ORDER", "HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", @@ -154,6 +155,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = { ("perp_options", "永期对冲"), ("options_options", "期期对冲"), ), + "HEDGE_PLAN_OPTION_PRIMARY": ( + ("true", "以期权为主"), + ("false", "保险模式"), + ), } _SELECT_ALIASES: dict[str, dict[str, str]] = { diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index 9dff06e..48205a3 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -186,6 +186,11 @@ _HEDGE_COMMON_FIELDS: list[tuple[str, str, str]] = [ ] _HEDGE_PO_FIELDS: list[tuple[str, str, str]] = [ + ( + "HEDGE_PLAN_OPTION_PRIMARY", + "永期模式(以期权为主/保险)", + "默认 true=以期权为主;false=保险模式;页面标题前显示标识,不可在页内切换", + ), ("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"), ("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"), ("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"), @@ -263,6 +268,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = { "HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget", "HEDGE_PLAN_OO_BIAS_RATIO": "0.7", "HEDGE_PLAN_BUDGET_BUFFER": "0.95", + "HEDGE_PLAN_OPTION_PRIMARY": "true", "HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true", "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true", } diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index 1f115c1..1712324 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -5,6 +5,7 @@ data-show-perp="{{ '1' if hedge_plan_show_perp_options else '0' }}" data-show-oo="{{ '1' if hedge_plan_show_options_options else '0' }}" data-oo-close-mode-enabled="{{ '1' if hedge_plan_oo_close_mode_enabled else '0' }}" + data-option-primary="{{ '1' if hedge_plan_option_primary|default(true) else '0' }}" data-budget-buffer="{{ hedge_plan_budget_buffer | default(0.95) }}" data-sizing-mode="{{ position_sizing_mode | default('risk') }}" data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}"> @@ -43,31 +44,31 @@
-

执行参数 · ETH 合约账户

+

+ 以期权为主 + 执行参数 + · ETH + 合约账户 +

规则说明

账户:永续腿走合约账户(USDT);期权腿走期权账户(USDC)。两账户分开下单、资金不互通。

-

保险模式(开关关):做多配 Put、做空配 Call;左填开仓/止盈止损;仅实值/平值;交易所 TP/SL 出场。

-

以期权为主(开关开):做多买 Call+永续空、做空买 Put+永续多;左填权利金/杠杆/比例/目标点数;开仓先期权后市价永续;期权目标验买一且净利>0后双平;永续目标只平永续、期权持有至到期。

+

模式:在 env HEDGE_PLAN_OPTION_PRIMARY 切换(true=以期权为主 / false=保险模式);标题前标识当前模式。

+

保险模式:做多配 Put、做空配 Call;左填开仓/止盈止损;仅实值/平值;交易所 TP/SL 出场。

+

以期权为主:做多买 Call+永续空、做空买 Put+永续多;左分资金/选约/出场三组;右侧上永续下期权(含杠杆),按类型自动匹配;开仓先期权后市价永续。

-
- - -
- 标记 —
-

加载中…

-
- - - - - - - - +
+
+

资金与杠杆配置

+
+ + + +
+
+
+

选约条件

+
+ + +
+ 类型 +
+ + + +
+
+ +
+
+
+

出场条件

+
+ + +
+
-
-

期权 · Call 期权账户

-

-
- - - - - - - 指数 — +
+
+

永续行情 合约账户

+
+ 标记 — +
+

加载中…

+
-
- - - - - - - - - - - - - -
行权价实虚值卖一/张买一/张操作
请刷新期权链
-
-
- - - -
-
-
- +
+

期权 · Call 期权账户

+
+ + + + + 指数 — +
+
+ + + + + + + + + + + + + + +
行权价实虚值杠杆卖一/张买一/张操作
请刷新期权链
+
+
+ + + +
+
+
+ +
@@ -368,4 +403,4 @@
- +