/** * 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); // 仅裁小数尾零;整数 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 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", "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; } } 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"); const startOo = $("hp-start-btn-oo"); if ((gates.plan_type || state.mode) === "options_options") { if (startOo) startOo.disabled = !gates.can_start; } else { 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 amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4; const q = $("hp-perp-quote"); if (q) { q.innerHTML = "" + (d.base || state.underlying) + " · " + acctLabel + "可用 " + fmt(d.available_usdt, 2) + " USDT
标记 " + fmt(d.mark, 2) + " · 最新 " + fmt(d.last, 2) + " · 卖一 " + 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.textContent = "全仓建议(" + acctLabel + "):保证金 " + fmt(s.margin_capital, 2) + " USDT × " + s.leverage + "x → 名义 " + fmt(s.notional_value, 2) + " USDT · 建议 " + fmt(d.suggest_contracts, amtPrec) + " 合约张(已按交易所精度)"; } 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 = (($("hp-direction") && $("hp-direction").value) || "long").toLowerCase(); if (!(entry > 0) || !(contracts > 0) || !(cs > 0)) { el.textContent = "填写开仓价与张数后,输入止盈/止损可看永续盈亏金额(USDT)"; return; } function pnlAt(exitPx) { const coins = contracts * cs; if (dir === "short") return (entry - exitPx) * coins; return (exitPx - entry) * coins; } const parts = []; if (tp > 0) { const p = pnlAt(tp); parts.push( "止盈预期 = 0 ? "hp-pnl-pos" : "hp-pnl-neg") + "\">" + (p >= 0 ? "+" : "") + fmt(p, 2) + " USDT" ); } else { parts.push("止盈预期 —"); } if (sl > 0) { const p = pnlAt(sl); parts.push( "止损预期 = 0 ? "hp-pnl-pos" : "hp-pnl-neg") + "\">" + (p >= 0 ? "+" : "") + fmt(p, 2) + " USDT" ); } else { parts.push("止损预期 —"); } el.innerHTML = parts.join(" · "); } 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) { 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 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() { 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"); updateOoPremiumLine(); } 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 = $(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 up = Number(($("hp-target-up") && $("hp-target-up").value) || 0); const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0); if (!up || !down) throw new Error("请填写上破与下破目标价"); if (up <= down) throw new Error("上破目标价必须大于下破目标价"); body = { plan_type: "options_options", target_price_up: up, target_price_down: down, target_price: up, index_px: (state.chain && state.chain.index_px) || (up + down) / 2, 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("请选用期权腿"); 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 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 () { 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(); }); }); } ["hp-entry", "hp-tp", "hp-sl", "hp-contracts"].forEach(function (id) { const el = $(id); if (el) el.addEventListener("input", updatePerpPnlHint); }); 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); ["hp-oo-sheets-a", "hp-oo-sheets-b"].forEach(function (id) { const el = $(id); if (el) el.addEventListener("input", updateOoPremiumLine); }); 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(); }); if ($("hp-start-btn")) $("hp-start-btn").addEventListener("click", function () { void startPlan("perp_options"); }); if ($("hp-start-btn-oo")) $("hp-start-btn-oo").addEventListener("click", function () { void startPlan("options_options"); }); 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") { return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl); } return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price); } 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 + "" + '进行中' + "" + activeTargetLabel(p) + "" + (p.opened_at || "—") + ''; tbody.appendChild(tr); }); tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) { btn.addEventListener("click", function () { void showPlanDetail(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: "期期下破·平盈利腿", expiry: "到期", manual: "人工结束", partial_fail: "半腿失败", cancelled: "已取消", }; 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 { 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) : "—") + "" + (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 startPlan(planType) { const isOo = planType === "options_options"; try { let body; if (isOo) { if (!state.legA || !state.legB) throw new Error("请选用两条期权腿"); const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0); const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0); if (!up || !down) throw new Error("请填写上破与下破目标价"); if (up <= down) throw new Error("上破目标价必须大于下破目标价"); body = { plan_type: "options_options", underlying: state.underlying, target_price_up: up, target_price_down: down, target_price: up, 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("请选用期权腿"); 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: ($("hp-direction") && $("hp-direction").value) || "long", 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, 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 (!window.confirm("确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)")) return; const d = await apiJson("/api/hedge-plan/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); setGateLine(d.gates); alert("计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : "")); void loadGates(); } catch (e) { alert(e.message || String(e)); } } 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(); })();