diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index f296605..b50a5e7 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -1002,23 +1002,46 @@ } function setGateLine(gates) { + const text = (function () { + if (!gates) return ""; + const parts = [ + "计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"), + "测算:" + (gates.can_preview ? "可" : "否"), + "开仓:" + (gates.can_start ? "可" : "否"), + ]; + if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; ")); + return parts.join(" · "); + })(); const el = $("hp-gate-line"); - if (!el) return; - if (!gates) { - el.textContent = ""; - return; - } - const parts = [ - "计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"), - "测算:" + (gates.can_preview ? "可" : "否"), - "开仓:" + (gates.can_start ? "可" : "否"), - ]; - if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; ")); - el.textContent = parts.join(" · "); - state.canStart = !!gates.can_start; + if (el) el.textContent = text; + const inline = $("hp-po-gate-inline"); + if (inline) inline.textContent = text; + state.canStart = !!(gates && gates.can_start); syncPreviewStartBtn(); } + function setPoOrderErr(msg, focusId) { + const err = $("hp-po-order-err"); + if (err) { + if (msg) { + err.textContent = msg; + err.removeAttribute("hidden"); + } else { + err.textContent = ""; + err.setAttribute("hidden", ""); + } + } + if (focusId && $(focusId)) { + try { + $(focusId).focus(); + $(focusId).classList.add("hp-input-invalid"); + setTimeout(function () { + if ($(focusId)) $(focusId).classList.remove("hp-input-invalid"); + }, 2400); + } catch (e) {} + } + } + function syncPreviewStartBtn() { const start = $("hp-preview-start"); if (!start) return; @@ -3150,19 +3173,41 @@ } else { if (!state.selected) throw new Error("请选用期权腿"); if (isOptionPrimary()) { + setPoOrderErr(""); const sized = currentOpSizing(); - if (!sized || !(sized.sheets > 0)) throw new Error("请选择期权并填写张数"); + if (!sized || !(sized.sheets > 0)) { + setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets"); + throw new Error("请选择期权并填写张数(≥1)"); + } const exitMode = state.exitMode === "target" ? "target" : "profit"; let optPts = 0; let perpPts = 0; let profitTarget = 0; if (exitMode === "profit") { profitTarget = numInput("hp-profit-target", NaN); - if (!(profitTarget > 0)) throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)"); + if (!(profitTarget > 0)) { + setPoOrderErr("请填写目标盈利金额(USDT,扣双边手续费后净利)", "hp-profit-target"); + throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)"); + } } else { optPts = numInput("hp-opt-target-pts", NaN); perpPts = numInput("hp-perp-target-pts", NaN); - if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)"); + if (!(optPts > 0) || !(perpPts > 0)) { + setPoOrderErr("请填写期权/永续目标位点数(须大于0)", !(optPts > 0) ? "hp-opt-target-pts" : "hp-perp-target-pts"); + throw new Error("请填写期权/永续目标位点数(须大于0)"); + } + } + if (!(state.market && state.market.exchange_symbol)) { + setPoOrderErr("永续行情未就绪,请先点「刷新行情」"); + throw new Error("永续行情未就绪,请先点「刷新行情」"); + } + if (!state.canStart) { + const gateTxt = + ($("hp-po-gate-inline") && $("hp-po-gate-inline").textContent) || + ($("hp-gate-line") && $("hp-gate-line").textContent) || + "请先刷新行情/检查账户"; + setPoOrderErr("门禁未满足,不可开仓: " + gateTxt); + throw new Error("门禁未满足,不可开仓: " + gateTxt); } const exp = currentExp("hp-exp-select"); const entry = indexPx() || Number((state.market && state.market.mark) || 0); @@ -3174,6 +3219,7 @@ plan_type: "perp_options", option_primary: true, market_order: 1, + watch_entry: 0, exit_mode: exitMode, underlying: state.underlying, direction: getDirection(), diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index ae69829..f56de5d 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3454,12 +3454,66 @@ html[data-theme="light"] .opt-be-dist-down { .hedge-plan-page-wrap .hp-oo-size-mode.is-selected .hp-oo-check, .hedge-plan-page-wrap .hp-oo-close-mode.is-selected .hp-oo-check, .hedge-plan-page-wrap .hp-po-dir.is-selected .hp-oo-check, +.hedge-plan-page-wrap .hp-po-exit-mode.is-selected .hp-oo-check, .hedge-plan-page-wrap .hp-oo-size-mode.active .hp-oo-check, .hedge-plan-page-wrap .hp-oo-close-mode.active .hp-oo-check, -.hedge-plan-page-wrap .hp-po-dir.active .hp-oo-check { +.hedge-plan-page-wrap .hp-po-dir.active .hp-oo-check, +.hedge-plan-page-wrap .hp-po-exit-mode.active .hp-oo-check { display: inline; color: var(--accent, #00d4ff); } +.hedge-plan-page-wrap .hp-po-exit-mode .hp-oo-check { + display: none; + margin-right: 4px; + font-weight: 700; +} +.hedge-plan-page-wrap .hp-po-exit-mode.is-selected, +.hedge-plan-page-wrap .hp-po-exit-mode.active { + border-color: var(--accent, #00d4ff); + color: var(--text, #fff); + background: rgba(0, 212, 255, 0.16); + box-shadow: inset 0 0 0 1px rgba(0, 212, 255, 0.35); + font-weight: 700; +} +.hedge-plan-page-wrap .hp-page-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin: 0 0 12px; +} +.hedge-plan-page-wrap .hp-po-gate-inline { + font-size: 0.82rem; + line-height: 1.35; +} +.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-quote-compact { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + align-items: baseline; + font-size: 0.84rem; +} +.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-quote-one { + margin: 2px 0 0; + font-size: 0.78rem; +} +.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-order-calc { + margin-top: 4px; +} +.hedge-plan-page-wrap .hp-po-order-err { + margin: 6px 0 0; + font-size: 0.82rem; +} +.hedge-plan-page-wrap .hp-input-invalid { + border-color: #e85d5d !important; + box-shadow: 0 0 0 1px rgba(232, 93, 93, 0.45); +} +.hedge-plan-page-wrap .hp-po-field--wide { + grid-column: 1 / -1; +} +.opt-order-dialog.hp-po-order-dialog { + width: min(96vw, 560px); +} .hedge-plan-page-wrap .hp-po-top { display: flex; flex-wrap: wrap; diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py index 881cc0e..f679255 100644 --- a/lib/hedge_plan/hedge_plan_orders_lib.py +++ b/lib/hedge_plan/hedge_plan_orders_lib.py @@ -1180,11 +1180,13 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]: if is_option_primary(body): from lib.hedge_plan.hedge_plan_option_primary_lib import validate_option_primary_watch - # 以期权为主默认盯盘启动(非现场开仓);显式 watch_entry=0 才走即开校验 + # 手动下单(market_order)或显式 watch_entry=0 → 即开校验;否则盯盘 + market = body.get("market_order") in (True, 1, "1", "true", "yes", "on") watch = body.get("watch_entry") - if watch in (None, "", True, 1, "1", "true", "yes", "on"): - return validate_option_primary_watch(body) - return validate_option_primary_start(body) + watch_off = watch in (False, 0, "0", "false", "no", "off") + if market or watch_off: + return validate_option_primary_start(body) + return validate_option_primary_watch(body) need = ("direction", "entry", "tp", "sl", "contracts", "opt_inst_id", "sheets", "exchange_symbol") for k in need: if body.get(k) in (None, ""): diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 3fef8e9..3977a90 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -863,12 +863,16 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: body["leverage"] = 100 else: body["leverage"] = int(cfg.get("alt_leverage") or 5) - # 以期权为主:策略启动=盯盘,不现场开仓 + # 以期权为主:默认盯盘;手动下单(market_order)或 watch_entry=0 则现场开仓 if plan_type == "perp_options": from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary + market = body.get("market_order") in (True, 1, "1", "true", "yes", "on") watch = body.get("watch_entry") - watch_on = watch in (None, "", True, 1, "1", "true", "yes", "on") + watch_off = watch in (False, 0, "0", "false", "no", "off") + watch_on = (not market) and (not watch_off) and ( + watch in (None, "", True, 1, "1", "true", "yes", "on") + ) if is_option_primary(body) and watch_on: if dry_run: return jsonify( diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index 6e67db8..8ea165c 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -21,30 +21,37 @@
永期与期期 Tab 均已隐藏:请在 env「期权/对冲模式」切换交易模式.
{% endif %} -
-
-

对冲计划 测算 · 下单 - 期权开平仓与监控说明 -

- -
-
- {% if hedge_plan_show_perp_options %} - - {% endif %} - {% if hedge_plan_show_options_options %} - - {% endif %} -
-

-

永续腿→合约账户 · 期权腿→期权账户

+ {# 顶栏「对冲计划」已改为导航「永期/期期对冲」;页内不再重复大标题 #} +
+ +
+ + +
- +

手动选约 · Call 币本位

+
+ 规则说明 +
+

手动选约:左侧选 Call/Put 与到期,点「选择」弹窗下单。买 Call → 永续空;买 Put → 永续多。比例默认 2(期权名义÷比例=永续名义)。

+

手动下单:开仓按卖一限价(非交易所市价)。先开期权(按张数买币并+5%滑点),再开永续。永续杠杆取 env:BTC=BTC_LEVERAGE/ETH=ALT_LEVERAGE。

+

出场:盈利金额(扣开平双边手续费后净利)或目标位点数二选一。平仓先期权(须买一流动性)再永续,再把币全部卖回 USDT。一账户同时仅一笔对冲仓。

+

开仓校验:已选合约 · 张数≥1 · 出场条件已填 · 门禁「开仓:可」· 账户有足够交易 USDT。

+

买 Call + 永续空 · 点「选择」弹窗下单。

+
+
@@ -68,7 +75,6 @@ 指数 —
-

买 Call + 永续空 · 点「选择」弹窗下单;比例默认 2(期权 1 → 永续 0.5)。

@@ -95,7 +101,7 @@
- +