(function () { "use strict"; const root = document.getElementById("options-root"); if (!root) return; const state = { underlying: root.dataset.defaultUnderly || "ETH", optType: "C", chain: null, selectedInst: null, convertQuoteId: null, }; function fmt(v, d) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(d == null ? 2 : d); } async function apiJson(url, opts) { const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {})); return r.json(); } function orderPanel() { return document.getElementById("opt-order-panel"); } function orderPanelHost() { return document.getElementById("opt-order-panel-host"); } function parkOrderPanel() { const panel = orderPanel(); const host = orderPanelHost(); const inline = document.querySelector(".opt-order-inline-row"); if (inline) inline.remove(); if (panel && host && panel.parentElement !== host) { host.appendChild(panel); host.hidden = true; } if (panel) panel.style.display = "none"; document.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.remove("opt-row-selected"); }); document.querySelectorAll(".opt-pick-btn").forEach(function (b) { b.classList.remove("active"); b.disabled = false; if (b.dataset.origText) b.textContent = b.dataset.origText; }); } function placeOrderPanelAfter(instId) { const panel = orderPanel(); if (!panel || !instId) return; const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]'); if (!row) return; document.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.toggle("opt-row-selected", r === row); }); document.querySelectorAll(".opt-pick-btn").forEach(function (b) { const on = b.getAttribute("data-inst") === instId; b.classList.toggle("active", on); if (!b.dataset.origText) b.dataset.origText = b.textContent; if (on) b.textContent = "已选"; }); const oldInline = document.querySelector(".opt-order-inline-row"); if (oldInline) oldInline.remove(); const tr = document.createElement("tr"); tr.className = "opt-order-inline-row"; const td = document.createElement("td"); td.colSpan = 6; td.appendChild(panel); tr.appendChild(td); row.after(tr); panel.style.display = ""; orderPanelHost().hidden = true; tr.scrollIntoView({ behavior: "smooth", block: "nearest" }); } function currentSizeMode() { const el = document.querySelector('input[name="opt-size-mode"]:checked'); return el ? el.value : "sheets"; } function updateSizeInputs() { const mode = currentSizeMode(); const sheetsEl = document.getElementById("opt-sheets-amount"); const ethEl = document.getElementById("opt-eth-amount"); if (sheetsEl) sheetsEl.style.display = mode === "sheets" ? "" : "none"; if (ethEl) ethEl.style.display = mode === "eth_amount" ? "" : "none"; } function quoteUrl(instId) { const mode = currentSizeMode(); let url = "/api/options/quote?inst_id=" + encodeURIComponent(instId) + "&mode=" + mode; if (mode === "eth_amount") { const eth = document.getElementById("opt-eth-amount").value; if (eth) url += "ð_amount=" + encodeURIComponent(eth); } else if (mode === "sheets") { const sheets = document.getElementById("opt-sheets-amount").value; if (sheets) url += "&sheets=" + encodeURIComponent(sheets); } return url; } function moneynessBadge(c) { const m = (c && c.moneyness) || ""; const label = (c && c.moneyness_label) || "—"; return '' + label + ""; } async function refreshBalances() { const d = await apiJson("/api/options/balances"); if (!d.ok) return; document.getElementById("opt-funding-usdt").textContent = fmt(d.funding_usdt) + " U"; document.getElementById("opt-funding-usdc").textContent = fmt(d.funding_usdc) + " U"; document.getElementById("opt-trading-usdt").textContent = fmt(d.trading_usdt) + " U"; document.getElementById("opt-trading-usdc").textContent = fmt(d.trading_usdc) + " U"; document.getElementById("opt-trading-usdg").textContent = fmt(d.trading_usdg) + " U"; document.getElementById("opt-trade-budget").textContent = fmt(d.trade_budget) + " USDC"; } function expLabel(ms) { try { const dt = new Date(Number(ms)); const now = Date.now(); const dte = Math.max(0, Math.ceil((Number(ms) - now) / 86400000)); const base = dt.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); return base + " · " + dte + "D"; } catch (e) { return String(ms); } } function renderExpiries() { const sel = document.getElementById("opt-exp-select"); sel.innerHTML = ''; const exps = (state.chain && state.chain.expiries) || []; exps.forEach(function (e) { const o = document.createElement("option"); o.value = String(e.exp_time); o.textContent = expLabel(e.exp_time) + " (" + e.contracts.length + ")"; sel.appendChild(o); }); const idx = state.chain && state.chain.index_px; const dte = state.chain && state.chain.chain_max_dte_days; if (dte != null) { const el = document.getElementById("opt-chain-dte"); if (el) el.textContent = String(Math.round(dte)); } document.getElementById("opt-index-line").textContent = "指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 实值=价内 · 虚值=价外"; } function renderStrikes() { const tbody = document.getElementById("opt-strike-tbody"); const expMs = document.getElementById("opt-exp-select").value; const prevSelected = state.selectedInst; parkOrderPanel(); tbody.innerHTML = ""; if (!expMs || !state.chain) { tbody.innerHTML = '请选择到期日'; state.selectedInst = null; return; } const exp = (state.chain.expiries || []).find(function (e) { return String(e.exp_time) === String(expMs); }); if (!exp) return; const list = (exp.contracts || []).filter(function (c) { return c.opt_type === state.optType; }); if (!list.length) { tbody.innerHTML = '该到期日暂无报价'; state.selectedInst = null; return; } let stillVisible = false; list.forEach(function (c) { const tr = document.createElement("tr"); tr.className = "opt-strike-row"; tr.setAttribute("data-inst", c.inst_id); if (c.moneyness) tr.classList.add("opt-row-" + c.moneyness); tr.innerHTML = "" + c.strike + "" + "" + moneynessBadge(c) + "" + "" + c.inst_id + "" + "" + fmt(c.ask, 4) + "" + "" + fmt(c.bid, 4) + "" + '' + ' ' + '' + ""; tbody.appendChild(tr); if (c.inst_id === prevSelected) stillVisible = true; }); tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) { btn.addEventListener("click", function () { selectContract(btn.getAttribute("data-inst"), btn); }); }); tbody.querySelectorAll(".opt-buy-btn").forEach(function (btn) { btn.addEventListener("click", async function () { await selectContract(btn.getAttribute("data-inst"), null); await openPosition(); }); }); if (stillVisible && prevSelected) { selectContract(prevSelected, null, true); } else { state.selectedInst = null; } } function fillOrderPanel(d) { const sz = d.sizing || {}; document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || ""; document.getElementById("opt-order-ask").textContent = fmt(d.ask, 4); document.getElementById("opt-order-sheets").textContent = sz.sheets != null ? sz.sheets : "—"; document.getElementById("opt-order-eth").textContent = sz.eth_amount != null ? sz.eth_amount : "—"; document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmt(sz.total_premium, 4) + " USDC" : "—"; const msgEl = document.getElementById("opt-order-msg"); if (!d.ok) { msgEl.textContent = d.msg || "报价失败"; msgEl.classList.add("opt-error"); } else if (sz.ok === false) { msgEl.textContent = sz.msg || ""; msgEl.classList.add("opt-error"); } else { msgEl.textContent = ""; msgEl.classList.remove("opt-error"); } } async function selectContract(instId, pickBtn, silent) { state.selectedInst = instId; placeOrderPanelAfter(instId); if (pickBtn) { pickBtn.disabled = true; if (!pickBtn.dataset.origText) pickBtn.dataset.origText = "选择"; pickBtn.textContent = "加载…"; } try { const d = await apiJson(quoteUrl(instId)); fillOrderPanel(d); return d; } finally { if (pickBtn) { pickBtn.disabled = false; pickBtn.textContent = "已选"; pickBtn.classList.add("active"); } if (!silent) placeOrderPanelAfter(instId); } } async function loadChain() { const d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(state.underlying)); if (!d.ok) { alert(d.msg || "加载失败"); return; } state.chain = d; state.selectedInst = null; parkOrderPanel(); renderExpiries(); renderStrikes(); } async function openPosition() { if (!state.selectedInst) { alert("请先选择合约"); return; } const btn = document.getElementById("opt-open-btn"); btn.disabled = true; try { const mode = currentSizeMode(); const body = { inst_id: state.selectedInst, mode: mode, signal_note: document.getElementById("opt-signal-note").value || "", }; if (mode === "eth_amount") { body.eth_amount = parseFloat(document.getElementById("opt-eth-amount").value); } else if (mode === "sheets") { body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10); } const d = await apiJson("/api/options/open", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const msgEl = document.getElementById("opt-order-msg"); msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败"); msgEl.classList.toggle("opt-error", !d.ok); if (d.ok) { refreshBalances(); refreshPositions(); } else { alert(d.msg || "下单失败"); } } finally { btn.disabled = false; } } async function closePosition(inst, btn) { const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=sheets&sheets=1"); if (!q.ok) { alert(q.msg || "获取买一价失败"); return; } const bid = q.bid; if (bid == null || bid <= 0) { alert("暂无买一价,请稍后在 OKX App 平仓或等盘口恢复"); return; } if (!confirm("限价卖出 @ 买一 " + fmt(bid, 4) + "(每 1 ETH/BTC)?\n合约:" + inst)) return; if (btn) btn.disabled = true; try { const r = await apiJson("/api/options/close", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inst_id: inst }), }); if (r.ok) { alert("平仓单已提交" + (r.bid != null ? " @ " + fmt(r.bid, 4) : "")); } else { alert(r.msg || "平仓失败"); } refreshPositions(); refreshBalances(); } finally { if (btn) btn.disabled = false; } } async function refreshPositions() { const d = await apiJson("/api/options/positions"); const tbody = document.getElementById("opt-positions-tbody"); tbody.innerHTML = ""; const list = (d.ok && d.positions) || []; if (!list.length) { tbody.innerHTML = '暂无持仓'; return; } list.forEach(function (p) { const tr = document.createElement("tr"); tr.innerHTML = "" + (p.inst_id || "") + "" + "" + fmt(p.pos, 0) + "" + "" + fmt(p.eth_amount, 4) + "" + "" + fmt(p.avg_px, 4) + "" + "" + fmt(p.mark_px, 4) + "" + "" + fmt(p.upl, 4) + "" + "" + (p.upl_ratio_pct != null ? p.upl_ratio_pct + "%" : "—") + "" + ''; tbody.appendChild(tr); }); tbody.querySelectorAll(".opt-close-btn").forEach(function (btn) { btn.addEventListener("click", function () { closePosition(btn.getAttribute("data-inst"), btn); }); }); } document.querySelectorAll(".opt-uly-btn").forEach(function (btn) { btn.addEventListener("click", function () { document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); state.underlying = btn.getAttribute("data-uly"); loadChain(); }); }); document.querySelectorAll(".opt-type-btn").forEach(function (btn) { btn.addEventListener("click", function () { document.querySelectorAll(".opt-type-btn").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); state.optType = btn.getAttribute("data-type"); renderStrikes(); }); }); document.getElementById("opt-exp-select").addEventListener("change", renderStrikes); document.getElementById("opt-load-chain").addEventListener("click", loadChain); document.getElementById("opt-refresh-balances").addEventListener("click", refreshBalances); document.getElementById("opt-refresh-positions").addEventListener("click", refreshPositions); document.getElementById("opt-open-btn").addEventListener("click", openPosition); document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) { r.addEventListener("change", function () { updateSizeInputs(); if (state.selectedInst) selectContract(state.selectedInst, null, true); }); }); ["opt-sheets-amount", "opt-eth-amount"].forEach(function (id) { const el = document.getElementById(id); if (!el) return; el.addEventListener("change", function () { if (state.selectedInst) selectContract(state.selectedInst, null, true); }); }); document.getElementById("opt-convert-quote-btn").addEventListener("click", async function () { const amount = parseFloat(document.getElementById("opt-convert-amount").value); if (!amount || amount <= 0) { alert("请输入 USDT 数量"); return; } const d = await apiJson("/api/options/convert/quote", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: amount }), }); const prev = document.getElementById("opt-convert-preview"); if (!d.ok) { prev.textContent = d.msg || "询价失败"; state.convertQuoteId = null; document.getElementById("opt-convert-exec-btn").disabled = true; return; } state.convertQuoteId = d.quote_id; prev.textContent = "预估获得 " + fmt(d.base_sz, 6) + " USDC,汇率 " + fmt(d.cnvt_px, 6); document.getElementById("opt-convert-exec-btn").disabled = false; }); document.getElementById("opt-convert-exec-btn").addEventListener("click", async function () { if (!state.convertQuoteId) return; const amount = parseFloat(document.getElementById("opt-convert-amount").value); const d = await apiJson("/api/options/convert/execute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ quote_id: state.convertQuoteId, rfq_sz: amount }), }); document.getElementById("opt-convert-preview").textContent = d.ok ? "兑换成功" : (d.msg || "失败"); state.convertQuoteId = null; document.getElementById("opt-convert-exec-btn").disabled = true; refreshBalances(); }); document.getElementById("opt-transfer-btn").addEventListener("click", async function () { const d = await apiJson("/api/options/transfer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ccy: document.getElementById("opt-transfer-ccy").value, from: document.getElementById("opt-transfer-from").value, to: document.getElementById("opt-transfer-to").value, amount: parseFloat(document.getElementById("opt-transfer-amount").value), }), }); document.getElementById("opt-transfer-msg").textContent = d.ok ? "划转成功" : (d.msg || "失败"); refreshBalances(); }); updateSizeInputs(); refreshBalances(); loadChain(); refreshPositions(); })();