/** * OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁. */ (function () { const root = document.getElementById("hedge-plan-root"); if (!root) return; const state = { mode: "perp_options", underlying: root.getAttribute("data-default-underly") || "ETH", 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 optTypeForDirection(dir) { return dir === "short" ? "C" : "P"; } function syncModeUI() { document.querySelectorAll(".hp-mode-btn").forEach(function (b) { b.classList.toggle("active", b.getAttribute("data-mode") === state.mode); }); const po = $("hp-po-layout"); const oo = $("hp-oo-layout"); if (po) po.classList.toggle("hidden", state.mode !== "perp_options"); if (oo) oo.classList.toggle("hidden", state.mode !== "options_options"); } 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; } 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 q = $("hp-perp-quote"); if (q) { q.innerHTML = "标记 " + fmt(d.mark, 2) + " · 最新 " + fmt(d.last, 2) + " · 卖一 " + fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + "
面值 " + fmt(d.contract_size, 4) + " · 可用 " + fmt(d.available_usdt, 2) + " U"; } const sz = $("hp-sizing-line"); if (sz) { if (d.full_margin_sizing) { const s = d.full_margin_sizing; sz.textContent = "全仓建议:保证金 " + 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 idx = $("hp-index-line"); if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2) + " · " + (d.inst_family || ""); const ooIdx = $("hp-oo-index"); if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2); 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 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"); tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '请选择到期日'; return; } const list = (exp.contracts || []).filter(function (c) { return String(c.opt_type || "").toUpperCase() === want; }); if (!list.length) { tbody.innerHTML = '无匹配合约'; return; } list.forEach(function (c) { const tr = document.createElement("tr"); tr.innerHTML = "" + c.strike + "" + c.opt_type + "" + fmt(c.ask, 4) + "" + fmt(c.bid, 4) + ''; 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; }); if (!c) return; state.selected = c; const el = $("hp-sel-inst"); if (el) el.textContent = c.inst_id; updatePremiumLine(); }); }); } 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 callAsk = row.call ? fmt(row.call.ask, 4) : "—"; const putAsk = row.put ? fmt(row.put.ask, 4) : "—"; tr.innerHTML = "" + callAsk + '' + (row.call ? '' : "—") + '' + row.strike + "" + putAsk + '' + (row.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 + " ask=" + fmt(c.ask, 4) + " (" + 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, }; } async function runPreview() { const tbody = $("hp-result-tbody"); const summary = $("hp-summary"); try { let body; if (state.mode === "options_options") { 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 ? " · 到期无盈利(记总亏损)" : ""); } } 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-mode-btn").forEach(function (b) { b.addEventListener("click", function () { state.mode = b.getAttribute("data-mode") || "perp_options"; syncModeUI(); loadGates(); }); }); document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) { b.addEventListener("click", function () { state.underlying = b.getAttribute("data-uly") || "ETH"; document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (x) { x.classList.toggle("active", x.getAttribute("data-uly") === state.underlying); }); state.selected = null; state.legA = null; state.legB = null; void refreshAll(); }); }); const dir = $("hp-direction"); if (dir) { dir.addEventListener("change", function () { 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 () { void runPreview(); }); } async function refreshAll() { 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) + ""; } } syncModeUI(); bind(); void refreshAll(); })();