From 857a3dec690b4a37bf197113f3679bd25514262b Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 20:38:08 +0800 Subject: [PATCH] Fix btc_eth dialog sizing labels and buy-coin index. Use option index for premium/U and buy-coin, perp underly for hedge size text/margin, so cross-asset no longer mislabels ETH qty as BTC. Co-authored-by: Cursor --- lib/common/static/hedge_plan.js | 156 +++++++++++------- .../templates/hedge_plan_panel.html | 2 +- 2 files changed, 98 insertions(+), 60 deletions(-) diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 5d824bc..fab077d 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -287,13 +287,35 @@ } function indexPx() { + // 期权标的指数(链优先);跨币时勿与永续指数混用 + return optionIndexPx(); + } + + function optionIndexPx() { const fromChain = state.chain && Number(state.chain.index_px); if (fromChain && !Number.isNaN(fromChain) && fromChain > 0) return fromChain; + if (!state.crossAsset) { + const fromMkt = state.market && Number(state.market.index_px || state.market.mark); + if (fromMkt && !Number.isNaN(fromMkt) && fromMkt > 0) return fromMkt; + } + return null; + } + + function perpIndexPx() { const fromMkt = state.market && Number(state.market.index_px || state.market.mark); if (fromMkt && !Number.isNaN(fromMkt) && fromMkt > 0) return fromMkt; + if (!state.crossAsset) return optionIndexPx(); return null; } + function optionCoin() { + return (state.optionUnderly || state.underlying || "ETH").toUpperCase(); + } + + function perpCoin() { + return (state.perpUnderly || state.underlying || "ETH").toUpperCase(); + } + function currentContracts(expSelectId) { const exp = currentExp(expSelectId); return (exp && exp.contracts) || []; @@ -473,37 +495,38 @@ return null; } let sheets; - let eth; + let coinQty; if (sheetsOverride != null && Number(sheetsOverride) > 0) { sheets = Math.floor(Number(sheetsOverride) + 1e-12); if (!(sheets > 0)) return null; - eth = Math.round(sheets * ct * 100) / 100; + coinQty = Math.round(sheets * ct * 100) / 100; } else { if (!(budget > 0) || !(a > 0)) return null; - eth = Math.floor((usable / a) * 100 + 1e-12) / 100; - if (!(eth > 0)) return null; - sheets = Math.floor(eth / ct + 1e-12); + coinQty = Math.floor((usable / a) * 100 + 1e-12) / 100; + if (!(coinQty > 0)) return null; + sheets = Math.floor(coinQty / ct + 1e-12); if (!(sheets > 0)) return null; - eth = Math.round(sheets * ct * 100) / 100; + coinQty = Math.round(sheets * ct * 100) / 100; } - // 跨币:期权币×期权指数×(1/ratio) / 永续指数 = 永续币 - let perpQty = eth / ratio; + const optIdx = Number(optionIndexPx() || 0); + const perpIdx = Number(perpIndexPx() || 0); + let perpQty = coinQty / ratio; let contracts = perpQty / cs; let optNotional = null; let perpNotional = null; - if (state.crossAsset) { - const optIdx = Number((state.chain && state.chain.index_px) || 0); - const perpIdx = Number((state.market && (state.market.index_px || state.market.mark)) || 0); - if (optIdx > 0 && perpIdx > 0) { - optNotional = eth * optIdx; - perpNotional = optNotional / ratio; - perpQty = perpNotional / perpIdx; - contracts = perpQty / cs; - } + // 有双指数时一律走名义U(同币/跨币同公式);跨币缺指数则不定仓 + if (optIdx > 0 && perpIdx > 0) { + optNotional = coinQty * optIdx; + perpNotional = optNotional / ratio; + perpQty = perpNotional / perpIdx; + contracts = perpQty / cs; + } else if (state.crossAsset) { + return null; } return { usable: usable, - eth_qty: eth, + eth_qty: coinQty, + coin_qty: coinQty, perp_qty: perpQty, sheets: sheets, contracts: contracts, @@ -511,6 +534,8 @@ ratio: ratio, option_notional_usdt: optNotional, perp_notional_usdt: perpNotional, + option_index: optIdx > 0 ? optIdx : null, + perp_index: perpIdx > 0 ? perpIdx : null, cross_asset: !!state.crossAsset, }; } @@ -542,25 +567,25 @@ function formatOpSizingHtml(sized, amtPrec) { if (!sized) return ""; - const optCoin = (state.optionUnderly || state.underlying || "ETH").toUpperCase(); - const perpCoin = (state.perpUnderly || state.underlying || "ETH").toUpperCase(); - const prec = amtPrec != null ? amtPrec : perpCoin === "BTC" ? 4 : 2; + const optC = optionCoin(); + const perpC = perpCoin(); + const prec = amtPrec != null ? amtPrec : perpC === "BTC" ? 4 : 2; let html = "期权 " + fmt(sized.eth_qty, 2) + " " + - optCoin + + optC + " / " + sized.sheets + " 张 → 永续 " + fmt(sized.perp_qty, 4) + " " + - perpCoin + + perpC + " (" + fmt(sized.contracts, prec) + " 张) · 比例 " + fmt(sized.ratio, 2); - if (sized.cross_asset && sized.option_notional_usdt != null) { + if (sized.option_notional_usdt != null && sized.perp_notional_usdt != null) { html += " · 名义U " + fmt(sized.option_notional_usdt, 0) + @@ -580,21 +605,18 @@ /** * 期权开仓最大张数:OKX 交易账户 USDT 共用. - * 每张开仓需:买币(权利金×1.05×指数) + 永续保证金(对冲名义÷杠杆); - * 选最大 N 时须保证扣完买币后剩余仍够永续保证金. + * 买币按期权指数折 U;永续保证金按期权名义U÷比例÷杠杆(跨币同口径). */ function computeMaxSheets() { const ask = state.selected && Number(state.selected.ask); const ct = Number((state.selected && state.selected.ct_mult) || 0.01); - const ratio = numInput("hp-opt-perp-ratio", 2); - const mark = Number( - (state.market && (state.market.mark || state.market.index_px)) || indexPx() || 0 - ); - const lev = defaultPerpLeverage(state.underlying); + const ratio = numInput("hp-opt-perp-ratio", lockedRatio || 2); + const optIdx = Number(optionIndexPx() || 0); + const lev = defaultPerpLeverage(perpCoin()); const tradingUsdt = Number( state.tradingUsdt != null ? state.tradingUsdt : state.tradingUsdc ); - if (!(ask > 0) || !(ct > 0) || !(ratio > 0) || !(mark > 0) || !(lev > 0)) { + if (!(ask > 0) || !(ct > 0) || !(ratio > 0) || !(optIdx > 0) || !(lev > 0)) { return null; } if (!(tradingUsdt > 0)) { @@ -607,10 +629,10 @@ total_per_sheet: 0, }; } - // 买币 USDT/张 = 权利金币 × 1.05 × 指数 - const buyUsdtPerSheet = ask * ct * 1.05 * mark; - // 永续名义/张 = (期权币/比例)×指数; 保证金 = 名义÷杠杆 - const perpNotionalPerSheet = (ct / ratio) * mark; + // 买币 USDT/张 = 权利金币 × 1.05 × 期权指数 + const buyUsdtPerSheet = ask * ct * 1.05 * optIdx; + // 永续名义U/张 = 期权币名义U / 比例 + const perpNotionalPerSheet = (ct * optIdx) / ratio; const marginPerSheet = perpNotionalPerSheet / lev; const totalPerSheet = buyUsdtPerSheet + marginPerSheet; if (!(totalPerSheet > 0)) return null; @@ -630,11 +652,11 @@ const perpEl = $("hp-po-perp-auto"); const maxInfo = computeMaxSheets(); const sized = currentOpSizing(); - const coin = (state.underlying || "ETH").toUpperCase(); - const lev = defaultPerpLeverage(state.underlying); - const mark = Number( - (state.market && (state.market.mark || state.market.index_px)) || indexPx() || 0 - ); + const optC = optionCoin(); + const perpC = perpCoin(); + const lev = defaultPerpLeverage(perpC); + const optIdx = Number(optionIndexPx() || 0); + const perpIdx = Number(perpIndexPx() || 0); if (maxEl) { if (maxInfo) { maxEl.innerHTML = @@ -648,17 +670,22 @@ fmt(maxInfo.margin_per_sheet, 2) + " (杠杆 " + lev + - "x)"; + "x · " + + perpC + + ")"; } else { maxEl.textContent = "选择期权并刷新后显示:期权开仓最大张数(扣买币后剩余须够永续保证金)"; } } if (perpEl) { - if (sized) { - const buyUsdt = sized.premium_est * 1.05 * (mark > 0 ? mark : 0); - const marginUsdt = - sized.perp_qty * (mark > 0 ? mark : 0) / (lev > 0 ? lev : 1); + if (sized && optIdx > 0) { + const buyUsdt = sized.premium_est * 1.05 * optIdx; + const perpNotional = + sized.perp_notional_usdt != null + ? Number(sized.perp_notional_usdt) + : sized.perp_qty * (perpIdx > 0 ? perpIdx : optIdx); + const marginUsdt = perpNotional / (lev > 0 ? lev : 1); const remain = Number(state.tradingUsdt != null ? state.tradingUsdt : state.tradingUsdc) - buyUsdt; @@ -667,16 +694,20 @@ "永续自动开 " + fmt(sized.perp_qty, 4) + " " + - coin + + perpC + " / " + - fmt(sized.contracts, coin === "BTC" ? 4 : 2) + - " 张 · 买币约 " + + fmt(sized.contracts, perpC === "BTC" ? 4 : 2) + + " 张 · 买" + + optC + + "约 " + fmt(sized.premium_est * 1.05, 4) + " " + - coin + + optC + "(≈" + fmt(buyUsdt, 2) + - " U含5%) · 扣后剩 " + + " U含5%·参考" + + fmt(optIdx, 2) + + ") · 扣后剩 " + fmt(remain, 2) + " U · 永续保证金约 " + fmt(marginUsdt, 2) + @@ -729,13 +760,21 @@ b.setAttribute("aria-pressed", on ? "true" : "false"); }); const lab = $("hp-perp-uly-label"); - if (lab) lab.textContent = uly; + if (lab) { + if (state.mode === "perp_options" && state.crossAsset) { + lab.textContent = optionCoin() + "期权/" + perpCoin() + "永续"; + } else if (state.mode === "perp_options") { + lab.textContent = optionCoin(); + } else { + lab.textContent = uly; + } + } const ooLab = $("hp-oo-uly-label"); if (ooLab) ooLab.textContent = uly; // 永续开仓杠杆跟随 env(BTC_LEVERAGE / ALT_LEVERAGE=ETH) const levEl = $("hp-perp-leverage"); if (levEl && !state.perpLevTouched) { - levEl.value = String(defaultPerpLeverage(uly)); + levEl.value = String(defaultPerpLeverage(perpCoin())); } } @@ -2317,19 +2356,18 @@ const ct = Number(state.selected.ct_mult || 0.01); const ask = Number(state.selected.ask || 0); const prem = ask * sheets * ct; - // U 以买币参考价(指数)为准,与开仓买币口径一致 - const buyPx = Number( - (state.market && (state.market.index_px || state.market.mark)) || indexPx() || 0 - ); + // U 以期权标的指数为准(买 BTC/ETH 开期权);跨币不可用永续 ETH 价 + const buyPx = Number(optionIndexPx() || 0); const premU = buyPx > 0 ? prem * buyPx : null; line.textContent = "权利金 ≈ " + fmt(prem, 4) + - " 币" + + " " + + optionCoin() + (premU != null ? " / " + fmt(premU, 2) + " U" : "") + " · 卖一 " + fmt(ask, 4) + - (buyPx > 0 ? " · 买币参考 " + fmt(buyPx, 2) : ""); + (buyPx > 0 ? " · 买" + optionCoin() + "参考 " + fmt(buyPx, 2) : ""); } function buildStraddleRows(contracts) { diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index 82e862b..06e2edd 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -508,4 +508,4 @@ - +