/** * 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: "all", 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", 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"; } function matchesMoneyFilter(c) { const f = state.moneyFilter || "all"; if (f === "all") return true; const m = (c.moneyness || "").toLowerCase(); if (f === "itm") return m === "itm" || m === "atm"; if (f === "otm") return m === "otm"; return true; } function optTypeForDirection(dir) { return dir === "short" ? "C" : "P"; } 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() { document.querySelectorAll(".hp-money-btn").forEach(function (b) { const on = b.getAttribute("data-money") === state.moneyFilter; b.classList.toggle("active", on); }); } 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) ); 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 q = $("hp-perp-quote"); if (q) { q.innerHTML = "可用 " + fmt(d.available_usdt, 2) + " USDT · 卖一 " + fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + " · 面值 " + fmt(d.contract_size, 4) + " · 精度 " + amtPrec + " 位"; } 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 (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 (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref; if (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; 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; sel.innerHTML = ''; (chain.expiries || []).forEach(function (e) { const opt = document.createElement("option"); opt.value = String(e.exp_time); const dt = new Date(Number(e.exp_time)); opt.textContent = dt.toLocaleString(); sel.appendChild(opt); }); if (prev) sel.value = prev; if (!sel.value && chain.expiries && chain.expiries[0]) { sel.value = String(chain.expiries[0].exp_time); } } async function loadChain() { const d = await apiJson( "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying) ); 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) { const idx = Number(d.index_px); if ($("hp-target-up") && !$("hp-target-up").value) { $("hp-target-up").value = String(Math.round(idx * 1.03)); } if ($("hp-target-down") && !$("hp-target-down").value) { $("hp-target-down").value = String(Math.round(idx * 0.97)); } } } 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; 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(); } 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 = '
" +
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();
}
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;
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-target-up")) $("hp-target-up").value = "";
if ($("hp-target-down")) $("hp-target-down").value = "";
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 = '" +
contracts +
"" +
contracts +
"加载中…
'; 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 += '" +
(d.contracts_summary || "—") +
"| 角色 | 合约名称 | 方向/类型 | 数量 | 开仓价 | 权利金 | 状态 | 腿盈亏 | 成交号 | 平仓原因 | "; html += "
|---|---|---|---|---|---|---|---|---|---|
| 无腿记录 | |||||||||
| " + roleLabel(leg.leg_role) + " | "; html += "" + contract + " | ";
html += "" + side + " | "; html += "" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + " | "; html += "" + fmt(leg.avg_open, 4) + " | "; html += "" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + " | "; html += "" + legStatusLabel(leg.status) + " | "; html += "" + fmt(leg.realized_pnl, 4) + " | "; html += "" + (leg.exchange_ord_id || "—") + " | ";
html += "" + reasonLabel(leg.close_reason) + " | "; 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 ( '暂无已结束样本
活跃 ' + (d.active || 0) + " · 已结 " + (d.closed_count || 0) + ' · 合计 ' + fmt(d.closed_pnl_total) + " ≈U