From c9e69ce8ef677af188601be0ebea9f4978bc6e5b Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 15:19:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B0=B8=E6=9C=9F=E6=94=B9=E4=B8=BA=E5=B8=82?= =?UTF-8?q?=E4=BB=B7=E9=80=89=E7=BA=A6:=E5=B7=A6=E5=90=8C=E6=9C=9F?= =?UTF-8?q?=E6=9D=83=E4=BA=BA=E5=B7=A5=E9=80=89,=E5=8F=B3=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E9=80=89=E7=BA=A6=E6=9D=A1=E4=BB=B6.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 左侧到期/CallPut/全部实虚值/展开全部;右侧仅资金杠杆与出场;下单走已选合约立即开仓而非条件盯盘. Co-authored-by: Cursor --- lib/common/static/hedge_plan.js | 263 ++++++++++++------ lib/common/static/instance_theme.css | 32 ++- .../hedge_plan_option_primary_lib.py | 31 ++- .../templates/hedge_plan_panel.html | 75 ++--- 4 files changed, 257 insertions(+), 144 deletions(-) diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 9a61e86..613e355 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -22,10 +22,11 @@ tab: pickDefaultTab(), mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options", underlying: root.getAttribute("data-default-underly") || "ETH", - moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "otm" : "itm", + moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "all" : "itm", ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值 ooRecommend: null, // atm_straddle | double_otm | null ooStrikeExpandAll: false, // 默认 Call/Put 各 3 档 + poStrikeExpandAll: false, // 永期市价选约:默认平值+实3+虚3 chain: null, selected: null, legA: null, @@ -68,7 +69,9 @@ const hint = $("hp-po-pair-hint"); if (hint) { if (isOptionPrimary()) { - hint.textContent = dir === "short" ? "买 Put + 永续多" : "买 Call + 永续空"; + hint.textContent = + (dir === "short" ? "买 Put + 永续多" : "买 Call + 永续空") + + " · 点「选择」填入右侧市价下单;比例默认 2(期权 1 → 永续 0.5)。"; } else { hint.textContent = dir === "short" ? "永续空 + 保险 Call" : "永续多 + 保险 Put"; } @@ -162,9 +165,9 @@ return fmt(v, 2) + ":1"; } - /** 列表/盯盘候选杠杆门槛(与后端 effective_min_opt_leverage 对齐). */ + /** 条件单杠杆门槛;市价下单传 0 则不筛. */ function optionPrimaryMinLev() { - const minLev = numInput("hp-opt-leverage", opMoneyKind() === "otm" ? 200 : 100); + const minLev = numInput("hp-opt-leverage", 0); if (!(minLev > 0)) return 0; if (opMoneyKind() === "otm") return Math.max(minLev, 180); return minLev; @@ -180,17 +183,17 @@ } function matchesMoneyFilter(c) { - const f = state.moneyFilter || "itm"; + const f = state.moneyFilter || "all"; const m = (c.moneyness || "").toLowerCase(); + // 保险模式不可选虚值 if (!isOptionPrimary() && f === "otm") return false; - // 列表:间隔+虚实值+杠杆门槛(达标才显示;启动盯盘后监控同样门槛) + // 市价下单:仅按 全部/实值/虚值 筛选,不做间隔/杠杆条件门槛 if (isOptionPrimary()) { - const idx = indexPx(); - const interval = numInput("hp-strike-interval", 15); - if (idx && interval > 0 && Math.abs(Number(c.strike) - idx) > interval + 1e-9) { - return false; - } - if (!optionPrimaryLevOk(c)) return false; + if (f === "all" || f === "any") return true; + if (f === "itm") return m === "itm" || m === "atm"; + if (f === "atm") return m === "atm"; + if (f === "otm") return m === "otm"; + return true; } if (f === "itm") return m === "itm" || m === "atm"; if (f === "atm") return m === "atm"; @@ -198,6 +201,31 @@ return m === "itm" || m === "atm"; } + const PO_ATM_SIDE = 3; + + function slicePoListByMoneyness(list) { + if (state.poStrikeExpandAll || !list.length) return list; + const sorted = list.slice().sort(function (a, b) { + return Number(a.strike) - Number(b.strike); + }); + const itm = []; + const atm = []; + const otm = []; + sorted.forEach(function (c) { + const m = String(c.moneyness || "").toLowerCase(); + if (m === "atm") atm.push(c); + else if (m === "itm") itm.push(c); + else if (m === "otm") otm.push(c); + }); + const n = PO_ATM_SIDE; + const isPut = optTypeForDirection(getDirection()) === "P"; + const pickedItm = isPut ? itm.slice(0, n) : itm.slice(-n); + const pickedOtm = isPut ? otm.slice(-n) : otm.slice(0, n); + return pickedItm.concat(atm, pickedOtm).sort(function (a, b) { + return Number(a.strike) - Number(b.strike); + }); + } + function matchesOoMoneyFilter(c) { // 期期:仅平值/虚值(禁实值) if (!c) return false; @@ -280,19 +308,15 @@ } function opMoneyKind() { - const f = state.moneyFilter || "itm"; + const f = state.moneyFilter || "all"; if (f === "otm") return "otm"; if (f === "atm") return "atm"; + if (f === "all" || f === "any") return "all"; return "itm"; } function applyOpDefaultsFromMoney(force) { - const kind = opMoneyKind(); - const levEl = $("hp-opt-leverage"); const ratioEl = $("hp-opt-perp-ratio"); - if (levEl && (force || !state.opLevTouched)) { - levEl.value = kind === "otm" ? "200" : "100"; - } if (ratioEl && (force || !state.opRatioTouched)) { // 默认 2:期权名义÷比例=永续名义(例 1 BTC 期权 → 0.5 BTC 永续) ratioEl.value = "2"; @@ -326,14 +350,28 @@ insMoney.classList.remove("hidden"); } } - if (!on && state.moneyFilter === "otm") { + const mktMoney = $("hp-po-mkt-money"); + if (mktMoney) { + if (on) { + mktMoney.removeAttribute("hidden"); + mktMoney.classList.remove("hidden"); + } else { + mktMoney.setAttribute("hidden", "hidden"); + mktMoney.classList.add("hidden"); + } + } + if (on && (state.moneyFilter === "atm" || !state.moneyFilter)) { + state.moneyFilter = "all"; + syncMoneyUI(); + } + if (!on && (state.moneyFilter === "otm" || state.moneyFilter === "all")) { state.moneyFilter = "itm"; syncMoneyUI(); } if (on) applyOpDefaultsFromMoney(false); const badge = $("hp-po-mode-badge"); if (badge) { - badge.textContent = on ? "以期权为主" : "保险模式"; + badge.textContent = on ? "市价下单" : "保险模式"; badge.classList.toggle("is-insurance", !on); } const title = $("hp-po-card-title"); @@ -343,7 +381,7 @@ if (dirLong) { dirLong.title = on ? "买 Call → 永续空" : "做多永续"; if (on) { - dirLong.innerHTML = 'Call'; + dirLong.innerHTML = '看涨 Call'; } else { dirLong.innerHTML = '做多'; } @@ -351,7 +389,7 @@ if (dirShort) { dirShort.title = on ? "买 Put → 永续多" : "做空永续"; if (on) { - dirShort.innerHTML = 'Put'; + dirShort.innerHTML = '看跌 Put'; } else { dirShort.innerHTML = '做空'; } @@ -472,7 +510,7 @@ function syncMoneyUI() { const moneySel = $("hp-money-select"); if (moneySel && isOptionPrimary()) { - moneySel.value = state.moneyFilter === "otm" ? "otm" : state.moneyFilter === "atm" ? "atm" : "itm"; + moneySel.value = state.moneyFilter || "all"; } document.querySelectorAll(".hp-money-btn").forEach(function (b) { const on = b.getAttribute("data-money") === state.moneyFilter; @@ -486,14 +524,16 @@ b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); + const expandCb = $("hp-strike-expand-all"); + if (expandCb) expandCb.checked = !!state.poStrikeExpandAll; } function syncPoActionBtn() { const btn = $("hp-preview-btn"); if (!btn) return; if (isOptionPrimary()) { - btn.textContent = "策略启动"; - btn.title = "按参数启动盯盘;杠杆/间隔达标后自动开仓(非现场开)"; + btn.textContent = "市价下单"; + btn.title = "按左侧已选合约立即开仓(买期权+对冲永续);非条件盯盘"; } else { btn.textContent = "计算"; btn.title = "情景测算后再启动"; @@ -597,7 +637,7 @@ updatePerpPnlHint(); } - /** 以期权为主:按类型/间隔自动匹配最近合约. */ + /** 市价选约:不自动代选;仅在已选失效(换 Call/Put 或筛掉)时清空. */ function autoMatchPoOption(force) { if (!isOptionPrimary()) return; const want = optTypeForDirection(getDirection()); @@ -608,9 +648,7 @@ 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(); + clearPoSelection(); } function findAtmStrikeFromRows(rows, idx) { @@ -1199,9 +1237,8 @@ function fillExpSelect(sel, chain) { if (!sel) return; const prev = sel.value; - const isPoSel = sel.id === "hp-exp-select"; - // 永期以期权为主:到期下拉按「最低剩余小时」过滤;期期下拉不过滤,保证明天到期可见 - const minH = isPoSel && isOptionPrimary() ? numInput("hp-min-hours", 36) : 0; + // 市价选约:到期列表不过滤(与期权页一致,默认最近一期);条件单 hidden 字段 min-hours=0 + const minH = sel.id === "hp-exp-select" ? numInput("hp-min-hours", 0) : 0; sel.innerHTML = ''; let firstOk = null; let skippedNear = 0; @@ -1222,10 +1259,10 @@ if (!sel.value && firstOk) { sel.value = String(firstOk.exp_time); } - if (isPoSel && skippedNear > 0 && minH > 0) { + if (skippedNear > 0 && minH > 0) { const tip = document.createElement("option"); tip.disabled = true; - tip.textContent = "(已隐藏 " + skippedNear + " 个不足 " + minH + "h 的到期 · 可改左侧最低剩余小时)"; + tip.textContent = "(已隐藏 " + skippedNear + " 个不足 " + minH + "h 的到期)"; sel.appendChild(tip); } } @@ -1270,7 +1307,7 @@ return; } if (isOptionPrimary() && !matchesMoneyFilter(c)) { - alert("不符合当前间隔/虚实值/杠杆门槛"); + alert("不符合当前筛选(全部/实值/虚值),请切换筛选后再选"); return; } state.selected = c; @@ -1300,38 +1337,38 @@ const want = optTypeForDirection(getDirection()); const exp = currentExp("hp-exp-select"); const prevInst = state.selected && state.selected.inst_id; + const cols = isOptionPrimary() ? 7 : 6; tbody.innerHTML = ""; if (!exp) { - tbody.innerHTML = '请选择到期日'; + tbody.innerHTML = '请选择到期日'; if (isOptionPrimary()) clearPoSelection(); return; } const sameType = (exp.contracts || []).filter(function (c) { return String(c.opt_type || "").toUpperCase() === want; }); - const list = sameType.filter(matchesMoneyFilter); + let list = sameType.filter(matchesMoneyFilter); + if (isOptionPrimary() && state.moneyFilter === "all") { + list = slicePoListByMoneyness(list); + } if (!list.length) { const idx = indexPx(); - const interval = isOptionPrimary() ? numInput("hp-strike-interval", 15) : null; let hint = "无匹配合约"; - if (isOptionPrimary() && sameType.length) { + if (sameType.length) { hint = - "无匹配" + + "当前筛选下无 " + (want === "C" ? "Call" : "Put") + - "(已滤 " + + "(链上 " + sameType.length + - " 档)·检查间隔" + - (interval != null ? "≤" + interval : "") + - "点/虚实值/杠杆≥" + - optionPrimaryMinLev(); - } else if (!sameType.length) { + " 档)·可改全部/实值/虚值或展开全部"; + } else { hint = "该到期无 " + (want === "C" ? "Call" : "Put") + (idx ? " · 指数 " + fmt(idx, 2) : "") + " · 可换到期或点刷新链"; } - tbody.innerHTML = '' + hint + ""; + tbody.innerHTML = '' + hint + ""; if (isOptionPrimary()) clearPoSelection(); return; } @@ -1340,24 +1377,53 @@ tr.className = "opt-strike-row" + (c.moneyness ? " opt-row-" + c.moneyness : ""); if (prevInst && c.inst_id === prevInst) tr.classList.add("opt-row-selected"); tr.setAttribute("data-inst", c.inst_id); - tr.innerHTML = - "" + - c.strike + - "" + - moneynessBadge(c) + - '' + - calcIndexAskLeverage(c.ask) + - '' + - fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + - '' + - fmtPxSz(c.bid, c.bid_sz, false, c.tick_sz) + - ''; + if (isOptionPrimary()) { + tr.innerHTML = + "" + + c.strike + + "" + + moneynessBadge(c) + + "" + + (c.inst_id || "") + + '' + + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + + '' + + calcIndexAskLeverage(c.ask) + + '' + + fmtPxSz(c.bid, c.bid_sz, false, c.tick_sz) + + ''; + } else { + tr.innerHTML = + "" + + c.strike + + "" + + moneynessBadge(c) + + '' + + calcIndexAskLeverage(c.ask) + + '' + + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + + '' + + fmtPxSz(c.bid, c.bid_sz, false, c.tick_sz) + + ''; + } tbody.appendChild(tr); }); + if (isOptionPrimary() && !state.poStrikeExpandAll && state.moneyFilter === "all") { + const hintTr = document.createElement("tr"); + hintTr.innerHTML = + '默认显示平值 + 实值3档 + 虚值3档 · 勾选「展开全部」查看该到期更多行权价'; + tbody.appendChild(hintTr); + } tbody.querySelectorAll(".hp-pick").forEach(function (btn) { btn.addEventListener("click", function () { const inst = btn.getAttribute("data-inst"); @@ -1707,14 +1773,20 @@ if (!state.selected) throw new Error("请选用期权腿"); if (isOptionPrimary()) { const sized = currentOpSizing(); - if (!sized) throw new Error("请填写权利金或张数并确认卖一有效"); + if (!sized) throw new Error("请选择期权并填写张数(或权利金预算)"); const optPts = numInput("hp-opt-target-pts", NaN); const perpPts = numInput("hp-perp-target-pts", NaN); if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)"); const exp = currentExp("hp-exp-select"); + const premEst = + Number(state.selected.ask || 0) * + sized.sheets * + Number(state.selected.ct_mult || 0.01); + const premBudget = numInput("hp-premium-budget", 0); body = { plan_type: "perp_options", option_primary: true, + market_order: 1, direction: getDirection(), entry: indexPx() || Number((state.market && state.market.mark) || 0), contracts: sized.contracts, @@ -1725,15 +1797,15 @@ ct_mult: state.selected.ct_mult || 0.01, ask: state.selected.ask, index_px: indexPx() || 0, - premium_budget: numInput("hp-premium-budget", 0), + premium_budget: premBudget > 0 ? premBudget : Math.max(premEst / 0.95, premEst), option_perp_ratio: numInput("hp-opt-perp-ratio", 2), option_target_points: optPts, perp_target_points: perpPts, - strike_interval: numInput("hp-strike-interval", 15), - min_option_hours: numInput("hp-min-hours", 36), - option_leverage: numInput("hp-opt-leverage", 100), + strike_interval: 0, + min_option_hours: 0, + option_leverage: 0, leverage: numInput("hp-perp-leverage", defaultPerpLeverage(state.underlying)), - moneyness: opMoneyKind(), + moneyness: (state.selected.moneyness || opMoneyKind() || "all").toLowerCase(), hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null, }; } else { @@ -1914,25 +1986,28 @@ }); document.querySelectorAll(".hp-money-btn").forEach(function (b) { b.addEventListener("click", function () { - const m = b.getAttribute("data-money") || "itm"; + const m = b.getAttribute("data-money") || "all"; if (m === "otm" && !isOptionPrimary()) { alert("永期保险腿仅允许实值或平值;请在 env 将 HEDGE_PLAN_OPTION_PRIMARY=true"); return; } - state.moneyFilter = m === "otm" ? "otm" : m === "atm" ? "atm" : "itm"; + if (isOptionPrimary()) { + state.moneyFilter = m === "otm" ? "otm" : m === "itm" ? "itm" : m === "atm" ? "atm" : "all"; + } else { + state.moneyFilter = m === "atm" ? "atm" : "itm"; + } if (isOptionPrimary()) applyOpDefaultsFromMoney(false); syncMoneyUI(); renderListStrikes(); }); }); - if ($("hp-money-select")) { - $("hp-money-select").addEventListener("change", function () { - const m = $("hp-money-select").value || "otm"; - state.moneyFilter = m === "otm" ? "otm" : m === "atm" ? "atm" : "itm"; - state.opLevTouched = false; - state.opRatioTouched = false; - applyOpDefaultsFromMoney(true); - syncMoneyUI(); + if ($("hp-strike-expand-all")) { + $("hp-strike-expand-all").addEventListener("change", function () { + state.poStrikeExpandAll = !!$("hp-strike-expand-all").checked; + if (state.poStrikeExpandAll && state.moneyFilter !== "all" && isOptionPrimary()) { + state.moneyFilter = "all"; + syncMoneyUI(); + } renderListStrikes(); }); } @@ -2154,7 +2229,8 @@ if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () { state.mode = "perp_options"; - if (isOptionPrimary()) void startOptionPrimaryWatch(); + // 以期权为主 = 市价下单(左侧人工选约后立即开);条件盯盘不再从此入口 + if (isOptionPrimary()) void startPlan("perp_options", false); else void runPreview(); }); if ($("hp-preview-btn-oo")) @@ -2744,15 +2820,21 @@ if (!state.selected) throw new Error("请选用期权腿"); if (isOptionPrimary()) { const sized = currentOpSizing(); - if (!sized) throw new Error("请填写权利金或张数并确认卖一有效"); + if (!sized) throw new Error("请选择期权并填写张数(或权利金预算)"); const optPts = numInput("hp-opt-target-pts", NaN); const perpPts = numInput("hp-perp-target-pts", NaN); if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)"); const exp = currentExp("hp-exp-select"); const entry = indexPx() || Number((state.market && state.market.mark) || 0); + const premEst = + Number(state.selected.ask || 0) * + sized.sheets * + Number(state.selected.ct_mult || 0.01); + const premBudget = numInput("hp-premium-budget", 0); body = { plan_type: "perp_options", option_primary: true, + market_order: 1, underlying: state.underlying, direction: getDirection(), entry: entry, @@ -2767,14 +2849,14 @@ index_px: entry, exchange_symbol: (state.market && state.market.exchange_symbol) || "", leverage: numInput("hp-perp-leverage", defaultPerpLeverage(state.underlying)), - option_leverage: numInput("hp-opt-leverage", 100), - premium_budget: numInput("hp-premium-budget", 0), + option_leverage: 0, + premium_budget: premBudget > 0 ? premBudget : Math.max(premEst / 0.95, premEst), option_perp_ratio: numInput("hp-opt-perp-ratio", 2), option_target_points: optPts, perp_target_points: perpPts, - strike_interval: numInput("hp-strike-interval", 15), - min_option_hours: numInput("hp-min-hours", 36), - moneyness: opMoneyKind(), + strike_interval: 0, + min_option_hours: 0, + moneyness: (state.selected.moneyness || opMoneyKind() || "all").toLowerCase(), hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null, }; } else { @@ -2807,7 +2889,14 @@ } } if (!fromPreviewModal) { - if (!window.confirm("确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)")) return; + const confMsg = isOptionPrimary() + ? "确认市价下单?\n期权 " + + (state.selected && state.selected.inst_id) + + " × " + + (body.sheets || "") + + " 张 + 永续对冲\n(期权腿币本位 / 永续腿合约账户)" + : "确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)"; + if (!window.confirm(confMsg)) return; } if (startBtn) startBtn.disabled = true; const d = await apiJson("/api/hedge-plan/start", { diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index f2e5ea3..57e4602 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3931,16 +3931,38 @@ html[data-theme="light"] .opt-be-dist-down { flex: 1 1 auto; min-height: 220px; } -.hedge-plan-page-wrap .hp-po-pair-hint { - margin-left: 8px; - font-size: 0.82rem; - white-space: nowrap; -} +.hedge-plan-page-wrap .hp-po-pair-hint, .hedge-plan-page-wrap .hp-po-chain-hint { margin: 4px 0 8px; font-size: 0.78rem; line-height: 1.4; } +.hedge-plan-page-wrap .hp-po-opt-toolbar { + flex-wrap: wrap; + gap: 6px 8px; + align-items: center; +} +.hedge-plan-page-wrap .hp-po-mkt-money { + display: inline-flex; + gap: 4px; + flex-wrap: wrap; +} +.hedge-plan-page-wrap .hp-po-mkt-money[hidden], +.hedge-plan-page-wrap .hp-po-mkt-money.hidden { + display: none !important; +} +.hedge-plan-page-wrap .hp-po-expand-label { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.8rem; + white-space: nowrap; + cursor: pointer; +} +.hedge-plan-page-wrap .opt-inst-code { + font-size: 0.72rem; + word-break: break-all; +} .hedge-plan-page-wrap .hp-po-order-card .hp-po-perp-quote-card { height: auto; flex: 0 0 auto; diff --git a/lib/hedge_plan/hedge_plan_option_primary_lib.py b/lib/hedge_plan/hedge_plan_option_primary_lib.py index efeaa27..67fff4e 100644 --- a/lib/hedge_plan/hedge_plan_option_primary_lib.py +++ b/lib/hedge_plan/hedge_plan_option_primary_lib.py @@ -266,7 +266,10 @@ def validate_option_primary_moneyness( return "行权价或指数无效" m_want = (moneyness or "atm").strip().lower() m_got = classify_moneyness(opt_type=o, strike=k, index_px=s) - if m_want == "itm": + if m_want in ("all", "any", ""): + # 市价人工选约:不限制虚实值 + pass + elif m_want == "itm": if not is_itm_or_atm(opt_type=o, strike=k, index_px=s): return "所选须为实值或平值" elif m_want == "atm": @@ -280,17 +283,27 @@ def validate_option_primary_moneyness( else: return "期权类型(实/平/虚)无效" - interval = float(_sf(strike_interval) or DEFAULT_STRIKE_INTERVAL) + # 间隔/最短到期/杠杆门槛:仅条件单使用;传 0 表示市价下单跳过 + interval_raw = _sf(strike_interval) + if interval_raw is None: + interval_raw = DEFAULT_STRIKE_INTERVAL + interval = float(interval_raw) if interval > 0 and abs(k - s) > interval + 1e-9: return f"行权价偏离指数 {abs(k - s):.1f} > 间隔 {interval:.0f}" - min_h = float(_sf(min_hours) or DEFAULT_MIN_HOURS) + min_h_raw = _sf(min_hours) + if min_h_raw is None: + min_h_raw = DEFAULT_MIN_HOURS + min_h = float(min_h_raw) h = _sf(hours_to_expiry) if min_h > 0 and h is not None and h < min_h: return f"剩余到期约 {h:.1f}h,低于最短 {min_h:.0f}h" a = _sf(ask) - min_lev = effective_min_opt_leverage(m_want if m_want != "atm" else m_got or "atm", min_opt_leverage) + lev_cfg = _sf(min_opt_leverage) + if lev_cfg is not None and lev_cfg <= 0: + return None + min_lev = effective_min_opt_leverage(m_want if m_want not in ("atm", "all", "any", "") else (m_got or "atm"), min_opt_leverage) if min_lev > 0 and a is not None and a > 0: lev = s / a if lev < min_lev: @@ -336,24 +349,27 @@ def validate_option_primary_watch(body: dict[str, Any]) -> Optional[str]: def validate_option_primary_start(body: dict[str, Any]) -> Optional[str]: + market = body.get("market_order") in (True, 1, "1", "true", "yes", "on") need = ( "direction", "contracts", "opt_inst_id", "sheets", "exchange_symbol", - "premium_budget", "option_target_points", "perp_target_points", "option_perp_ratio", ) + if not market: + need = need + ("premium_budget",) for k in need: if body.get(k) in (None, ""): return f"缺少字段: {k}" try: if float(body["contracts"]) <= 0 or float(body["sheets"]) <= 0: return "张数必须大于 0" - if float(body["premium_budget"]) <= 0: + prem = _sf(body.get("premium_budget")) + if not market and (prem is None or prem <= 0): return "权利金须大于 0" if float(body["option_target_points"]) <= 0 or float(body["perp_target_points"]) <= 0: return "目标位点数须大于 0" @@ -374,6 +390,9 @@ def validate_option_primary_start(body: dict[str, Any]) -> Optional[str]: want = opt_type_for_view(direction) if opt_type != want: return f"以期权为主时做{'多' if direction == 'long' else '空'}须用 {'Call' if want == 'C' else 'Put'}" + # 市价下单:左侧人工选约,跳过条件单(间隔/最短到期/杠杆门槛) + if market: + return None moneyness = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower() from lib.hedge_plan.hedge_plan_moneyness_lib import parse_strike_from_inst diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index dbdeca9..e14b4fe 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -45,47 +45,55 @@
- +
-

期权选择 · Call 币本位

-
+

市价选约 · Call 币本位

+
-
-
-
- - -
- 买 Call + 永续空 -
-
+ + + + - + + + + + + 指数 —
-

点「选择」填入右侧下单;比例默认 2(期权名义:永续名义),如期权 1 BTC → 永续约 0.5 BTC。

+

买 Call + 永续空 · 点「选择」填入右侧市价下单;比例默认 2(期权 1 → 永续 0.5)。

+ {# 条件单残留字段:隐藏,市价下单不用 #} + + + +
- - + + + - +
行权价实虚值杠杆类型合约 卖一/张杠杆 买一/张 操作
请刷新期权链
请刷新期权链
@@ -98,10 +106,10 @@
- +

- 以期权为主 + 市价下单 下单 · ETH 合约账户 @@ -109,9 +117,9 @@
规则说明
+

市价下单:左侧人工选约(同期权页:到期/Call·Put/实虚值),点「选择」后右侧下单;非条件盯盘。

组合:买 Call → 永续空;买 Put → 永续多。

比例:期权:永续默认 2(期权名义 ÷ 比例 = 永续名义)。例:期权 1 BTC → 永续 0.5 BTC。

-

账户:永续腿合约账户(USDT);期权腿币本位(交易账户 USDT 买币)。

@@ -152,37 +160,12 @@ 永续杠杆 开仓 -
-
-

选约过滤

-
- - - -
-

出场条件

@@ -203,7 +186,7 @@
- +