/** * OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁. */ (function () { const root = document.getElementById("hedge-plan-root"); if (!root) return; const state = { tab: "perp_options", mode: "perp_options", underlying: root.getAttribute("data-default-underly") || "ETH", moneyFilter: "all", chain: null, selected: null, legA: null, legB: null, market: null, }; 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).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 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() { const tab = state.tab || "perp_options"; 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", "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; } } 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(" · "); const start = $("hp-start-btn"); if (start) start.disabled = !gates.can_start; } 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; 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 oo = $("hp-oo-bal-line"); if (oo) oo.textContent = line; } async function loadGates() { try { const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode)); setGateLine(d); } catch (e) { setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false }); } } async function loadMarket() { const dir = ($("hp-direction") && $("hp-direction").value) || "long"; 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 q = $("hp-perp-quote"); if (q) { q.innerHTML = "" + (d.base || state.underlying) + " · " + acctLabel + "可用 " + fmt(d.available_usdt, 2) + " U
标记 " + fmt(d.mark, 2) + " · 最新 " + fmt(d.last, 2) + " · 卖一 " + fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + " · 面值 " + fmt(d.contract_size, 4); } const sz = $("hp-sizing-line"); if (sz) { if (d.full_margin_sizing) { const s = d.full_margin_sizing; sz.textContent = "全仓建议(" + acctLabel + "):保证金 " + fmt(s.margin_capital, 2) + "U × " + s.leverage + "x → 名义 " + fmt(s.notional_value, 2) + "U · 建议约 " + fmt(d.suggest_contracts, 4) + " 张"; } else { sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓."; } } const entry = $("hp-entry"); if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref; const contracts = $("hp-contracts"); if (contracts && d.suggest_contracts != null && !contracts.value) { contracts.value = d.suggest_contracts; } const label = $("hp-opt-type-label"); if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put"; } 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 uly = d.underlying || state.underlying; const idx = $("hp-index-line"); if (idx) { idx.textContent = "指数 " + uly + " " + fmt(d.index_px, 2) + " · " + (d.inst_family || "") + " · 实值含平值"; } const ooIdx = $("hp-oo-index"); if (ooIdx) ooIdx.textContent = "指数 " + uly + " " + fmt(d.index_px, 2); setOptionsBalance(d); fillExpSelect($("hp-exp-select"), d); fillExpSelect($("hp-oo-exp-select"), d); renderListStrikes(); renderTStrikes(); if (d.index_px && $("hp-target") && !$("hp-target").value) { $("hp-target").value = d.index_px; } } 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 dir = ($("hp-direction") && $("hp-direction").value) || "long"; const want = optTypeForDirection(dir); const exp = currentExp("hp-exp-select"); const prevInst = state.selected && state.selected.inst_id; tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '请选择到期日'; return; } const list = (exp.contracts || []).filter(function (c) { return String(c.opt_type || "").toUpperCase() === want && matchesMoneyFilter(c); }); if (!list.length) { tbody.innerHTML = '无匹配合约'; 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) + '' + 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); }); }); } 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 renderTStrikes() { const tbody = $("hp-oo-tbody"); if (!tbody) return; const exp = currentExp("hp-oo-exp-select"); tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '请选择到期日'; return; } const rows = buildStraddleRows(exp.contracts); rows.forEach(function (row) { const tr = document.createElement("tr"); const call = row.call; const put = row.put; const callAsk = call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated, call.tick_sz) : "—"; const putAsk = put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated, put.tick_sz) : "—"; tr.innerHTML = '' + callAsk + '' + (call ? moneynessBadge(call) : "—") + "" + (call ? '' : "—") + '' + row.strike + '' + (put ? moneynessBadge(put) : "—") + '' + putAsk + "" + (put ? '' : "—") + ""; tbody.appendChild(tr); }); 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) 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; } renderOoLegs(); }); }); } function renderOoLegs() { const el = $("hp-oo-legs"); if (!el) return; function one(tag, c) { if (!c) return tag + ": —"; return ( tag + ": " + c.opt_type + " K" + c.strike + " " + (c.moneyness_label || "") + " ask=" + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + " (" + c.inst_id + ")" ); } el.innerHTML = one("腿A", state.legA) + "
" + one("腿B", state.legB); } 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")) $("hp-target").value = ""; if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; if ($("hp-premium-line")) $("hp-premium-line").textContent = ""; if ($("hp-oo-legs")) $("hp-oo-legs").textContent = "尚未选用两腿"; void refreshAll(); } async function runPreview() { const isOo = state.mode === "options_options"; const tbody = $(isOo ? "hp-result-tbody-oo" : "hp-result-tbody"); const summary = $(isOo ? "hp-summary-oo" : "hp-summary"); try { let body; if (isOo) { if (!state.legA || !state.legB) throw new Error("请选用两条期权腿"); const target = Number(($("hp-target") && $("hp-target").value) || 0); if (!target) throw new Error("请填写目标价"); body = { plan_type: "options_options", target_price: target, index_px: (state.chain && state.chain.index_px) || target, leg_a: legPayload(state.legA, 1), leg_b: legPayload(state.legB, 1), }; } else { if (!state.selected) 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: ($("hp-direction") && $("hp-direction").value) || "long", 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: state.chain && state.chain.index_px, }; } 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") { summary.innerHTML = "止盈合计 " + fmt(s.tp_total) + " · 止损合计 " + fmt(s.sl_total) + " · 保费 " + fmt(s.premium_paid) + (s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : ""); } else { summary.innerHTML = "目标价合计 " + fmt(s.at_target_total) + " · 到期现价 " + fmt(s.expiry_flat_total) + " · 保费 " + fmt(s.premium_paid) + (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 = "永续 " + fmt(sc.perp_pnl); } else { mid = "A " + fmt(sc.leg_a_pnl) + " / B " + fmt(sc.leg_b_pnl); } const optCol = sc.options_pnl != null ? fmt(sc.options_pnl) : "—"; tr.innerHTML = "" + (sc.label || sc.id) + "" + fmt(sc.spot) + "" + mid + "" + optCol + "" + fmt(sc.total) + "" + (sc.note || "") + ""; tbody.appendChild(tr); }); } catch (e) { if (tbody) tbody.innerHTML = '' + (e.message || e) + ""; if (summary) summary.textContent = ""; } } function bind() { document.querySelectorAll(".hp-tab").forEach(function (b) { b.addEventListener("click", function () { state.tab = b.getAttribute("data-tab") || "perp_options"; syncTabUI(); if (state.tab === "perp_options" || state.tab === "options_options") { void loadGates(); } else { const el = $("hp-gate-line"); if (el) { el.textContent = state.tab === "history" ? "历史记录:独立对冲表,与普通交易记录分离" : "统计:止盈=盈利−保费;止损=期权盈利−永续亏损"; } } }); }); 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 () { state.moneyFilter = b.getAttribute("data-money") || "all"; syncMoneyUI(); renderListStrikes(); }); }); const dir = $("hp-direction"); if (dir) { dir.addEventListener("change", function () { state.selected = null; if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; void loadMarket().then(function () { renderListStrikes(); }); }); } 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-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); if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () { state.mode = "perp_options"; void runPreview(); }); if ($("hp-preview-btn-oo")) $("hp-preview-btn-oo").addEventListener("click", function () { state.mode = "options_options"; void runPreview(); }); } 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) + ""; } } syncTabUI(); syncUnderlyingUI(); syncMoneyUI(); bind(); void refreshAll(); })();