/** * OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁. */ (function () { const root = document.getElementById("hedge-plan-root"); if (!root) return; function flagOn(attr) { return root.getAttribute(attr) !== "0"; } const showPerp = flagOn("data-show-perp"); const showOo = flagOn("data-show-oo"); function pickDefaultTab() { if (showPerp) return "perp_options"; if (showOo) return "options_options"; return "active"; } const state = { 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", ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值 ooRecommend: null, // atm_straddle | double_otm | null ooStrikeExpandAll: false, // 默认 Call/Put 各 3 档 chain: null, selected: null, legA: null, legB: null, market: null, ooSheetsMode: "same_sheets", ooBiasSplitBy: "budget", ooBiasRatio: 0.7, ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0", ooCloseMode: "close_all", direction: "long", optionPrimary: root.getAttribute("data-option-primary") !== "0", opLevTouched: false, opRatioTouched: false, tradingUsdc: null, fundingUsdc: null, tradeBudgetUsdc: null, budgetBuffer: (function () { const raw = root.getAttribute("data-budget-buffer"); const n = raw != null && raw !== "" ? Number(raw) : NaN; return !Number.isNaN(n) && n > 0 ? n : 0.95; })(), previewOk: false, canStart: false, previewPlanType: null, }; function getDirection() { return (state.direction || "long").toLowerCase() === "short" ? "short" : "long"; } function syncPoDirUI() { const dir = getDirection(); document.querySelectorAll(".hp-po-dir").forEach(function (b) { const on = (b.getAttribute("data-dir") || "") === dir; b.classList.toggle("is-selected", on); b.classList.toggle("active", on); }); } function setDirection(dir, forceReload) { const next = (dir || "long").toLowerCase() === "short" ? "short" : "long"; const changed = next !== getDirection(); state.direction = next; syncPoDirUI(); if (!changed && !forceReload) return; state.selected = null; if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; void loadMarket().then(function () { renderListStrikes(); }); } function $(id) { return document.getElementById(id); } async function apiJson(url, opts) { const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {})); const data = await res.json().catch(function () { return {}; }); if (!res.ok) throw new Error(data.msg || res.statusText || "请求失败"); return data; } function fmt(v, d) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(d == null ? 2 : d); } function fmtOptionPx(v, tickSz) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); const tick = Number(tickSz); if (!tickSz || Number.isNaN(tick) || tick <= 0) { return String(n).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, ""); } let decimals = 0; if (tick < 1) decimals = Math.max(0, -Math.round(Math.log10(tick))); else if (String(tick).indexOf(".") >= 0) decimals = String(tick).split(".")[1].length; let s = n.toFixed(decimals); // 仅裁小数尾零;整数 tick(BTC=5)时绝不能把 1370 裁成 137 if (decimals > 0) s = s.replace(/\.?0+$/, ""); return s || "0"; } /** 价格/流动性(张),价格按 tick_sz 对齐交易所精度. */ function fmtPxSz(px, sz, estimated, tickSz) { if (px === null || px === undefined || Number.isNaN(Number(px))) return "—"; let price = fmtOptionPx(px, tickSz); if (price === "—") return "—"; if (estimated) price += "~"; if (sz === null || sz === undefined || sz === "" || Number.isNaN(Number(sz))) return price; const s = Number(sz); const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s); return price + "/" + size; } function moneynessBadge(c) { const m = (c && c.moneyness) || ""; const label = (c && c.moneyness_label) || "—"; return '' + label + ""; } function pnlClass(v) { if (v == null || v === "") return ""; const n = Number(v); if (Number.isNaN(n)) return ""; return n >= 0 ? "hp-pnl-pos" : "hp-pnl-neg"; } function fmtPnlHtml(v, digits) { if (v == null || v === "" || Number.isNaN(Number(v))) return "—"; const cls = pnlClass(v); return '' + fmt(v, digits) + ""; } function fmtRr(v) { if (v == null || Number.isNaN(Number(v))) return "—"; return fmt(v, 2) + ":1"; } /** 列表/盯盘候选杠杆门槛(与后端 effective_min_opt_leverage 对齐). */ function optionPrimaryMinLev() { const minLev = numInput("hp-opt-leverage", opMoneyKind() === "otm" ? 200 : 100); if (!(minLev > 0)) return 0; if (opMoneyKind() === "otm") return Math.max(minLev, 180); return minLev; } function optionPrimaryLevOk(c) { const idx = indexPx(); const ask = Number(c && c.ask); const floor = optionPrimaryMinLev(); if (!(floor > 0)) return true; if (!(idx > 0) || !(ask > 0)) return false; return idx / ask >= floor - 1e-9; } function matchesMoneyFilter(c) { const f = state.moneyFilter || "itm"; 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 === "itm") return m === "itm" || m === "atm"; if (f === "atm") return m === "atm"; if (f === "otm") return m === "otm"; return m === "itm" || m === "atm"; } function matchesOoMoneyFilter(c) { // 期期:仅平值/虚值(禁实值) if (!c) return false; const m = (c.moneyness || "").toLowerCase(); const f = state.ooMoneyFilter || "atm_otm"; if (f === "atm") return m === "atm"; if (f === "otm") return m === "otm"; return m === "atm" || m === "otm"; } function indexPx() { const fromChain = state.chain && Number(state.chain.index_px); if (fromChain && !Number.isNaN(fromChain) && fromChain > 0) return fromChain; const fromMkt = state.market && Number(state.market.index_px || state.market.mark); if (fromMkt && !Number.isNaN(fromMkt) && fromMkt > 0) return fromMkt; return null; } function currentContracts(expSelectId) { const exp = currentExp(expSelectId); return (exp && exp.contracts) || []; } function pickClosestItmAtm(contracts, want) { const idx = indexPx(); if (!idx) return null; const list = (contracts || []).filter(function (c) { return ( String(c.opt_type || "").toUpperCase() === want && matchesMoneyFilter(c) ); }); if (!list.length) return null; list.sort(function (a, b) { return Math.abs(Number(a.strike) - idx) - Math.abs(Number(b.strike) - idx); }); return list[0]; } function pickOoTemplate(template) { const idx = indexPx(); const contracts = currentContracts("hp-oo-exp-select"); if (!idx || !contracts.length) return null; const preferOtm = template === "double_otm"; function pickSide(want) { const list = contracts.filter(function (c) { if (String(c.opt_type || "").toUpperCase() !== want) return false; if (!matchesOoMoneyFilter(c)) return false; const m = (c.moneyness || "").toLowerCase(); if (preferOtm) return m === "otm"; return m === "atm" || m === "otm"; }); if (!list.length) return null; list.sort(function (a, b) { const ma = (a.moneyness || "").toLowerCase(); const mb = (b.moneyness || "").toLowerCase(); if (!preferOtm) { if (ma === "atm" && mb !== "atm") return -1; if (mb === "atm" && ma !== "atm") return 1; } return Math.abs(Number(a.strike) - idx) - Math.abs(Number(b.strike) - idx); }); return list[0]; } const call = pickSide("C"); const put = pickSide("P"); if (!call || !put) return null; return { call: call, put: put }; } function isOptionPrimary() { return !!state.optionPrimary; } function optTypeForDirection(dir) { if (isOptionPrimary()) { return dir === "short" ? "P" : "C"; } return dir === "short" ? "C" : "P"; } function opMoneyKind() { const f = state.moneyFilter || "itm"; if (f === "otm") return "otm"; if (f === "atm") return "atm"; 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)) { ratioEl.value = kind === "otm" ? "4" : "2"; } } function syncOptionPrimaryUI() { const on = isOptionPrimary(); 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"); else ins.removeAttribute("hidden"); ins.style.display = on ? "none" : ""; } if (op) { op.classList.toggle("hidden", !on); if (!on) op.setAttribute("hidden", "hidden"); else op.removeAttribute("hidden"); op.style.display = on ? "" : "none"; } 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 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+永续多" : "做空永续"; syncPoActionBtn(); } function hoursFromExpMs(expMs) { const n = Number(expMs); if (!n || Number.isNaN(n)) return null; const ms = n < 1e12 ? n * 1000 : n; return (ms - Date.now()) / 3600000; } function numInput(id, fallback) { const el = $(id); const n = Number(el && el.value); if (Number.isNaN(n)) return fallback; return n; } function computeOpSizing(ask, ctMult) { const budget = numInput("hp-premium-budget", 0); const ratio = numInput("hp-opt-perp-ratio", 2); const cs = Number((state.market && state.market.contract_size) || 0.01); const usable = budget * 0.95; const a = Number(ask || 0); const ct = Number(ctMult || 0.01); if (!(budget > 0) || !(a > 0) || !(ct > 0) || !(ratio > 0) || !(cs > 0)) { return null; } let eth = Math.floor((usable / a) * 100 + 1e-12) / 100; if (!(eth > 0)) return null; let sheets = Math.floor(eth / ct + 1e-12); if (!(sheets > 0)) return null; eth = Math.round(sheets * ct * 100) / 100; const perpEth = eth / ratio; const contracts = perpEth / cs; return { usable: usable, eth_qty: eth, sheets: sheets, contracts: contracts, premium_est: a * sheets * ct, ratio: ratio, }; } function syncUnderlyingUI() { const uly = state.underlying || "ETH"; document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) { const on = b.getAttribute("data-uly") === uly; b.classList.toggle("active", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); const lab = $("hp-perp-uly-label"); if (lab) lab.textContent = uly; const ooLab = $("hp-oo-uly-label"); if (ooLab) ooLab.textContent = uly; } function syncMoneyUI() { const moneySel = $("hp-money-select"); if (moneySel && isOptionPrimary()) { moneySel.value = state.moneyFilter === "otm" ? "otm" : state.moneyFilter === "atm" ? "atm" : "itm"; } document.querySelectorAll(".hp-money-btn").forEach(function (b) { const on = b.getAttribute("data-money") === state.moneyFilter; b.classList.toggle("active", on); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); document.querySelectorAll(".hp-oo-money-btn").forEach(function (b) { const on = b.getAttribute("data-oo-money") === state.ooMoneyFilter; b.classList.toggle("active", on); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function syncPoActionBtn() { const btn = $("hp-preview-btn"); if (!btn) return; if (isOptionPrimary()) { btn.textContent = "策略启动"; btn.title = "按参数启动盯盘;杠杆/间隔达标后自动开仓(非现场开)"; } else { btn.textContent = "计算"; btn.title = "情景测算后再启动"; } } function isPoOptionPrimaryPlan(p) { if (!p || p.plan_type !== "perp_options") return false; return p.option_primary == 1 || p.option_primary === true || Number(p.option_primary) === 1; } function setPoStrategyStatus(kind, planId) { const el = $("hp-po-strategy-status"); if (!el) return; el.classList.remove("is-watching", "is-holding", "is-idle"); if (!isOptionPrimary()) { el.textContent = ""; return; } const idPart = planId ? " #" + planId : ""; if (kind === "watching") { el.textContent = "盯盘中" + idPart; el.classList.add("is-watching"); } else if (kind === "holding") { el.textContent = "持仓中" + idPart; el.classList.add("is-holding"); } else { el.textContent = "未启动"; el.classList.add("is-idle"); } } async function refreshPoStrategyStatus() { const el = $("hp-po-strategy-status"); if (!el) return; if (!isOptionPrimary()) { setPoStrategyStatus("", null); return; } try { const d = await apiJson("/api/hedge-plan/active"); const uly = (state.underlying || "ETH").toUpperCase(); const rows = (d.plans || []).filter(function (p) { return isPoOptionPrimaryPlan(p) && String(p.underlying || "").toUpperCase() === uly; }); if (!rows.length) { setPoStrategyStatus("idle", null); return; } rows.sort(function (a, b) { return Number(b.id || 0) - Number(a.id || 0); }); const p = rows[0]; const st = String(p.status || ""); if (st === "watching") setPoStrategyStatus("watching", p.id); else if (st === "active" || st === "opening" || st === "partial") setPoStrategyStatus("holding", p.id); else setPoStrategyStatus("idle", null); } catch (e) { /* ignore status refresh errors */ } } function syncOoRecommendUI() { const cur = state.ooRecommend || ""; document.querySelectorAll(".hp-oo-recommend-btn").forEach(function (b) { const on = (b.getAttribute("data-oo-rec") || "") === cur; b.classList.toggle("active", on); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function syncOoExpandUI() { const btn = $("hp-oo-expand-all"); if (!btn) return; const on = !!state.ooStrikeExpandAll; btn.classList.toggle("active", on); btn.classList.toggle("is-selected", on); btn.setAttribute("aria-pressed", on ? "true" : "false"); } function calcStrikeAskLeverage(strike, ask) { const k = Number(strike); const a = Number(ask); if (!Number.isFinite(k) || k <= 0 || !Number.isFinite(a) || a <= 0) return "—"; 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; let best = rows[0].strike; let bestDist = Math.abs(Number(rows[0].strike) - Number(idx)); rows.forEach(function (row) { const d = Math.abs(Number(row.strike) - Number(idx)); if (d < bestDist || (d === bestDist && Number(row.strike) < Number(best))) { bestDist = d; best = row.strike; } }); return best; } function sliceOoTRows(prepared, idx) { // prepared: [{strike, call, put, callOk, putOk}] if (state.ooStrikeExpandAll || !prepared.length) return prepared; const n = 3; const anchor = Number(idx) || Number(findAtmStrikeFromRows(prepared, idx)) || 0; function nearest(list, count) { return list .slice() .sort(function (a, b) { return Math.abs(Number(a.strike) - anchor) - Math.abs(Number(b.strike) - anchor); }) .slice(0, count); } const byStrike = {}; nearest( prepared.filter(function (r) { return r.callOk; }), n ).forEach(function (r) { byStrike[String(r.strike)] = r; }); nearest( prepared.filter(function (r) { return r.putOk; }), n ).forEach(function (r) { byStrike[String(r.strike)] = r; }); // 已选用腿始终保留,避免折叠后看不到选中项 [state.legA, state.legB].forEach(function (leg) { if (!leg) return; const hit = prepared.find(function (r) { return ( (r.call && r.call.inst_id === leg.inst_id) || (r.put && r.put.inst_id === leg.inst_id) ); }); if (hit) byStrike[String(hit.strike)] = hit; }); return Object.keys(byStrike) .map(function (k) { return byStrike[k]; }) .sort(function (a, b) { return Number(a.strike) - Number(b.strike); }); } function syncTabUI() { let tab = state.tab || pickDefaultTab(); if (tab === "perp_options" && !showPerp) tab = pickDefaultTab(); if (tab === "options_options" && !showOo) tab = pickDefaultTab(); state.tab = tab; document.querySelectorAll(".hp-tab").forEach(function (b) { const on = b.getAttribute("data-tab") === tab; b.classList.toggle("active", on); b.setAttribute("aria-selected", on ? "true" : "false"); }); ["perp_options", "options_options", "active", "history", "stats"].forEach(function (id) { const panel = $("hp-tab-" + id); if (!panel) return; const on = id === tab; panel.classList.toggle("hidden", !on); if (on) panel.removeAttribute("hidden"); else panel.setAttribute("hidden", ""); }); if (tab === "perp_options" || tab === "options_options") { state.mode = tab; } const hint = $("hp-acct-hint"); if (hint) { if (tab === "options_options") { hint.textContent = "期期双腿→期权账户"; } else if (tab === "perp_options") { hint.textContent = "永续腿→合约账户 · 期权腿→期权账户"; } else { hint.textContent = "进行中/历史含永期与期期;显示开关只影响新建测算 Tab"; } } } function setGateLine(gates) { 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; syncPreviewStartBtn(); } function syncPreviewStartBtn() { const start = $("hp-preview-start"); if (!start) return; start.disabled = !(state.previewOk && state.canStart); } function openPreviewModal() { const modal = $("hp-preview-modal"); if (modal) modal.hidden = false; } function closePreviewModal() { const modal = $("hp-preview-modal"); if (modal) modal.hidden = true; } function applyBudgetBuffer(raw) { if (raw == null || raw === "") return; const buf = Number(raw); if (Number.isNaN(buf) || buf <= 0) return; state.budgetBuffer = buf; const el = $("hp-oo-buf-ratio"); if (el) el.textContent = fmt(buf, 2); } function setOptionsBalance(chain) { const acct = (chain && chain.options_account) || {}; const label = (chain && chain.account_label) || acct.label || "期权账户"; const tag = $("hp-opt-acct-tag"); if (tag) tag.textContent = label; if (acct.trading_usdc != null && acct.trading_usdc !== "") { state.tradingUsdc = Number(acct.trading_usdc); } if (acct.funding_usdc != null && acct.funding_usdc !== "") { state.fundingUsdc = Number(acct.funding_usdc); } if (chain && chain.trade_budget_usdc != null && chain.trade_budget_usdc !== "") { state.tradeBudgetUsdc = Number(chain.trade_budget_usdc); } if (chain && chain.budget_buffer != null && chain.budget_buffer !== "") { applyBudgetBuffer(chain.budget_buffer); } const line = label + " · 交易 USDC " + fmt(acct.trading_usdc, 2) + " · 资金 USDC " + fmt(acct.funding_usdc, 2); const el = $("hp-opt-bal-line"); if (el) el.textContent = line; const fundEl = $("hp-oo-funding-usdc"); const tradeEl = $("hp-oo-trading-usdc"); if (fundEl) fundEl.textContent = fmt(acct.funding_usdc, 2); if (tradeEl) tradeEl.textContent = fmt(acct.trading_usdc, 2); autoFillOoSheets(); } function setOoXferMsg(text, kind) { const el = $("hp-oo-xfer-msg"); if (!el) return; el.textContent = text || ""; el.classList.toggle("is-err", kind === "err"); el.classList.toggle("is-ok", kind === "ok"); el.classList.toggle("muted", !kind); } function ooXferDir() { const sel = $("hp-oo-xfer-dir"); return (sel && sel.value) || "funding_to_trading"; } async function submitOoUsdcTransfer(amount) { const dir = ooXferDir(); const from = dir === "trading_to_funding" ? "trading" : "funding"; const to = dir === "trading_to_funding" ? "funding" : "trading"; setOoXferMsg("划转中…", null); try { const d = await apiJson("/api/options/transfer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ccy: "USDC", from: from, to: to, amount: amount }), }); if (!d.ok) { setOoXferMsg(d.msg || "划转失败", "err"); return; } setOoXferMsg("划转成功", "ok"); if ($("hp-oo-xfer-amount")) $("hp-oo-xfer-amount").value = ""; await loadChain(); } catch (e) { setOoXferMsg(e.message || "划转失败", "err"); } } function resolveOoBudget() { const buf = state.budgetBuffer > 0 ? state.budgetBuffer : 0.95; const trading = state.tradingUsdc; const cap = state.tradeBudgetUsdc; let tradingCap = null; let tradeCap = null; if (trading != null && !Number.isNaN(Number(trading))) { tradingCap = Math.max(0, Number(trading) * buf); } if (cap != null && !Number.isNaN(Number(cap))) { tradeCap = Math.max(0, Number(cap)); } if (tradingCap == null && tradeCap == null) { return { ok: false, budget: 0, tradingCap: null, tradeCap: null, buf: buf, msg: "缺少交易户余额与单笔预算" }; } let budget = 0; if (tradingCap == null) budget = tradeCap; else if (tradeCap == null) budget = tradingCap; else budget = Math.min(tradingCap, tradeCap); budget = Math.floor(budget * 1e6 + 1e-12) / 1e6; return { ok: budget > 0, budget: budget, tradingCap: tradingCap, tradeCap: tradeCap, buf: buf, msg: budget > 0 ? "" : "可用预算为 0", }; } function unitCost(c) { if (!c) return 0; const ask = Number(c.ask || 0); if (!(ask > 0)) return 0; return ask * Number(c.ct_mult || 0.01); } function capByDepth(n, askSz) { let out = Math.max(0, Math.floor(Number(n) || 0)); if (askSz == null || askSz === "") return out; const d = Number(askSz); if (Number.isNaN(d)) return out; if (d <= 0) return 0; return Math.min(out, Math.floor(d + 1e-12)); } function normalizeOptCP(ot) { const u = String(ot || "").toUpperCase(); if (u.indexOf("C") === 0) return "C"; if (u.indexOf("P") === 0) return "P"; return ""; } function ooSizeModeLabel(mode) { if (mode === "long_bias") return "做多"; if (mode === "short_bias") return "做空"; if (mode === "split_budget") return "均分"; return "同张数"; } function suggestOoSheetsLocal(budget, mode) { const costA = unitCost(state.legA); const costB = unitCost(state.legB); if (!(budget > 0)) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "可用预算为 0" }; } if (!(costA > 0) || !(costB > 0)) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" }; } let ratio = Number(state.ooBiasRatio); if (!(ratio > 0) || !(ratio < 1)) ratio = 0.7; const splitBy = state.ooBiasSplitBy === "sheets" ? "sheets" : "budget"; const pair = costA + costB; const nPair = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0; const nSame = Math.min( capByDepth(nPair, state.legA && state.legA.ask_sz), capByDepth(nPair, state.legB && state.legB.ask_sz) ); let nA = 0; let nB = 0; if (mode === "long_bias" || mode === "short_bias") { const aCP = normalizeOptCP(state.legA && state.legA.opt_type); const bCP = normalizeOptCP(state.legB && state.legB.opt_type); if (!((aCP === "C" && bCP === "P") || (aCP === "P" && bCP === "C"))) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "做多/做空需一腿 Call、一腿 Put" }; } const callIsA = aCP === "C"; const majorIsCall = mode === "long_bias"; let nCall = 0; let nPut = 0; if (splitBy === "sheets") { // 总张数 = 同张数两侧合计(每腿 n → 共 2n),再按比例拆 const total = nSame * 2; if (total < 2) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "同张数总规模不足 2,无法按比例拆分" }; } let majorN = Math.round(total * ratio); majorN = Math.max(1, Math.min(majorN, total - 1)); const minorN = total - majorN; nCall = majorIsCall ? majorN : minorN; nPut = majorIsCall ? minorN : majorN; } else { const majBudget = budget * ratio; const minBudget = budget * (1 - ratio); const costCall = callIsA ? costA : costB; const costPut = callIsA ? costB : costA; if (majorIsCall) { nCall = Math.floor(majBudget / costCall + 1e-12); nPut = Math.floor(minBudget / costPut + 1e-12); } else { nPut = Math.floor(majBudget / costPut + 1e-12); nCall = Math.floor(minBudget / costCall + 1e-12); } } nA = callIsA ? nCall : nPut; nB = callIsA ? nPut : nCall; nA = capByDepth(nA, state.legA && state.legA.ask_sz); nB = capByDepth(nB, state.legB && state.legB.ask_sz); } else if (mode === "split_budget") { const half = budget / 2; nA = Math.floor(half / costA + 1e-12); nB = Math.floor(half / costB + 1e-12); nA = capByDepth(nA, state.legA && state.legA.ask_sz); nB = capByDepth(nB, state.legB && state.legB.ask_sz); } else { nA = nSame; nB = nSame; } const premium = costA * nA + costB * nB; const ok = nA >= 1 && nB >= 1; return { sheetsA: nA, sheetsB: nB, premium: premium, ok: ok, msg: ok ? "" : "预算不够开 1+1(或卖一深度不足)", }; } function syncOoSizeModeUI() { document.querySelectorAll(".hp-oo-size-mode").forEach(function (b) { const on = b.getAttribute("data-oo-size") === state.ooSheetsMode; b.classList.toggle("active", on); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function syncOoCloseModeUI() { const enabled = !!state.ooCloseModeEnabled; const row = $("hp-oo-close-mode-row"); if (row) row.classList.toggle("hidden", !enabled); if (!enabled) state.ooCloseMode = "hold_expiry"; document.querySelectorAll(".hp-oo-close-mode").forEach(function (b) { const on = b.getAttribute("data-oo-close") === state.ooCloseMode; b.classList.toggle("active", on); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function updateOoBudgetLine(extra) { const line = $("hp-oo-budget-line"); if (!line) return; const b = resolveOoBudget(); const sizeLabel = ooSizeModeLabel(state.ooSheetsMode); const closeLabel = !state.ooCloseModeEnabled ? "" : state.ooCloseMode === "hold_expiry" ? "到期平" : "全平"; const parts = ["可用 " + fmt(b.budget, 2) + "U", sizeLabel]; if (closeLabel) parts.push(closeLabel); if (extra && extra.msg) parts.push(extra.msg); else if (b.msg) parts.push(b.msg); line.textContent = parts.join(" · "); line.title = "对冲预算=min(交易×" + fmt(b.buf, 2) + ", 单笔) · 缓冲 HEDGE_PLAN_BUDGET_BUFFER" + (b.tradingCap != null ? " · 交易×缓冲 " + fmt(b.tradingCap, 2) : "") + (b.tradeCap != null ? " · 单笔 " + fmt(b.tradeCap, 2) : ""); line.classList.toggle("hp-oo-budget-warn", !!(extra && extra.msg) || !b.ok); } function autoFillOoSheets() { syncOoSizeModeUI(); if (!state.legA || !state.legB) { updateOoBudgetLine(null); return; } const b = resolveOoBudget(); const sug = suggestOoSheetsLocal(b.budget, state.ooSheetsMode); const a = $("hp-oo-sheets-a"); const bb = $("hp-oo-sheets-b"); if (a && !a.disabled) a.value = String(sug.sheetsA); if (bb && !bb.disabled) bb.value = String(sug.sheetsB); updateOoBudgetLine(sug.ok ? null : sug); updateOoPremiumLine(); } async function loadGates() { try { const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode)); if (d.oo_close_mode_enabled != null) { state.ooCloseModeEnabled = !!d.oo_close_mode_enabled; } if (!state.ooCloseModeEnabled) { state.ooCloseMode = "hold_expiry"; } else if (d.oo_close_mode_default && !state._ooCloseModeTouched) { state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all"; } if (d.oo_bias_split_by != null) { state.ooBiasSplitBy = d.oo_bias_split_by === "sheets" ? "sheets" : "budget"; } if (d.oo_bias_ratio != null && Number(d.oo_bias_ratio) > 0 && Number(d.oo_bias_ratio) < 1) { state.ooBiasRatio = Number(d.oo_bias_ratio); } if (d.budget_buffer != null) applyBudgetBuffer(d.budget_buffer); syncOoCloseModeUI(); setGateLine(d); if (state.mode === "options_options") autoFillOoSheets(); } catch (e) { setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false }); } } async function loadMarket() { const dir = getDirection(); const d = await apiJson( "/api/hedge-plan/market?base=" + encodeURIComponent(state.underlying) + "&direction=" + encodeURIComponent(dir) + "&option_primary=" + (isOptionPrimary() ? "1" : "0") ); state.market = d; setGateLine(d.gates); const acctLabel = d.account_label || "合约账户"; const tag = $("hp-perp-acct-tag"); if (tag) tag.textContent = acctLabel; const amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4; const markEl = $("hp-po-mark"); if (markEl) markEl.textContent = "标记 " + fmt(d.mark, 2); const quoteHtml = "可用 " + fmt(d.available_usdt, 2) + " USDT · 卖一 " + fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + " · 面值 " + fmt(d.contract_size, 4) + " · 精度 " + amtPrec + " 位" + (d.perp_direction ? " · 永续方向 " + (d.perp_direction === "short" ? "空" : "多") : ""); const q = $("hp-perp-quote"); if (q) q.innerHTML = quoteHtml; const contractsInput = $("hp-contracts"); if (contractsInput) { const step = amtPrec <= 0 ? "1" : String(Math.pow(10, -amtPrec)); contractsInput.step = step; } const sz = $("hp-sizing-line"); if (sz) { if (isOptionPrimary()) { const sized = state.selected && computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01); if (sized) { sz.innerHTML = "执行预算 " + fmt(sized.usable, 2) + " · 期权 ETH " + fmt(sized.eth_qty, 2) + " / " + sized.sheets + " 张 · 永续 " + fmt(sized.contracts, amtPrec) + " 张"; } else { sz.textContent = "填写权利金并选用期权后显示定仓(权利金×0.95,ETH两位小数)"; } } else if (d.full_margin_sizing) { const s = d.full_margin_sizing; sz.innerHTML = "全仓建议 " + fmt(d.suggest_contracts, amtPrec) + " 张 · 保证金 " + fmt(s.margin_capital, 2) + " × " + s.leverage + "x · 名义 " + fmt(s.notional_value, 2) + " USDT"; } else { sz.textContent = "非全仓时请手动填张数;永期开仓需全仓"; } } const entry = $("hp-entry"); if (!isOptionPrimary() && entry && d.entry_ref && !entry.value) entry.value = d.entry_ref; if (!isOptionPrimary() && contractsInput && d.suggest_contracts != null && !contractsInput.value) { contractsInput.value = fmt(d.suggest_contracts, amtPrec); } const label = $("hp-opt-type-label"); if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put"; updatePerpPnlHint(); } function updatePerpPnlHint() { const el = $("hp-perp-pnl-line"); if (!el) return; if (isOptionPrimary()) { const n = numInput("hp-opt-target-pts", NaN); const m = numInput("hp-perp-target-pts", NaN); const k = state.selected && Number(state.selected.strike); if (!(k > 0) || (!(n >= 0) && !(m >= 0))) { el.innerHTML = '选用期权并填目标点数后显示 K±N 出场参考'; return; } const dir = getDirection(); const optT = n >= 0 ? (dir === "short" ? k - n : k + n) : null; const perpT = m >= 0 ? (dir === "short" ? k - m : k + m) : null; el.innerHTML = "期权目标指数 " + (optT != null ? fmt(optT, 2) : "—") + " · 永续目标指数 " + (perpT != null ? fmt(perpT, 2) : "—") + ' (相对K;期权目标需买一且扣费净利>0)'; return; } const entry = Number(($("hp-entry") && $("hp-entry").value) || NaN); const tp = Number(($("hp-tp") && $("hp-tp").value) || NaN); const sl = Number(($("hp-sl") && $("hp-sl").value) || NaN); const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || NaN); const cs = Number((state.market && state.market.contract_size) || 0.01); const dir = getDirection(); if (!(entry > 0) || !(contracts > 0) || !(cs > 0)) { el.innerHTML = '填开仓价与张数后,输入止盈/止损可看盈亏'; return; } function pnlAt(exitPx) { const coins = contracts * cs; if (dir === "short") return (entry - exitPx) * coins; return (exitPx - entry) * coins; } function chip(lab, px) { if (!(px > 0)) { return '' + lab + " —"; } const p = pnlAt(px); const cls = p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg"; const sign = p >= 0 ? "+" : ""; return ( '' + lab + ' ' + sign + fmt(p, 2) + "" ); } el.innerHTML = chip("止盈", tp) + chip("止损", sl); } 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; sel.innerHTML = ''; let firstOk = null; let skippedNear = 0; (chain.expiries || []).forEach(function (e) { const h = hoursFromExpMs(e.exp_time); if (minH > 0 && h != null && h < minH) { skippedNear += 1; return; } const opt = document.createElement("option"); opt.value = String(e.exp_time); const dt = new Date(Number(e.exp_time) < 1e12 ? Number(e.exp_time) * 1000 : Number(e.exp_time)); opt.textContent = dt.toLocaleString() + (h != null ? " · " + fmt(h, 1) + "h" : ""); sel.appendChild(opt); if (!firstOk) firstOk = e; }); if (prev) sel.value = prev; if (!sel.value && firstOk) { sel.value = String(firstOk.exp_time); } if (isPoSel && skippedNear > 0 && minH > 0) { const tip = document.createElement("option"); tip.disabled = true; tip.textContent = "(已隐藏 " + skippedNear + " 个不足 " + minH + "h 的到期 · 可改左侧最低剩余小时)"; sel.appendChild(tip); } } async function loadChain() { // 拉完整链(按 CHAIN_MAX_DTE);永期「最低剩余小时」只在左侧到期下拉里过滤,不影响期期看到明天到期 const url = "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying); const d = await apiJson(url); state.chain = d; const idx = $("hp-index-line"); if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2); const ooIdx = $("hp-oo-index"); if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2); setOptionsBalance(d); fillExpSelect($("hp-exp-select"), d); fillExpSelect($("hp-oo-exp-select"), d); renderListStrikes(); renderTStrikes(); if (d.index_px) { // 盈亏比默认2,不随指数自动改写 if ($("hp-profit-rr") && !$("hp-profit-rr").value) { $("hp-profit-rr").value = "2"; } } } function currentExp(selectId) { const sel = $(selectId); const expMs = sel && sel.value; if (!expMs || !state.chain) return null; return (state.chain.expiries || []).find(function (e) { return String(e.exp_time) === String(expMs); }); } function pickContract(c) { if (!c) return; const m = (c.moneyness || "").toLowerCase(); if (!isOptionPrimary() && m === "otm") { alert("永期保险腿须为实值或平值,不可选虚值"); return; } if (isOptionPrimary() && !matchesMoneyFilter(c)) { alert("不符合当前间隔/虚实值/杠杆门槛"); return; } state.selected = c; const el = $("hp-sel-inst"); if (el) el.textContent = c.inst_id; const tbody = $("hp-strike-tbody"); if (tbody) { tbody.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === c.inst_id); }); tbody.querySelectorAll(".hp-pick").forEach(function (b) { b.classList.toggle("active", b.getAttribute("data-inst") === c.inst_id); }); } updatePremiumLine(); if (isOptionPrimary()) { const sized = computeOpSizing(c.ask, c.ct_mult || 0.01); if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets); void loadMarket(); updatePerpPnlHint(); } } function renderListStrikes() { const tbody = $("hp-strike-tbody"); if (!tbody) return; const want = optTypeForDirection(getDirection()); const exp = currentExp("hp-exp-select"); const prevInst = state.selected && state.selected.inst_id; tbody.innerHTML = ""; if (!exp) { 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); if (!list.length) { const idx = indexPx(); const interval = isOptionPrimary() ? numInput("hp-strike-interval", 15) : null; let hint = "无匹配合约"; if (isOptionPrimary() && sameType.length) { hint = "无匹配" + (want === "C" ? "Call" : "Put") + "(已滤 " + sameType.length + " 档)·检查间隔" + (interval != null ? "≤" + interval : "") + "点/虚实值/杠杆≥" + optionPrimaryMinLev(); } else if (!sameType.length) { hint = "该到期无 " + (want === "C" ? "Call" : "Put") + (idx ? " · 指数 " + fmt(idx, 2) : "") + " · 可换到期或点刷新链"; } tbody.innerHTML = '' + hint + ""; if (isOptionPrimary()) clearPoSelection(); return; } list.forEach(function (c) { const tr = document.createElement("tr"); 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) + ''; tbody.appendChild(tr); }); tbody.querySelectorAll(".hp-pick").forEach(function (btn) { btn.addEventListener("click", function () { const inst = btn.getAttribute("data-inst"); const c = list.find(function (x) { return x.inst_id === inst; }); pickContract(c); }); }); autoMatchPoOption(false); } function updatePremiumLine() { const line = $("hp-premium-line"); if (!line || !state.selected) { if (line) line.textContent = ""; return; } const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1); const ct = Number(state.selected.ct_mult || 0.01); const ask = Number(state.selected.ask || 0); const prem = ask * sheets * ct; line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC(期权账户)"; } function buildStraddleRows(contracts) { const map = {}; (contracts || []).forEach(function (c) { const key = String(c.strike); if (!map[key]) map[key] = { strike: c.strike, call: null, put: null }; const o = (c.opt_type || "").toUpperCase(); if (o === "C") map[key].call = c; else if (o === "P") map[key].put = c; }); return Object.keys(map) .map(function (k) { return map[k]; }) .sort(function (a, b) { return Number(a.strike) - Number(b.strike); }); } function selectedOoLegMap() { const map = {}; if (state.legA && state.legA.inst_id) map[String(state.legA.inst_id)] = "A"; if (state.legB && state.legB.inst_id) map[String(state.legB.inst_id)] = "B"; return map; } function syncOoPickHighlight() { const tbody = $("hp-oo-tbody"); if (!tbody) return; const map = selectedOoLegMap(); tbody.querySelectorAll(".hp-oo-pick").forEach(function (btn) { const inst = String(btn.getAttribute("data-inst") || ""); const leg = map[inst]; const on = !!leg; btn.classList.toggle("is-selected", on); btn.classList.toggle("active", on); btn.setAttribute("aria-pressed", on ? "true" : "false"); const side = String(btn.getAttribute("data-side") || "").toUpperCase(); const base = side === "P" ? "Put" : "Call"; btn.textContent = on ? "腿" + leg + " · " + base : base; btn.title = on ? "已选用为腿" + leg + "(再点其他合约可改选)" : "选用此 " + base; }); tbody.querySelectorAll("tr").forEach(function (tr) { const callBtn = tr.querySelector('.hp-oo-pick[data-side="C"]'); const putBtn = tr.querySelector('.hp-oo-pick[data-side="P"]'); const callOn = !!(callBtn && map[String(callBtn.getAttribute("data-inst") || "")]); const putOn = !!(putBtn && map[String(putBtn.getAttribute("data-inst") || "")]); tr.classList.toggle("hp-oo-row-selected", callOn || putOn); tr.querySelectorAll("td.opt-t-call").forEach(function (td) { td.classList.toggle("hp-oo-side-selected", callOn); }); if (callBtn && callBtn.parentElement) { callBtn.parentElement.classList.toggle("hp-oo-side-selected", callOn); } tr.querySelectorAll("td.opt-t-put").forEach(function (td) { td.classList.toggle("hp-oo-side-selected", putOn); }); if (putBtn && putBtn.parentElement) { putBtn.parentElement.classList.toggle("hp-oo-side-selected", putOn); } }); } function renderTStrikes() { const tbody = $("hp-oo-tbody"); if (!tbody) return; const exp = currentExp("hp-oo-exp-select"); const cols = 9; tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '请选择到期日'; return; } const prepared = []; buildStraddleRows(exp.contracts).forEach(function (row) { const callOk = !!(row.call && matchesOoMoneyFilter(row.call)); const putOk = !!(row.put && matchesOoMoneyFilter(row.put)); if (!callOk && !putOk) return; prepared.push({ strike: row.strike, call: row.call, put: row.put, callOk: callOk, putOk: putOk, }); }); const rows = sliceOoTRows(prepared, indexPx()); const selected = selectedOoLegMap(); if (!rows.length) { tbody.innerHTML = '该筛选下暂无平值/虚值合约'; return; } rows.forEach(function (row) { const tr = document.createElement("tr"); const call = row.call; const put = row.put; const callOk = row.callOk; const putOk = row.putOk; const callAsk = callOk ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated, call.tick_sz) : "—"; const putAsk = putOk ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated, put.tick_sz) : "—"; const callLev = callOk ? calcStrikeAskLeverage(row.strike, call.ask) : "—"; const putLev = putOk ? calcStrikeAskLeverage(row.strike, put.ask) : "—"; const callLeg = callOk ? selected[String(call.inst_id)] : ""; const putLeg = putOk ? selected[String(put.inst_id)] : ""; tr.innerHTML = '' + callAsk + '' + callLev + '' + (callOk ? moneynessBadge(call) : "—") + "" + (callOk ? '" : "—") + '' + row.strike + '' + (putOk ? moneynessBadge(put) : "—") + '' + putAsk + '' + putLev + "" + (putOk ? '" : "—") + ""; tbody.appendChild(tr); }); if (!state.ooStrikeExpandAll && prepared.length > rows.length) { const hint = document.createElement("tr"); hint.className = "opt-strike-hint-row"; hint.innerHTML = '默认显示 Call/Put 各最近 3 档 · 点「显示全部」查看该到期更多行权价(若当前为「仅平值」会自动切到「平/虚」)'; tbody.appendChild(hint); } else if (state.ooStrikeExpandAll && state.ooMoneyFilter === "otm") { const hint = document.createElement("tr"); hint.className = "opt-strike-hint-row"; hint.innerHTML = '当前为「仅虚值」筛选 · 切到「平/虚」可看平值档'; tbody.appendChild(hint); } tbody.querySelectorAll(".hp-oo-pick").forEach(function (btn) { btn.addEventListener("click", function () { const inst = btn.getAttribute("data-inst"); const exp2 = currentExp("hp-oo-exp-select"); const c = (exp2.contracts || []).find(function (x) { return x.inst_id === inst; }); if (!c || !matchesOoMoneyFilter(c)) { alert("期期仅可选平值或虚值,不可选实值"); return; } if (!state.legA) state.legA = c; else if (!state.legB || state.legB.inst_id === state.legA.inst_id) state.legB = c; else { state.legA = c; state.legB = null; } state.ooRecommend = null; syncOoRecommendUI(); renderOoLegs(); }); }); syncOoPickHighlight(); } function renderOoLegs() { function fill(tag, c, infoId, sheetsId) { const info = $(infoId); const sheets = $(sheetsId); if (!info) return; if (!c) { info.textContent = tag + ": 尚未选用"; if (sheets) { sheets.disabled = true; sheets.value = "1"; } return; } info.innerHTML = tag + ": " + c.opt_type + " K" + c.strike + " " + (c.moneyness_label || "") + " · 卖一 " + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + " " + c.inst_id + ""; if (sheets) sheets.disabled = false; } fill("腿A", state.legA, "hp-oo-leg-a-info", "hp-oo-sheets-a"); fill("腿B", state.legB, "hp-oo-leg-b-info", "hp-oo-sheets-b"); autoFillOoSheets(); syncOoPickHighlight(); } function ooSheets(id) { const n = Number(($(id) && $(id).value) || 1); return n > 0 ? n : 1; } function updateOoPremiumLine() { const line = $("hp-oo-prem-line"); if (!line) return; if (!state.legA && !state.legB) { line.textContent = ""; return; } function prem(c, sheets) { if (!c) return 0; return Number(c.ask || 0) * sheets * Number(c.ct_mult || 0.01); } const a = prem(state.legA, ooSheets("hp-oo-sheets-a")); const b = prem(state.legB, ooSheets("hp-oo-sheets-b")); line.textContent = "预估权利金 A " + fmt(a, 4) + " + B " + fmt(b, 4) + " ≈ " + fmt(a + b, 4) + " USDC"; } function legPayload(c, sheets) { return { opt_type: c.opt_type, strike: c.strike, sheets: sheets, ct_mult: c.ct_mult || 0.01, ask: c.ask, inst_id: c.inst_id, }; } function setUnderlying(uly, forceReload) { const next = (uly || "ETH").toUpperCase(); const changed = next !== state.underlying; state.underlying = next; syncUnderlyingUI(); if (!changed && !forceReload) return; state.selected = null; state.legA = null; state.legB = null; state.ooRecommend = null; syncOoRecommendUI(); if ($("hp-entry")) $("hp-entry").value = ""; if ($("hp-contracts")) $("hp-contracts").value = ""; if ($("hp-tp")) $("hp-tp").value = ""; if ($("hp-sl")) $("hp-sl").value = ""; if ($("hp-profit-rr")) $("hp-profit-rr").value = "2"; if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; if ($("hp-premium-line")) $("hp-premium-line").textContent = ""; if ($("hp-oo-sheets-a")) { $("hp-oo-sheets-a").value = "1"; $("hp-oo-sheets-a").disabled = true; } if ($("hp-oo-sheets-b")) { $("hp-oo-sheets-b").value = "1"; $("hp-oo-sheets-b").disabled = true; } renderOoLegs(); void refreshAll(); } async function runPreview() { const isOo = state.mode === "options_options"; const tbody = $("hp-result-tbody"); const summary = $("hp-preview-summary"); const midTh = $("hp-preview-mid-th"); const title = $("hp-preview-title"); state.previewOk = false; state.previewPlanType = isOo ? "options_options" : "perp_options"; syncPreviewStartBtn(); if (midTh) midTh.textContent = isOo ? "腿盈亏" : "永续/腿盈亏"; if (title) title.textContent = isOo ? "情景测算 · 期期" : "情景测算 · 永期"; if (summary) summary.textContent = ""; if (tbody) tbody.innerHTML = '计算中…'; openPreviewModal(); try { let body; if (isOo) { if (!state.legA || !state.legB) throw new Error("请选用两条期权腿"); if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) { throw new Error("期期两腿须为平值或虚值,不可选实值"); } const rr = Number(($("hp-profit-rr") && $("hp-profit-rr").value) || 0); if (!(rr > 0)) throw new Error("请填写盈亏比(须大于0,默认2)"); body = { plan_type: "options_options", profit_rr: rr, index_px: indexPx() || 0, leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")), leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")), }; } else { if (!state.selected) throw new Error("请选用期权腿"); if (isOptionPrimary()) { const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01); 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"); body = { plan_type: "perp_options", option_primary: true, direction: getDirection(), entry: indexPx() || Number((state.market && state.market.mark) || 0), contracts: sized.contracts, sheets: sized.sheets, contract_size: (state.market && state.market.contract_size) || 0.01, opt_type: state.selected.opt_type, strike: state.selected.strike, ct_mult: state.selected.ct_mult || 0.01, ask: state.selected.ask, index_px: indexPx() || 0, premium_budget: numInput("hp-premium-budget", 0), 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), leverage: numInput("hp-perp-leverage", 100), moneyness: opMoneyKind(), hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null, }; } else { const mSel = (state.selected.moneyness || "").toLowerCase(); if (mSel === "otm") throw new Error("永期保险腿须为实值或平值,不可选虚值"); const entry = Number(($("hp-entry") && $("hp-entry").value) || 0); const tp = Number(($("hp-tp") && $("hp-tp").value) || 0); const sl = Number(($("hp-sl") && $("hp-sl").value) || 0); const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || 0); const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1); if (!entry || !tp || !sl || !contracts) throw new Error("请完整填写开仓/止盈/止损/张数"); body = { plan_type: "perp_options", direction: getDirection(), entry: entry, tp: tp, sl: sl, contracts: contracts, contract_size: (state.market && state.market.contract_size) || 0.01, opt_type: state.selected.opt_type, strike: state.selected.strike, sheets: sheets, ct_mult: state.selected.ct_mult || 0.01, ask: state.selected.ask, index_px: indexPx() || entry, }; } } const d = await apiJson("/api/hedge-plan/preview", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); setGateLine(d.gates); const s = d.summary || {}; if (summary) { if (d.plan_type === "perp_options" && (d.option_primary || s.opt_target_total != null)) { const sz = d.sizing || {}; summary.innerHTML = "期权目标净利 " + fmtPnlHtml(s.opt_target_total) + " · 永续目标净利 " + fmtPnlHtml(s.perp_target_total) + " · 保费 " + fmt(s.premium_paid) + (sz.eth_qty != null ? " · ETH " + fmt(sz.eth_qty, 2) : "") + (s.perp_direction ? " · 永续" + (s.perp_direction === "short" ? "空" : "多") : ""); } else if (d.plan_type === "perp_options") { summary.innerHTML = "止盈合计 " + fmtPnlHtml(s.tp_total) + " · 止损合计 " + fmtPnlHtml(s.sl_total) + " · 保费 " + fmt(s.premium_paid) + (s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : ""); } else { const rrTarget = s.profit_rr != null ? s.profit_rr : null; let rrLine = ""; if (rrTarget != null) { rrLine = " · 目标盈亏比 " + fmt(rrTarget, 2) + '(盈利金额/总权利金)'; } else if (s.rr_at_up != null || s.rr_at_down != null) { rrLine = " · 盈亏比 上破 " + fmtRr(s.rr_at_up) + (s.at_target_down_total != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") + '(亏=全额保费 ' + fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) + ")"; } const aTot = s.at_rr_a_full_total != null ? s.at_rr_a_full_total : s.at_target_up_total; const bTot = s.at_rr_b_full_total != null ? s.at_rr_b_full_total : s.at_target_down_total; summary.innerHTML = (rrTarget != null ? "腿A达标 " : "上破 ") + fmtPnlHtml(aTot) + (bTot != null ? (rrTarget != null ? " · 腿B达标 " : " · 下破 ") + fmtPnlHtml(bTot) : "") + " · 到期现价 " + fmtPnlHtml(s.expiry_flat_total) + " · 保费 " + fmt(s.premium_paid) + rrLine + (s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : ""); } } if (!tbody) return; tbody.innerHTML = ""; (d.scenarios || []).forEach(function (sc) { const tr = document.createElement("tr"); let mid; if (sc.perp_pnl != null) { mid = "永续 " + fmtPnlHtml(sc.perp_pnl); } else { mid = "A " + fmtPnlHtml(sc.leg_a_pnl) + " / B " + fmtPnlHtml(sc.leg_b_pnl); } const optCol = sc.options_pnl != null ? fmtPnlHtml(sc.options_pnl) : "—"; tr.innerHTML = "" + (sc.label || sc.id) + "" + fmt(sc.spot) + "" + mid + "" + optCol + "" + fmtPnlHtml(sc.total) + "" + (sc.note || "") + ""; tbody.appendChild(tr); }); state.previewOk = true; syncPreviewStartBtn(); } catch (e) { state.previewOk = false; syncPreviewStartBtn(); if (tbody) tbody.innerHTML = '' + (e.message || e) + ""; if (summary) summary.textContent = ""; } } function hardenAmountAutofill(ids) { (ids || []).forEach(function (id) { const el = $(id); if (!el) return; function wipe() { const v = String(el.value || "").trim(); // 浏览器常把登录用户名(如 dekun)灌进数量框 if (/^[a-z][a-z0-9._-]{1,31}$/i.test(v)) el.value = ""; } wipe(); const lockRo = id.indexOf("xfer") >= 0 || el.hasAttribute("readonly"); if (lockRo) { el.setAttribute("readonly", "readonly"); el.addEventListener("focus", function () { el.removeAttribute("readonly"); }); el.addEventListener("blur", function () { if (!el.value) el.setAttribute("readonly", "readonly"); }); } setTimeout(wipe, 200); setTimeout(wipe, 800); setTimeout(wipe, 2000); }); } function bind() { document.querySelectorAll(".hp-tab").forEach(function (b) { b.addEventListener("click", function () { const next = b.getAttribute("data-tab") || pickDefaultTab(); if (next === "perp_options" && !showPerp) return; if (next === "options_options" && !showOo) return; state.tab = next; syncTabUI(); if (state.tab === "perp_options" || state.tab === "options_options") { void loadGates(); if (state.tab === "perp_options") void refreshPoStrategyStatus(); } else if (state.tab === "active") { void loadActivePlans(); } else if (state.tab === "history") { void loadHistory(); } else if (state.tab === "stats") { void loadStats(); } else { const el = $("hp-gate-line"); if (el) el.textContent = ""; } }); }); document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) { b.addEventListener("click", function () { setUnderlying(b.getAttribute("data-uly") || "ETH", true); }); }); document.querySelectorAll(".hp-money-btn").forEach(function (b) { b.addEventListener("click", function () { const m = b.getAttribute("data-money") || "itm"; if (m === "otm" && !isOptionPrimary()) { alert("永期保险腿仅允许实值或平值;请在 env 将 HEDGE_PLAN_OPTION_PRIMARY=true"); return; } state.moneyFilter = m === "otm" ? "otm" : 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(); renderListStrikes(); }); } if ($("hp-opt-leverage")) { $("hp-opt-leverage").addEventListener("input", function () { state.opLevTouched = true; if (isOptionPrimary()) renderListStrikes(); }); } if ($("hp-opt-perp-ratio")) { $("hp-opt-perp-ratio").addEventListener("input", function () { state.opRatioTouched = true; if (state.selected) { const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01); if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets); } void loadMarket(); }); } let _poChainReloadTimer = null; function schedulePoChainReload() { if (_poChainReloadTimer) clearTimeout(_poChainReloadTimer); _poChainReloadTimer = setTimeout(function () { _poChainReloadTimer = null; void loadChain(); }, 350); } ["hp-premium-budget", "hp-strike-interval", "hp-min-hours", "hp-opt-target-pts", "hp-perp-target-pts"].forEach( function (id) { const el = $(id); if (!el) return; el.addEventListener("input", function () { if (id === "hp-min-hours" || id === "hp-strike-interval") { schedulePoChainReload(); } if (id === "hp-premium-budget") renderListStrikes(); if (state.selected && id === "hp-premium-budget") { const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01); if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets); void loadMarket(); } updatePerpPnlHint(); }); } ); document.querySelectorAll(".hp-oo-money-btn").forEach(function (b) { b.addEventListener("click", function () { const m = b.getAttribute("data-oo-money") || "atm_otm"; if (m === "itm") { alert("期期两腿仅允许平值或虚值"); return; } state.ooMoneyFilter = m; syncMoneyUI(); renderTStrikes(); }); }); 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) { alert("当前到期日无可用实值/平值合约,请换到期或刷新链"); return; } pickContract(c); }); } if ($("hp-oo-recommend-atm")) { $("hp-oo-recommend-atm").addEventListener("click", function () { const pair = pickOoTemplate("atm_straddle"); if (!pair) { alert("无法推荐平值跨式,请确认到期日与链数据"); return; } state.legA = pair.call; state.legB = pair.put; state.ooRecommend = "atm_straddle"; syncOoRecommendUI(); renderOoLegs(); autoFillOoSheets(); updateOoPremiumLine(); }); } if ($("hp-oo-recommend-otm")) { $("hp-oo-recommend-otm").addEventListener("click", function () { const pair = pickOoTemplate("double_otm"); if (!pair) { alert("无法推荐双虚值,请确认到期日与链数据"); return; } state.legA = pair.call; state.legB = pair.put; state.ooRecommend = "double_otm"; syncOoRecommendUI(); renderOoLegs(); autoFillOoSheets(); updateOoPremiumLine(); }); } document.querySelectorAll(".hp-po-dir").forEach(function (b) { b.addEventListener("click", function () { setDirection(b.getAttribute("data-dir") || "long", true); }); }); ["hp-entry", "hp-tp", "hp-sl", "hp-contracts"].forEach(function (id) { const el = $(id); if (el) el.addEventListener("input", updatePerpPnlHint); }); syncOptionPrimaryUI(); if ($("hp-refresh")) $("hp-refresh").addEventListener("click", function () { void refreshAll(); }); if ($("hp-load-chain")) $("hp-load-chain").addEventListener("click", function () { void loadChain(); }); if ($("hp-oo-load-chain")) $("hp-oo-load-chain").addEventListener("click", function () { void loadChain(); }); if ($("hp-oo-expand-all")) { $("hp-oo-expand-all").addEventListener("click", function () { state.ooStrikeExpandAll = !state.ooStrikeExpandAll; // 「仅平值」时本来就只有 1~3 档,展开几乎无变化;展开时自动切到平/虚以便看到全部可选档 if (state.ooStrikeExpandAll && state.ooMoneyFilter === "atm") { state.ooMoneyFilter = "atm_otm"; syncMoneyUI(); } syncOoExpandUI(); renderTStrikes(); }); } if ($("hp-exp-select")) $("hp-exp-select").addEventListener("change", renderListStrikes); if ($("hp-oo-exp-select")) $("hp-oo-exp-select").addEventListener("change", renderTStrikes); if ($("hp-sheets")) $("hp-sheets").addEventListener("input", updatePremiumLine); ["hp-oo-sheets-a", "hp-oo-sheets-b"].forEach(function (id) { const el = $(id); if (el) el.addEventListener("input", updateOoPremiumLine); }); document.querySelectorAll(".hp-oo-size-mode").forEach(function (b) { b.addEventListener("click", function () { state.ooSheetsMode = b.getAttribute("data-oo-size") || "same_sheets"; syncOoSizeModeUI(); autoFillOoSheets(); updateOoBudgetLine(null); }); }); document.querySelectorAll(".hp-oo-close-mode").forEach(function (b) { b.addEventListener("click", function () { if (!state.ooCloseModeEnabled) return; state.ooCloseMode = b.getAttribute("data-oo-close") || "close_all"; state._ooCloseModeTouched = true; syncOoCloseModeUI(); updateOoBudgetLine(null); }); }); if ($("hp-oo-xfer-btn")) { $("hp-oo-xfer-btn").addEventListener("click", function () { const amount = Number(($("hp-oo-xfer-amount") && $("hp-oo-xfer-amount").value) || 0); if (!(amount > 0)) { setOoXferMsg("请输入有效数量", "err"); return; } void submitOoUsdcTransfer(amount); }); } if ($("hp-oo-xfer-all")) { $("hp-oo-xfer-all").addEventListener("click", function () { const dir = ooXferDir(); const max = dir === "trading_to_funding" ? Number(state.tradingUsdc || 0) : Number(state.fundingUsdc || 0); if (!(max > 0)) { setOoXferMsg("划出账户可用余额不足", "err"); return; } const amt = Math.floor(max * 100) / 100; const label = dir === "trading_to_funding" ? "交易 → 资金" : "资金 → 交易"; if (!window.confirm("确认全部划转?\n方向:" + label + "\n数量:" + amt + " USDC")) return; const amtEl = $("hp-oo-xfer-amount"); if (amtEl) { amtEl.removeAttribute("readonly"); amtEl.value = String(amt); } void submitOoUsdcTransfer(amt); }); } hardenAmountAutofill([ "hp-oo-xfer-amount", "hp-entry", "hp-contracts", "hp-tp", "hp-sl", "hp-sheets", "hp-profit-rr", ]); if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () { state.mode = "perp_options"; if (isOptionPrimary()) void startOptionPrimaryWatch(); else void runPreview(); }); if ($("hp-preview-btn-oo")) $("hp-preview-btn-oo").addEventListener("click", function () { state.mode = "options_options"; void runPreview(); }); if ($("hp-preview-cancel")) $("hp-preview-cancel").addEventListener("click", closePreviewModal); if ($("hp-preview-cancel-x")) $("hp-preview-cancel-x").addEventListener("click", closePreviewModal); if ($("hp-preview-start")) $("hp-preview-start").addEventListener("click", function () { const planType = state.previewPlanType || state.mode; void startPlan(planType, true); }); const previewModal = $("hp-preview-modal"); if (previewModal) { previewModal.addEventListener("click", function (ev) { if (ev.target === previewModal) closePreviewModal(); }); } if ($("hp-detail-close")) $("hp-detail-close").addEventListener("click", closeModal); const modal = $("hp-detail-modal"); if (modal) { modal.addEventListener("click", function (ev) { if (ev.target === modal) closeModal(); }); } } async function loadHistory() { const tbody = $("hp-history-tbody"); if (!tbody) return; try { const d = await apiJson("/api/hedge-plan/history"); const rows = d.plans || []; if (!rows.length) { tbody.innerHTML = '暂无已结束计划'; return; } tbody.innerHTML = ""; rows.forEach(function (p) { const tr = document.createElement("tr"); const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期"; const contracts = p.contracts_summary || "—"; const pnl = p.realized_pnl_total; const pnlCls = pnl == null || Number.isNaN(Number(pnl)) ? "" : Number(pnl) >= 0 ? "hp-pnl-pos" : "hp-pnl-neg"; tr.innerHTML = "#" + p.id + "" + typeLabel + "" + (p.underlying || "") + "" + contracts + "" + (p.status || "") + '' + fmt(pnl) + "" + reasonLabel(p.close_reason) + "" + (p.opened_at || "—") + "" + (p.closed_at || "—") + '' + ' ' + '' + ""; tbody.appendChild(tr); }); tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) { btn.addEventListener("click", function () { void showPlanDetail(Number(btn.getAttribute("data-id"))); }); }); tbody.querySelectorAll(".hp-btn-del").forEach(function (btn) { btn.addEventListener("click", function () { void deletePlan(Number(btn.getAttribute("data-id"))); }); }); } catch (e) { tbody.innerHTML = '' + (e.message || e) + ""; } } function activeTargetLabel(p) { if (p.plan_type === "perp_options" && (p.option_primary == 1 || p.option_primary === true || Number(p.option_primary) === 1)) { return ( "期权K±" + fmt(p.option_target_points, 0) + " · 永续K±" + fmt(p.perp_target_points, 0) ); } if (p.plan_type === "perp_options") { return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl); } if (p.profit_rr != null && Number(p.profit_rr) > 0) { return "盈亏比 " + fmt(p.profit_rr, 2); } return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price); } function activeStatusLabel(p) { if ((p.status || "") === "watching") { return '盯盘中'; } if ((p.status || "") === "partial") { return '半腿待补'; } if ((p.status || "") === "opening") { return '开仓中'; } return '进行中'; } function completeLegButtonHtml(p) { if ((p.status || "") !== "partial") return ""; const role = p.missing_leg || ""; let label = ""; if (role === "perp") label = "补开永续"; else if (role === "option_b") label = "补开腿B"; else if (role === "option_hedge" || role === "option_a") label = "补开期权"; else return ""; return ( ' " ); } async function completeMissingLeg(planId) { if (!window.confirm("确认补开缺失腿并真实下单?")) return; try { const d = await apiJson("/api/hedge-plan/" + planId + "/complete-leg", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); alert("补开成功 #" + (d.plan_id || planId) + " · 已完全成交并进入进行中"); void loadActivePlans(); void loadGates(); } catch (e) { alert(e.message || String(e)); } } async function endActivePlan(planId) { if ( !window.confirm( "确认结束计划 #" + planId + "?\n不会自动平仓;未成交/待补腿将标为未成交取消。\n已有持仓请自行平掉。" ) ) { return; } try { const d = await apiJson("/api/hedge-plan/" + planId + "/end", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); alert(d.msg || "计划已结束 #" + (d.plan_id || planId)); void loadActivePlans(); void loadHistory(); void loadGates(); void refreshPoStrategyStatus(); } catch (e) { alert(e.message || String(e)); } } function legStatusLabel(st) { const map = { open: "持仓中", pending: "待补/未成交", cancelled: "未成交取消", canceled: "未成交取消", closed: "已平仓", }; return map[st] || st || "—"; } async function loadActivePlans() { const tbody = $("hp-active-tbody"); if (!tbody) return; try { const d = await apiJson("/api/hedge-plan/active"); const rows = d.plans || []; if (!rows.length) { tbody.innerHTML = '暂无进行中的计划'; return; } tbody.innerHTML = ""; rows.forEach(function (p) { const tr = document.createElement("tr"); const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期"; const contracts = p.contracts_summary || "—"; tr.innerHTML = "#" + p.id + "" + typeLabel + "" + (p.underlying || "") + "" + contracts + "" + activeStatusLabel(p) + "" + activeTargetLabel(p) + "" + (p.opened_at || "—") + '' + completeLegButtonHtml(p) + ' ' + ''; tbody.appendChild(tr); }); tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) { btn.addEventListener("click", function () { void showPlanDetail(Number(btn.getAttribute("data-id"))); }); }); tbody.querySelectorAll(".hp-btn-complete").forEach(function (btn) { btn.addEventListener("click", function () { void completeMissingLeg(Number(btn.getAttribute("data-id"))); }); }); tbody.querySelectorAll(".hp-btn-end").forEach(function (btn) { btn.addEventListener("click", function () { void endActivePlan(Number(btn.getAttribute("data-id"))); }); }); } catch (e) { tbody.innerHTML = '' + (e.message || e) + ""; } } function reasonLabel(r) { const map = { perp_tp: "永续止盈", perp_sl: "永续止损", oo_expiry_loss: "期期到期亏损", oo_expiry_win: "期期到期盈利", target_win_leg: "期期平盈利腿", target_up_win_leg: "期期上破·平盈利腿", target_down_win_leg: "期期下破·平盈利腿", profit_rr_win_leg: "期期盈亏比达标·平盈利腿", oo_rest_closing: "期期残值平·清亏损腿中", oo_rest_closed: "期期残值平·两腿已平", orphaned_after_tp: "止盈后持有至到期", orphaned_option_expiry: "残腿到期", hold_to_expiry: "持有至到期", expiry: "到期", manual: "人工结束", manual_end: "人工结束", partial_fail: "半腿失败", cancelled: "已取消", unfilled: "未成交", }; return map[r] || r || "—"; } function roleLabel(role) { const map = { perp: "永续腿", option_hedge: "保险期权", option_a: "期期腿A", option_b: "期期腿B", }; return map[role] || role || "—"; } function closeModal() { const m = $("hp-detail-modal"); if (m) m.hidden = true; } async function showPlanDetail(planId) { const modal = $("hp-detail-modal"); const body = $("hp-detail-body"); const title = $("hp-detail-title"); if (!modal || !body) return; modal.hidden = false; body.innerHTML = '

加载中…

'; if (title) title.textContent = "成交细节 #" + planId; try { const d = await apiJson("/api/hedge-plan/" + planId); const p = d.plan || {}; const legs = d.legs || []; const typeLabel = p.plan_type === "perp_options" ? "永期对冲" : "期期对冲"; let html = ""; html += '
'; html += "
类型 " + typeLabel + "
"; html += "
标的 " + (p.underlying || "—"); if (p.direction) html += " · " + (p.direction === "long" ? "做多" : "做空"); html += "
"; html += "
状态 " + (p.status || "—") + " / " + reasonLabel(p.close_reason) + "
"; html += "
时间 " + (p.opened_at || "—") + " → " + (p.closed_at || "—") + "
"; html += "
盈亏 永续 " + fmt(p.realized_pnl_perp) + " · 期权 " + fmt(p.realized_pnl_options) + " · 合计 = 0 ? "hp-pnl-pos" : "hp-pnl-neg") + '">' + fmt(p.realized_pnl_total) + " ≈U
"; if (p.plan_type === "perp_options") { html += "
参考价 开 " + fmt(p.entry_mark) + " · 止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl) + " · 杠杆 " + fmt(p.leverage, 0) + "x · 张数 " + fmt(p.perp_size, 4) + "
"; } else { if (p.profit_rr != null && Number(p.profit_rr) > 0) { html += "
盈亏比 " + fmt(p.profit_rr, 2) + " (盈利金额/总权利金)
"; } else { html += "
目标价 上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price) + "
"; } } html += "
权利金合计 " + fmt(p.premium_total, 4) + " USDC
"; html += "
合约摘要 " + (d.contracts_summary || "—") + "
"; html += "
"; html += ''; html += ""; html += ""; if (!legs.length) { html += ''; } else { legs.forEach(function (leg) { const contract = leg.leg_role === "perp" ? leg.symbol || "—" : leg.inst_id || "—"; const side = leg.leg_role === "perp" ? leg.side || "—" : (leg.opt_type || "") + (leg.strike != null ? " K" + fmt(leg.strike, 0) : ""); html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; }); } html += "
角色合约名称方向/类型数量开仓价权利金状态腿盈亏成交号平仓原因
无腿记录
" + roleLabel(leg.leg_role) + "" + contract + "" + side + "" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + "" + fmt(leg.avg_open, 4) + "" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + "" + legStatusLabel(leg.status) + "" + fmt(leg.realized_pnl, 4) + "" + (leg.exchange_ord_id || "—") + "" + reasonLabel(leg.close_reason) + "
"; if (p.note) { html += '

备注 ' + String(p.note) + "

"; } body.innerHTML = html; } catch (e) { body.innerHTML = '

' + (e.message || e) + "

"; } } async function deletePlan(planId) { if (!window.confirm("确认删除历史计划 #" + planId + "?此操作不可恢复。")) return; try { await apiJson("/api/hedge-plan/" + planId, { method: "DELETE" }); await loadHistory(); if (state.tab === "stats") await loadStats(); } catch (e) { window.alert(e.message || String(e)); } } function metricCard(title, m) { if (!m || !m.count) { return ( '

' + title + '

暂无已结束样本

' ); } const wr = m.win_rate == null ? "—" : (Number(m.win_rate) * 100).toFixed(1) + "%"; let pf = "—"; if (m.profit_factor_infinite) pf = "∞"; else if (m.profit_factor != null) pf = fmt(m.profit_factor, 2); return ( '

' + title + "

" ); } async function loadStats() { const box = $("hp-stats-box"); if (!box) return; try { const d = await apiJson("/api/hedge-plan/stats"); const by = d.by_type || {}; let html = '
'; html += '

总览

活跃 ' + (d.active || 0) + " · 已结 " + (d.closed_count || 0) + ' · 合计 ' + fmt(d.closed_pnl_total) + " ≈U

"; html += metricCard("永期对冲", by.perp_options); html += metricCard("期期对冲", by.options_options); html += "
"; const poB = (by.perp_options && by.perp_options.buckets) || {}; const ooB = (by.options_options && by.options_options.buckets) || {}; if ((poB.tp && poB.tp.count) || (poB.sl && poB.sl.count) || (ooB.expiry_loss && ooB.expiry_loss.count)) { html += '
'; if (poB.tp && poB.tp.count) html += metricCard("永期·止盈桶", poB.tp); if (poB.sl && poB.sl.count) html += metricCard("永期·止损桶", poB.sl); if (ooB.expiry_loss && ooB.expiry_loss.count) html += metricCard("期期·到期亏损", ooB.expiry_loss); if (ooB.expiry_win && ooB.expiry_win.count) html += metricCard("期期·到期盈利", ooB.expiry_win); html += "
"; } box.innerHTML = html; } catch (e) { box.textContent = e.message || String(e); } } async function startOptionPrimaryWatch() { try { const optPts = numInput("hp-opt-target-pts", NaN); const perpPts = numInput("hp-perp-target-pts", NaN); const prem = numInput("hp-premium-budget", 0); const optLev = numInput("hp-opt-leverage", 200); if (!(prem > 0)) throw new Error("请填写权利金预算"); if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)"); if (!(optLev > 0)) throw new Error("请填写期权杠杆门槛"); if (!(state.market && state.market.exchange_symbol)) throw new Error("永续行情未就绪,请先刷新"); if (!state.canStart) { throw new Error("当前不可启动(门禁未满足),请查看上方提示"); } const msg = "确认启动盯盘?\n" + "类型 " + (opMoneyKind() === "otm" ? "虚值" : opMoneyKind() === "atm" ? "平值" : "实/平") + " · 间隔 " + numInput("hp-strike-interval", 15) + " · 杠杆≥" + optionPrimaryMinLev() + "\n达标后自动开仓(非现场立即开)"; if (!window.confirm(msg)) return; const body = { plan_type: "perp_options", option_primary: true, watch_entry: 1, underlying: state.underlying, direction: getDirection(), exchange_symbol: state.market.exchange_symbol, contract_size: state.market.contract_size || 0.01, index_px: indexPx() || Number(state.market.mark || 0), entry: indexPx() || Number(state.market.mark || 0), premium_budget: prem, option_perp_ratio: numInput("hp-opt-perp-ratio", 4), 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: optLev, leverage: numInput("hp-perp-leverage", 100), moneyness: opMoneyKind(), }; const d = await apiJson("/api/hedge-plan/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); setGateLine(d.gates); setPoStrategyStatus("watching", d.plan_id || null); alert((d.msg || "已启动盯盘") + (d.plan_id ? "\n计划 #" + d.plan_id : "")); void refreshPoStrategyStatus(); void loadGates(); } catch (e) { alert(e.message || String(e)); } } async function startPlan(planType, fromPreviewModal) { const isOo = planType === "options_options"; const startBtn = $("hp-preview-start"); try { let body; if (isOo) { if (!state.legA || !state.legB) throw new Error("请选用两条期权腿"); if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) { throw new Error("期期两腿须为平值或虚值,不可选实值"); } const rr = Number(($("hp-profit-rr") && $("hp-profit-rr").value) || 0); if (!(rr > 0)) throw new Error("请填写盈亏比(须大于0,默认2)"); body = { plan_type: "options_options", underlying: state.underlying, profit_rr: rr, index_px: indexPx() || 0, oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry", oo_sheets_mode: state.ooSheetsMode || "same_sheets", leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")), leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")), }; } else { if (!state.selected) throw new Error("请选用期权腿"); if (isOptionPrimary()) { const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01); 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); body = { plan_type: "perp_options", option_primary: true, underlying: state.underlying, direction: getDirection(), entry: entry, contracts: sized.contracts, sheets: sized.sheets, contract_size: (state.market && state.market.contract_size) || 0.01, ct_mult: state.selected.ct_mult || 0.01, opt_inst_id: state.selected.inst_id, opt_type: state.selected.opt_type, strike: state.selected.strike, ask: state.selected.ask, index_px: entry, exchange_symbol: (state.market && state.market.exchange_symbol) || "", leverage: numInput("hp-perp-leverage", 100), option_leverage: numInput("hp-opt-leverage", 100), premium_budget: numInput("hp-premium-budget", 0), 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(), hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null, }; } else { const m = (state.selected.moneyness || "").toLowerCase(); if (m === "otm") throw new Error("永期保险腿须为实值或平值,不可选虚值"); const entry = Number(($("hp-entry") && $("hp-entry").value) || 0); const tp = Number(($("hp-tp") && $("hp-tp").value) || 0); const sl = Number(($("hp-sl") && $("hp-sl").value) || 0); const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || 0); const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1); if (!entry || !tp || !sl || !contracts) throw new Error("请完整填写开仓/止盈/止损/张数"); body = { plan_type: "perp_options", underlying: state.underlying, direction: getDirection(), entry: entry, tp: tp, sl: sl, contracts: contracts, sheets: sheets, opt_inst_id: state.selected.inst_id, opt_type: state.selected.opt_type, strike: state.selected.strike, ask: state.selected.ask, index_px: indexPx() || entry, exchange_symbol: (state.market && state.market.exchange_symbol) || "", leverage: 10, margin: state.market && state.market.full_margin_sizing && state.market.full_margin_sizing.margin_capital, }; } } if (!fromPreviewModal) { if (!window.confirm("确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)")) return; } if (startBtn) startBtn.disabled = true; const d = await apiJson("/api/hedge-plan/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); setGateLine(d.gates); closePreviewModal(); const refreshHint = d.refresh && d.refresh.msg ? "\n" + String(d.refresh.msg) : ""; if (d.partial) { alert( (d.msg || "半腿失败,已挂待补") + (d.plan_id ? "\n计划 #" + d.plan_id : "") + refreshHint + "\n请到「进行中的计划」补开缺失腿" ); state.tab = "active"; syncTabUI(); void loadActivePlans(); } else { alert( "计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : "") + refreshHint ); } void loadGates(); if (isOo) void loadChain(); } catch (e) { alert(e.message || String(e)); syncPreviewStartBtn(); } } async function refreshAll() { if (state.tab === "perp_options" || state.tab === "options_options") { await loadGates(); } try { await loadMarket(); } catch (e) { const q = $("hp-perp-quote"); if (q) q.textContent = e.message || String(e); } try { await loadChain(); } catch (e) { const tbody = $("hp-strike-tbody"); if (tbody) tbody.innerHTML = '' + (e.message || e) + ""; } if (state.tab === "perp_options") void refreshPoStrategyStatus(); } syncTabUI(); syncUnderlyingUI(); syncMoneyUI(); syncOoRecommendUI(); syncOoExpandUI(); syncPoDirUI(); syncOoSizeModeUI(); syncOoCloseModeUI(); applyBudgetBuffer(state.budgetBuffer); updateOoBudgetLine(null); bind(); syncOptionPrimaryUI(); void refreshAll().then(function () { void refreshPoStrategyStatus(); }); setInterval(function () { if (state.tab === "perp_options" && isOptionPrimary()) void refreshPoStrategyStatus(); }, 15000); })();