(function () { "use strict"; const root = document.getElementById("options-root"); if (!root) return; if (root.getAttribute("data-options-booted") === "1") return; root.setAttribute("data-options-booted", "1"); const panelCache = (window.__optionsPanelCache = window.__optionsPanelCache || {}); const state = { underlying: root.dataset.defaultUnderly || "ETH", optType: "C", moneyFilter: "all", chainView: "list", strikeExpandAll: false, /** 环境 OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED;链接口可热更新 */ askLiqFilter: root.dataset.askLiqFilter !== "0", budgetBuffer: (function () { const raw = root.dataset.budgetBuffer; const n = raw != null && raw !== "" ? Number(raw) : NaN; return !Number.isNaN(n) && n > 0 ? n : 0.95; })(), chain: panelCache.chain || null, selectedInst: null, orderQuote: null, expandedPosInst: null, posTab: "live", /** 未点设定前的目标输入草稿,避免持仓轮询重绘清空 */ targetDraftByInst: {}, /** 翻倍倍数草稿,避免轮询重绘把正在输入的值刷回 1 */ profitExitDraftByInst: {}, }; let lastGoodPositions = null; let lastGoodPositionsAt = 0; let positionsRefreshSeq = 0; let chainLoadSeq = 0; let selectSeq = 0; let refreshAllTimer = null; let pendingRefreshTimer = null; let pendingTtlSeconds = 600; const POSITIONS_STALE_MS = 45000; const PENDING_POLL_MS = 8000; const orderPanelHome = (function () { const host = document.getElementById("opt-order-panel-host"); return host ? host.parentElement : null; })(); function fmt(v, d) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(d == null ? 2 : d); } function fmtDisplay(v, fallback) { if (v !== null && v !== undefined && String(v).trim() !== "") return String(v); if (fallback !== undefined) return fmtDisplay(fallback); return "—"; } 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) { // 无 tick 时裁掉浮点毛刺,勿 482.4881990066513 let s = n.toFixed(4).replace(/\.?0+$/, ""); return s || "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"; } 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 syncPickButtons(instId) { document.querySelectorAll(".opt-pick-btn").forEach(function (btn) { const on = !!instId && btn.getAttribute("data-inst") === instId; btn.classList.toggle("active", on); btn.disabled = false; if (!btn.dataset.origText) btn.dataset.origText = "选择"; btn.textContent = on ? "已选" : btn.dataset.origText; }); } function parkOrderPanel() { const panel = orderPanel(); const host = orderPanelHost(); // 弹窗挂到 body;关闭后收回原位,绝不插入期权链表格 if (panel && host && panel.parentElement !== host) host.appendChild(panel); if (host) { host.hidden = true; host.setAttribute("aria-hidden", "true"); if (orderPanelHome && host.parentElement !== orderPanelHome) { orderPanelHome.appendChild(host); } } if (panel) panel.style.display = "none"; const inline = document.querySelector(".opt-order-inline-row"); if (inline) inline.remove(); document.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.remove("opt-row-selected"); }); syncPickButtons(null); } function placeOrderPanelAfter(instId) { const panel = orderPanel(); const host = orderPanelHost(); if (!panel || !host || !instId) { syncPickButtons(instId || null); return false; } const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]') || document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-call-inst="' + CSS.escape(instId) + '"]') || document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-put-inst="' + CSS.escape(instId) + '"]'); document.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.toggle("opt-row-selected", !!row && r === row); }); syncPickButtons(instId); const oldInline = document.querySelector(".opt-order-inline-row"); if (oldInline) oldInline.remove(); if (panel.parentElement !== host) host.appendChild(panel); // 挂到 body,避免被卡片 overflow 裁成「行内展开」 if (host.parentElement !== document.body) document.body.appendChild(host); host.hidden = false; host.setAttribute("aria-hidden", "false"); panel.style.display = ""; return true; } function closeOrderDialog() { state.selectedInst = null; state.orderQuote = null; parkOrderPanel(); } function fmtPendingAge(sec) { if (sec == null || Number.isNaN(Number(sec))) return "—"; let s = Math.max(0, Math.round(Number(sec))); if (s < 60) return s + "秒"; const m = Math.floor(s / 60); const rs = s % 60; if (m < 60) return rs ? m + "分" + rs + "秒" : m + "分"; const h = Math.floor(m / 60); const rm = m % 60; return rm ? h + "时" + rm + "分" : h + "时"; } function paintPendingOrders(orders, ttlSec) { const host = document.getElementById("opt-pending-list"); const hint = document.getElementById("opt-pending-ttl-hint"); if (ttlSec != null && !Number.isNaN(Number(ttlSec))) { pendingTtlSeconds = Number(ttlSec); } if (hint) { const ttl = pendingTtlSeconds; hint.textContent = ttl > 0 ? ("平仓限价超 " + fmtPendingAge(ttl) + " 未成交将自动撤销") : "平仓超时自动撤单已关闭"; } if (!host) return; const rows = Array.isArray(orders) ? orders : []; if (!rows.length) { host.innerHTML = '
暂无未成交委托
'; return; } host.innerHTML = rows.map(function (o) { const side = String(o.side || "").toLowerCase(); const sideCls = side === "buy" ? "is-buy" : side === "sell" ? "is-sell" : ""; const remain = (o.sz != null && o.fill_sz != null) ? Math.max(0, Number(o.sz) - Number(o.fill_sz)) : o.sz; const pxTxt = o.px != null ? fmtOptionPx(o.px, null) : "—"; const kind = o.is_close_order ? "平仓" : "开仓"; let ttlTxt = ""; if (o.auto_cancel_enabled) { if (o.stale) ttlTxt = " · 超时待撤"; else if (o.expire_in_sec != null) ttlTxt = " · 剩 " + fmtPendingAge(o.expire_in_sec) + " 自动撤"; } const ageTxt = o.age_sec != null ? ("已挂 " + fmtPendingAge(o.age_sec)) : ""; return ( '
' + '
' + '' + kind + " · " + (o.side_label || side || "—") + "" + '' + "
" + '
' + (o.inst_id || "—") + "
" + '
价 ' + pxTxt + " · 张数 " + (o.sz != null ? o.sz : "—") + (o.fill_sz != null && Number(o.fill_sz) > 0 ? " · 已成 " + o.fill_sz : "") + (remain != null && o.fill_sz != null && Number(o.fill_sz) > 0 ? " · 剩余 " + remain : "") + (ageTxt ? " · " + ageTxt : "") + ttlTxt + "
" ); }).join(""); host.querySelectorAll(".opt-pending-cancel").forEach(function (btn) { btn.addEventListener("click", function () { cancelPendingOrder(btn.getAttribute("data-inst"), btn.getAttribute("data-ord"), btn); }); }); } async function refreshPendingOrders() { const host = document.getElementById("opt-pending-list"); if (!host) return; try { const d = await apiJson("/api/options/orders/pending"); if (!d.ok) { host.innerHTML = '
' + (d.msg || "获取委托失败") + "
"; return; } paintPendingOrders(d.orders || [], d.pending_ttl_seconds); } catch (e) { host.innerHTML = '
获取委托失败
'; } } function startPendingOrdersPoll() { stopPendingOrdersPoll(); pendingRefreshTimer = setInterval(function () { if (!document.getElementById("options-root")) { stopPendingOrdersPoll(); return; } refreshPendingOrders(); }, PENDING_POLL_MS); } function stopPendingOrdersPoll() { if (pendingRefreshTimer) { clearInterval(pendingRefreshTimer); pendingRefreshTimer = null; } } async function cancelPendingOrder(inst, ordId, btn) { if (!inst || !ordId) return; if (!confirm("撤销该委托?\n合约: " + inst + "\n订单: " + ordId)) return; if (btn) btn.disabled = true; try { const d = await apiJson("/api/options/orders/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inst_id: inst, ord_id: ordId }), }); if (!d.ok) { alert(d.msg || "撤销失败"); return; } await refreshPendingOrders(); refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); void refreshBridgeSellUi(true); } finally { if (btn) btn.disabled = false; } } function compoundFullEnabled() { // 缺省按关闭,避免热更关闭后仍误用全仓复利 return !!(root && String(root.dataset.compoundFullEnabled || "0") === "1"); } function currentSizeMode() { const el = document.querySelector('input[name="opt-size-mode"]:checked:not(:disabled)'); if (el) return el.value; const any = document.querySelector('input[name="opt-size-mode"]:checked'); if (any && any.value === "compound_full" && !compoundFullEnabled()) return "sheets"; if (any && any.value === "budget_full" && compoundFullEnabled()) return "compound_full"; return "sheets"; } function applyCompoundModeUi(compoundOn) { if (root) root.dataset.compoundFullEnabled = compoundOn ? "1" : "0"; updateSizeInputs(); } function syncCompoundFlagsFromPayload(d) { if (!d || typeof d !== "object") return; if (d.compound_full_enabled != null) { applyCompoundModeUi(!!d.compound_full_enabled); } else if (d.cfg && d.cfg.compound_full_enabled != null) { applyCompoundModeUi(!!d.cfg.compound_full_enabled); } } let lastBalancesPayload = null; function tradingCoinAvailFromPayload(d, uly) { if (!d) return null; const u = String(uly || state.underlying || "ETH").toUpperCase(); const key = u === "BTC" ? "trading_btc_avail" : "trading_eth_avail"; const v = d[key]; if (v == null) return d[u === "BTC" ? "trading_btc" : "trading_eth"]; return v; } function coinMarginModeFromPayload(d) { if (d && d.options_margin_mode === "coin") return true; if (d && d.options_margin_mode === "usdc") return false; return isCoinMarginMode(); } function updateRetrySellCoinUi(d) { const btn = document.getElementById("opt-retry-sell-coin"); const hint = document.getElementById("opt-bridge-sell-hint"); if (!btn) return; const payload = d || lastBalancesPayload; const coinOn = coinMarginModeFromPayload(payload); const uly = String(state.underlying || "ETH").toUpperCase(); btn.hidden = !coinOn; if (!coinOn) { if (hint) hint.hidden = true; btn.classList.remove("opt-retry-sell-coin-btn--pending"); return; } btn.textContent = "重试卖回 " + uly; const avail = tradingCoinAvailFromPayload(payload, uly); const pending = payload && payload.bridge_status === "pending_sell_spot"; btn.classList.toggle("opt-retry-sell-coin-btn--pending", !!pending); if (hint) { const parts = []; if (pending) parts.push("平仓后卖币未成功,可点此重试"); if (avail != null && Number(avail) > 0) { parts.push("交易户可用 " + fmt(Number(avail), 6) + " " + uly); } if (parts.length) { hint.textContent = parts.join(" · "); hint.hidden = false; } else { hint.hidden = true; } } } async function refreshBridgeSellUi(force) { try { const qs = force ? "?force=1" : ""; const d = await apiJson("/api/options/balances" + qs); lastBalancesPayload = d; syncCompoundFlagsFromPayload(d); if (d && d.options_margin_mode === "coin") { if (!state.chain) state.chain = {}; state.chain.options_margin_mode = "coin"; state.chain.margin_mode = "coin"; } if (d && d.trade_budget != null && root) { root.dataset.tradeBudget = String(d.trade_budget); } updateRetrySellCoinUi(d); return d; } catch (_) { updateRetrySellCoinUi(null); return null; } } function setXferMsg(text, isErr) { const el = document.getElementById("opt-pos-xfer-msg"); if (!el) return; el.textContent = text || ""; el.classList.toggle("opt-error", !!isErr); el.classList.toggle("opt-success", !!text && !isErr); } function xferRoundAvail(v) { const n = Number(v); if (!Number.isFinite(n) || n <= 0) return null; return Math.round(n * 100) / 100; } function xferPickBalance(bal, account, ccy) { if (!bal) return null; const acct = account === "trading" ? "trading" : "funding"; const c = String(ccy || "").toLowerCase(); const availKey = acct + "_" + c + "_avail"; const totalKey = acct + "_" + c; return xferRoundAvail(bal[availKey] != null ? bal[availKey] : bal[totalKey]); } function xferAccountLabel(acct) { return acct === "trading" ? "交易账户" : "资金账户"; } function setPosXferBusy(busy) { ["opt-pos-xfer-btn", "opt-pos-xfer-all-btn"].forEach(function (id) { const btn = document.getElementById(id); if (!btn) return; if (busy) { if (!btn.dataset.origText) btn.dataset.origText = btn.textContent; btn.disabled = true; } else { btn.disabled = false; if (btn.dataset.origText) { btn.textContent = btn.dataset.origText; delete btn.dataset.origText; } } }); const input = document.getElementById("opt-pos-xfer-amount"); if (input) input.disabled = !!busy; } async function submitPosTransfer(amount) { const ccyEl = document.getElementById("opt-pos-xfer-ccy"); const fromEl = document.getElementById("opt-pos-xfer-from"); const toEl = document.getElementById("opt-pos-xfer-to"); if (!ccyEl || !fromEl || !toEl) return { ok: false }; setPosXferBusy(true); setXferMsg("划转中…", false); try { const r = await apiJson("/api/options/transfer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ccy: ccyEl.value, from: fromEl.value, to: toEl.value, amount: amount, }), }); if (r.ok) { setXferMsg("划转成功", false); const amtEl = document.getElementById("opt-pos-xfer-amount"); if (amtEl) amtEl.value = ""; refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot({ force: true }); await refreshBridgeSellUi(true); } else { setXferMsg("划转失败:" + (r.msg || "未知错误"), true); } return r; } catch (e) { setXferMsg("划转失败:" + (e.message || "网络错误"), true); return { ok: false }; } finally { setPosXferBusy(false); } } function bindPosTransferControls() { const xferBtn = document.getElementById("opt-pos-xfer-btn"); const xferAllBtn = document.getElementById("opt-pos-xfer-all-btn"); const amtEl = document.getElementById("opt-pos-xfer-amount"); if (!xferBtn && !xferAllBtn) return; function hardenXferAmountInput() { if (!amtEl) return; function wipe() { const v = String(amtEl.value || "").trim(); if (/^[a-z][a-z0-9._-]{1,31}$/i.test(v)) amtEl.value = ""; } wipe(); amtEl.setAttribute("readonly", "readonly"); amtEl.addEventListener("focus", function () { amtEl.removeAttribute("readonly"); }); amtEl.addEventListener("blur", function () { if (!amtEl.value) amtEl.setAttribute("readonly", "readonly"); }); setTimeout(wipe, 200); setTimeout(wipe, 800); } hardenXferAmountInput(); if (xferBtn) { xferBtn.addEventListener("click", async function () { const amount = parseFloat((amtEl || {}).value); if (!amount || amount <= 0) { setXferMsg("请输入有效数量", true); return; } await submitPosTransfer(amount); }); } if (xferAllBtn) { xferAllBtn.addEventListener( "click", function () { if (amtEl) amtEl.removeAttribute("readonly"); }, true ); xferAllBtn.addEventListener("click", async function () { try { const ccyEl = document.getElementById("opt-pos-xfer-ccy"); const fromEl = document.getElementById("opt-pos-xfer-from"); const toEl = document.getElementById("opt-pos-xfer-to"); const bal = await refreshBridgeSellUi(true); const ccy = ccyEl ? ccyEl.value : "USDT"; const from = fromEl ? fromEl.value : "funding"; const to = toEl ? toEl.value : "trading"; const amount = xferPickBalance(bal, from, ccy); if (!amount) { setXferMsg("划出账户可用余额不足", true); return; } const msg = "确认全部划转?\n\n" + "币种:" + ccy + "\n" + "划出:" + xferAccountLabel(from) + "\n" + "划入:" + xferAccountLabel(to) + "\n" + "金额:" + amount.toFixed(2) + " " + ccy + "\n\n" + "将划转该账户全部可用余额。"; if (!confirm(msg)) return; if (amtEl) amtEl.value = String(amount); await submitPosTransfer(amount); } catch (e) { setXferMsg("划转失败:" + (e.message || "余额拉取失败"), true); } }); } } async function retrySellBackCoin() { const uly = String(state.underlying || "ETH").toUpperCase(); const d = await refreshBridgeSellUi(true); const avail = tradingCoinAvailFromPayload(d, uly); const availTxt = avail != null && Number(avail) > 0 ? ("\n交易账户可用: " + fmt(Number(avail), 6) + " " + uly) : ""; const msg = "确认市价卖出交易账户中的全部可用 " + uly + " 换回 USDT?" + availTxt + "\n\n将按可用余额全额卖出(非 bridge 记账量)。"; if (!confirm(msg)) return; const btn = document.getElementById("opt-retry-sell-coin"); if (btn) btn.disabled = true; try { const r = await apiJson("/api/options/spot-bridge/retry-sell", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ underlying: uly }), }); if (r.ok) { if (r.skipped) { alert(r.msg || "无残留币需卖回"); } else { const sold = r.sell && r.sell.coin_sold; alert("卖回已提交" + (sold != null ? (" " + fmt(Number(sold), 6) + " " + uly) : "")); } } else { alert(r.msg || "卖回失败"); } refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); await refreshBridgeSellUi(true); } finally { if (btn) btn.disabled = false; } } function updateSizeInputs() { const mode = currentSizeMode(); const sheetsEl = document.getElementById("opt-sheets-amount"); const ethEl = document.getElementById("opt-eth-amount"); const hint = document.getElementById("opt-budget-full-hint"); const compoundHint = document.getElementById("opt-compound-full-hint"); const budgetWrap = document.getElementById("opt-size-mode-budget-wrap"); const compoundWrap = document.getElementById("opt-size-mode-compound-wrap"); const capEl = document.getElementById("opt-budget-full-cap"); const compoundCapLine = document.getElementById("opt-compound-cap-line"); const compoundOn = compoundFullEnabled(); if (budgetWrap) { budgetWrap.hidden = !!compoundOn; budgetWrap.style.display = compoundOn ? "none" : ""; const radio = budgetWrap.querySelector('input[name="opt-size-mode"]'); if (radio) radio.disabled = !!compoundOn; } if (compoundWrap) { compoundWrap.hidden = !compoundOn; compoundWrap.style.display = compoundOn ? "" : "none"; const radio = compoundWrap.querySelector('input[name="opt-size-mode"]'); if (radio) radio.disabled = !compoundOn; } if (compoundOn && (mode === "budget_full" || mode === "compound_full")) { const compoundRadio = document.querySelector('input[name="opt-size-mode"][value="compound_full"]'); if (compoundRadio) { compoundRadio.disabled = false; compoundRadio.checked = true; } } else if (!compoundOn) { const compoundRadio = document.querySelector('input[name="opt-size-mode"][value="compound_full"]'); if (compoundRadio) { compoundRadio.checked = false; compoundRadio.disabled = true; } // currentSizeMode 会把残留 compound 映射成 sheets,须实际勾选,避免无选中无法开仓 const checkedOk = document.querySelector('input[name="opt-size-mode"]:checked:not(:disabled)'); if (!checkedOk) { const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]'); if (sheetsRadio) { sheetsRadio.disabled = false; sheetsRadio.checked = true; } } } const modeNow = currentSizeMode(); if (sheetsEl) sheetsEl.style.display = modeNow === "sheets" ? "" : "none"; if (ethEl) ethEl.style.display = modeNow === "eth_amount" ? "" : "none"; if (hint) hint.style.display = modeNow === "budget_full" && !compoundOn ? "" : "none"; if (compoundHint) compoundHint.style.display = modeNow === "compound_full" && compoundOn ? "" : "none"; if (capEl && root && root.dataset.tradeBudget) { const n = Number(root.dataset.tradeBudget); if (Number.isFinite(n) && n > 0) capEl.textContent = n.toFixed(2); } if (compoundCapLine && root) { const on = String(root.dataset.compoundCapEnabled || "") === "1"; const cap = Number(root.dataset.compoundCapUsdc); if (on && Number.isFinite(cap) && cap > 0) { compoundCapLine.textContent = "全仓上限已开启:" + cap.toFixed(2) + "U"; } else { compoundCapLine.textContent = "全仓上限关闭(env可开)"; } } document.querySelectorAll(".opt-size-mode-chip").forEach(function (chip) { const radio = chip.querySelector('input[name="opt-size-mode"]'); const selected = !!(radio && radio.checked && !radio.disabled); chip.classList.toggle("is-selected", selected); chip.classList.toggle("active", selected); }); } function hardenOrderAutofill() { function looksLikeUsername(v) { return /^[a-z][a-z0-9._-]{1,31}$/i.test(String(v || "").trim()); } function harden(el) { if (!el) return; function wipe() { if (looksLikeUsername(el.value)) el.value = ""; } wipe(); el.addEventListener("focus", function () { el.removeAttribute("readonly"); }); el.addEventListener("blur", function () { if (!el.value) el.setAttribute("readonly", "readonly"); }); setTimeout(wipe, 200); setTimeout(wipe, 800); setTimeout(wipe, 2000); } const note = document.getElementById("opt-signal-note"); harden(note); [ "opt-sheets-amount", "opt-eth-amount", "opt-target-idx", ].forEach(function (id) { harden(document.getElementById(id)); }); } function quoteUrl(instId) { updateSizeInputs(); 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 strikeTableColspan() { return 9; } function syncChainViewUI() { const isT = state.chainView === "t"; document.querySelectorAll(".opt-view-btn").forEach(function (b) { b.classList.toggle("active", (b.getAttribute("data-view") || "") === state.chainView); }); const typeGroup = document.getElementById("opt-type-btn-group"); if (typeGroup) typeGroup.hidden = isT; const expandWrap = document.getElementById("opt-strike-expand-wrap"); if (expandWrap) expandWrap.hidden = false; const headList = document.getElementById("opt-strike-head-list"); const headT = document.getElementById("opt-strike-head-t"); const headTCols = document.getElementById("opt-strike-head-t-cols"); if (headList) { headList.classList.toggle("hidden", isT); headList.hidden = isT; } if (headT) { headT.classList.toggle("hidden", !isT); headT.hidden = !isT; } if (headTCols) { headTCols.classList.toggle("hidden", !isT); headTCols.hidden = !isT; } const wrap = document.getElementById("opt-strike-table-wrap"); if (wrap) wrap.classList.toggle("options-strike-table-wrap--t", isT); const table = document.getElementById("opt-strike-table"); if (table) table.classList.toggle("options-strike-table--t", isT); } function matchesMoneyFilter(moneyness) { const m = (moneyness || "").toLowerCase(); if (state.moneyFilter === "all") return true; if (state.moneyFilter === "otm") return m === "otm"; return m === "itm" || m === "atm"; } function moneyFilterLabel() { if (state.moneyFilter === "otm") return "虚值"; if (state.moneyFilter === "itm") return "实值"; return ""; } function askLiqFilterOn() { return !!state.askLiqFilter; } function hasAskLiquidity(c) { if (!c) return false; if (c.ask_estimated) return false; const a = Number(c.ask); const s = Number(c.ask_sz); return Number.isFinite(a) && a > 0 && Number.isFinite(s) && s >= 1; } function syncAskLiqFilterFromChain(d) { if (!d || d.ask_liq_filter_enabled == null) return; state.askLiqFilter = !!d.ask_liq_filter_enabled; root.dataset.askLiqFilter = state.askLiqFilter ? "1" : "0"; } function countContractsForType(contracts) { if (state.chainView === "t") { return countStraddleStrikes(contracts); } return (contracts || []).filter(function (c) { if (c.opt_type !== state.optType) return false; if (askLiqFilterOn() && !hasAskLiquidity(c)) return false; return true; }).length; } function countStraddleStrikes(contracts) { const rows = buildStraddleRows(contracts).filter(function (row) { if (!askLiqFilterOn()) return true; return hasAskLiquidity(row.call) || hasAskLiquidity(row.put); }); return rows.length; } 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 findAtmStrike(rows, indexPx) { if (!rows.length || indexPx == null || Number.isNaN(Number(indexPx))) return null; let best = rows[0].strike; let bestDist = Math.abs(Number(rows[0].strike) - Number(indexPx)); rows.forEach(function (row) { const d = Math.abs(Number(row.strike) - Number(indexPx)); if (d < bestDist || (d === bestDist && Number(row.strike) < Number(best))) { bestDist = d; best = row.strike; } }); return best; } function matchesStrikeRowFilter(strike, indexPx, atmStrike) { if (state.moneyFilter === "all") return true; if (atmStrike != null && Number(strike) === Number(atmStrike)) return true; if (indexPx == null || Number.isNaN(Number(indexPx))) return true; if (state.moneyFilter === "itm") return Number(strike) <= Number(indexPx); if (state.moneyFilter === "otm") return Number(strike) >= Number(indexPx); return true; } function filterStraddleRows(rows, indexPx) { const atmStrike = findAtmStrike(rows, indexPx); return rows.filter(function (row) { if (!matchesStrikeRowFilter(row.strike, indexPx, atmStrike)) return false; if (askLiqFilterOn() && !hasAskLiquidity(row.call) && !hasAskLiquidity(row.put)) { return false; } return true; }); } // 默认窗口:平值 + 实值 3 档 + 虚值 3 档(T 型按行权价 ATM±3) const DEFAULT_ATM_SIDE = 3; function sliceAtmWindow(rows, indexPx) { if (state.strikeExpandAll || !rows.length) return rows; const atmStrike = findAtmStrike(rows, indexPx); const idx = rows.findIndex(function (r) { return Number(r.strike) === Number(atmStrike); }); const side = DEFAULT_ATM_SIDE; if (idx < 0) return rows.slice(0, Math.min(rows.length, side * 2 + 1)); const start = Math.max(0, idx - side); const end = Math.min(rows.length, idx + side + 1); return rows.slice(start, end); } function sliceListByMoneyness(list) { if (state.strikeExpandAll || !list.length) return list; const sorted = list.slice().sort(function (a, b) { return Number(a.strike) - Number(b.strike); }); const itm = []; const atm = []; const otm = []; sorted.forEach(function (c) { const m = String(c.moneyness || "").toLowerCase(); if (m === "atm") atm.push(c); else if (m === "itm") itm.push(c); else if (m === "otm") otm.push(c); }); if (!atm.length && !itm.length && !otm.length) { const indexPx = state.chain && state.chain.index_px; return sliceAtmWindow( sorted.map(function (c) { return { strike: c.strike, _c: c }; }), indexPx ).map(function (r) { return r._c; }); } const n = DEFAULT_ATM_SIDE; const isPut = String(state.optType || "").toUpperCase() === "P"; // Call: ITM 在下方取靠近 ATM 的末 N;OTM 取前 N。Put 相反。 const pickedItm = isPut ? itm.slice(0, n) : itm.slice(-n); const pickedOtm = isPut ? otm.slice(-n) : otm.slice(0, n); return pickedItm.concat(atm, pickedOtm).sort(function (a, b) { return Number(a.strike) - Number(b.strike); }); } function strikeWindowHintHtml(cols) { return ( '默认显示平值 + 实值3档 + 虚值3档 · 勾选「展开全部」查看该到期全部行权价' ); } function straddleAskPerUnit(callAsk, putAsk) { const c = Number(callAsk); const p = Number(putAsk); if (!Number.isFinite(c) || !Number.isFinite(p) || c <= 0 || p <= 0) return null; return Math.round((c + p) * 10000) / 10000; } function formatStraddleBand(strike, combinedAsk, callAsk, putAsk) { const k = Number(strike); if (!Number.isFinite(k)) return "—"; if (isCoinMarginMode()) { const pc = Number(callAsk); const pp = Number(putAsk); if (!Number.isFinite(pc) || !Number.isFinite(pp) || pc <= 0 || pp <= 0 || pc >= 1) return "—"; const lo = Math.round((k / (1 + pp)) * 10) / 10; const hi = Math.round((k / (1 - pc)) * 10) / 10; return lo.toFixed(0) + " ~ " + hi.toFixed(0); } const per = combinedAsk; if (per == null) return "—"; const d = Number(per); if (!Number.isFinite(d)) return "—"; const lo = Math.round((k - d) * 10) / 10; const hi = Math.round((k + d) * 10) / 10; return lo.toFixed(0) + " ~ " + hi.toFixed(0); } function formatStraddlePremiumCell(callAsk, putAsk) { const per = straddleAskPerUnit(callAsk, putAsk); if (per == null) return '不可双买'; return fmtUsdc(per) + " USDC"; } function pickBtnHtml(instId) { if (!instId) return "—"; return ''; } function syncMoneyFilterButtons() { document.querySelectorAll(".opt-money-btn").forEach(function (b) { b.classList.toggle("active", (b.getAttribute("data-money") || "") === state.moneyFilter); }); } function resetMoneyFilterToAll() { state.moneyFilter = "all"; syncMoneyFilterButtons(); } function updateUnderlyingLabel() { const el = document.getElementById("opt-order-eth-label"); if (el) el.textContent = state.underlying + " 数量"; } function filterChainContracts(contracts) { return (contracts || []).filter(function (c) { if (c.opt_type !== state.optType) return false; if (!matchesMoneyFilter(c.moneyness)) return false; if (askLiqFilterOn() && !hasAskLiquidity(c)) return false; return true; }); } function moneynessBadge(c) { const m = (c && c.moneyness) || ""; const label = (c && c.moneyness_label) || "—"; return '' + label + ""; } function optTypeLabel(t) { return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call"; } function sourceText(p) { const lab = (p && p.source_label) || "纯期权"; const src = (p && p.source) || "option"; let pid = p && p.source_plan_id; if (pid == null && p && p.hedge_plan_target && p.hedge_plan_target.plan_id != null) { pid = p.hedge_plan_target.plan_id; } if (src !== "option" && pid != null && pid !== "") return lab + " #" + pid; return lab; } function sourceBadgeHtml(p) { const src = (p && p.source) || "option"; const cls = src === "options_options" ? "opt-source-badge opt-source-badge--oo" : src === "perp_options" ? "opt-source-badge opt-source-badge--po" : "opt-source-badge opt-source-badge--plain"; return '' + sourceText(p) + ""; } 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 applyBudgetBuffer(raw) { if (raw == null || raw === "") return; const buf = Number(raw); if (Number.isNaN(buf) || buf <= 0) return; state.budgetBuffer = buf; const el = document.getElementById("opt-budget-buf"); if (el) el.textContent = fmt(buf, 2); } function renderIndexLine() { 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)); } if (state.chain && state.chain.budget_buffer != null) { applyBudgetBuffer(state.chain.budget_buffer); } const line = document.getElementById("opt-index-line"); if (line) { const liqHint = askLiqFilterOn() ? "仅显示卖一深度≥1张" : "显示全部卖一(含估算~)"; line.textContent = "指数 " + state.underlying + " ≈ " + fmt(idx, 2) + " · 默认最近一期 · " + liqHint + " · 实值含平值 · 虚值=价外"; } } function pickNearestExpiry(exps) { if (!exps || !exps.length) return ""; const now = Date.now(); let best = null; let bestDelta = Infinity; exps.forEach(function (e) { const t = Number(e.exp_time); if (!Number.isFinite(t)) return; const delta = t - now; if (delta < -60000) return; if (delta < bestDelta) { bestDelta = delta; best = e; } }); if (best) return String(best.exp_time); return String(exps[0].exp_time); } function renderExpiryOptions(preserveSelection) { const sel = document.getElementById("opt-exp-select"); if (!sel) return; const prev = preserveSelection !== false ? sel.value : ""; const exps = (state.chain && state.chain.expiries) || []; sel.innerHTML = ''; exps.forEach(function (e) { const o = document.createElement("option"); o.value = String(e.exp_time); o.textContent = expLabel(e.exp_time) + " (" + countContractsForType(e.contracts) + ")"; sel.appendChild(o); }); if (prev && exps.some(function (e) { return String(e.exp_time) === String(prev); })) { sel.value = prev; } else if (exps.length) { sel.value = pickNearestExpiry(exps); } } function setExpirySelectStatus(text) { const sel = document.getElementById("opt-exp-select"); if (!sel) return; sel.innerHTML = ""; const o = document.createElement("option"); o.value = ""; o.textContent = text || "选择到期日"; sel.appendChild(o); } function chainHasExpiries(chain) { return !!(chain && Array.isArray(chain.expiries) && chain.expiries.length > 0); } function renderExpiries() { renderExpiryOptions(true); renderIndexLine(); } function fmtPxSz(px, sz, estimated) { if (px === null || px === undefined || Number.isNaN(Number(px))) return "—"; let price = Number(px).toFixed(4).replace(/\.?0+$/, ""); 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 fmtCloseLevels(preview, tickSz) { if (preview && preview.bid_invalid) { return "暂无有效买盘"; } const levels = ((preview && preview.levels) || []).slice(0, 5); if (!levels.length) return "—"; return levels.map(function (x, idx) { const levelNo = x.level != null ? x.level : idx + 1; const liq = x.available_sheets != null ? x.available_sheets : x.sz; const pxTxt = fmtOptionPx(x.px, tickSz); if (liq === null || liq === undefined || liq === "" || Number.isNaN(Number(liq))) { return "买" + levelNo + " " + pxTxt; } const s = Number(liq); const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s); return "买" + levelNo + " " + pxTxt + "/" + size; }).join(" · "); } function closeGateHint(preview) { if (!preview) return ""; if (preview.bid_invalid || preview.manual_close_blocked) { return preview.bid_invalid_reason || "当前买一无效,禁止买一平仓"; } const gate = preview.close_gate || {}; // 2× 只是目标平仓门控,本身不会自动平;手动买一平不拦截 if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) { return "目标门控: " + (preview.close_gate_msg || gate.msg || "门控未过(见 env 目标平仓门控)"); } return ""; } function netPnlFromPos(p) { const preview = (p && p.close_preview) || {}; if (preview.bid_invalid) { const upl = p && p.upl != null ? Number(p.upl) : NaN; return Number.isFinite(upl) ? upl : null; } if (preview.estimated_pnl != null && !Number.isNaN(Number(preview.estimated_pnl))) { return Number(preview.estimated_pnl); } const covered = Number(preview.covered_sheets); const recv = Number(preview.total_received); const prem = Number(p && p.premium_paid); if ( preview.total_received != null && Number.isFinite(covered) && covered > 0 && !Number.isNaN(recv) && !Number.isNaN(prem) ) { return recv - prem; } const upl = p && p.upl != null ? Number(p.upl) : NaN; return Number.isFinite(upl) ? upl : null; } function netRoiFromPos(p, net) { const preview = (p && p.close_preview) || {}; if (preview.estimated_pnl_ratio_pct != null && !Number.isNaN(Number(preview.estimated_pnl_ratio_pct))) { return Number(preview.estimated_pnl_ratio_pct); } const prem = Number(p && p.premium_paid); if (net == null || Number.isNaN(prem) || prem <= 0) return null; return (net / prem) * 100; } function fmtUsdc(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(2); } function posPremiumCcy(p) { const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase(); if (ccy) return ccy; const mode = String((p && p.margin_mode) || "").toLowerCase(); const inst = String((p && p.inst_id) || ""); if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) { return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH"; } if (isCoinMarginMode && isCoinMarginMode()) { return ((inst.split("-")[0]) || "ETH").toUpperCase() || "ETH"; } return "USDC"; } function isCoinPos(p) { return posPremiumCcy(p) !== "USDC"; } function fmtPremiumAmt(v, ccy) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); const unit = String(ccy || "USDC").toUpperCase(); if (unit === "ETH" || unit === "BTC") { let s = n.toFixed(8).replace(/\.?0+$/, ""); return s || "0"; } return fmtUsdc(n); } /** 币本位盈亏双显:0.0018 ETH / 4.09U(按指数/现货价换算). */ function spotPxOf(p) { const n = Number(p && (p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px)); return Number.isFinite(n) && n > 0 ? n : null; } function fmtCoinUsdtDual(coinAmt, spotPx, ccy, signed) { if (coinAmt === null || coinAmt === undefined || Number.isNaN(Number(coinAmt))) return "—"; const n = Number(coinAmt); const unit = String(ccy || "ETH").toUpperCase(); if (unit !== "ETH" && unit !== "BTC") { const sign = signed && n > 0 ? "+" : ""; return sign + fmtUsdc(n) + "U"; } const absCoin = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0"; const coinSign = n < 0 ? "-" : signed && n > 0 ? "+" : ""; const coinTxt = coinSign + absCoin + " " + unit; const px = Number(spotPx); if (!Number.isFinite(px) || !(px > 0)) return coinTxt; const u = n * px; const absU = Math.abs(u).toFixed(2); const uSign = u < 0 ? "-" : signed && u > 0 ? "+" : ""; return coinTxt + " / " + uSign + absU + "U"; } function fmtPremiumAmtSigned(v, ccy) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); const sign = n > 0 ? "+" : ""; return sign + fmtPremiumAmt(n, ccy) + " " + (String(ccy || "USDC").toUpperCase()); } function fmtNetPnlDual(net, p) { const ccy = posPremiumCcy(p); if (ccy === "USDC") { if (net == null || Number.isNaN(Number(net))) return "—"; return fmtUsdc(Number(net)) + "U"; } return fmtCoinUsdtDual(net, spotPxOf(p), ccy, true); } function fmtClosePreview(preview, premiumPaid, p) { if (!preview || preview.total_received == null) return "—"; const ccy = posPremiumCcy(p); const recvTxt = fmtPremiumAmt(preview.total_received, ccy); let cls = ""; const prem = Number(premiumPaid); const recv = Number(preview.total_received); if (!Number.isNaN(prem) && !Number.isNaN(recv)) { if (recv > prem) cls = " pos-pnl-profit"; else if (recv < prem) cls = " pos-pnl-loss"; } return '' + recvTxt + " " + ccy + ""; } function fmtClosePreviewText(preview, p) { if (!preview || preview.total_received == null) return "—"; const ccy = posPremiumCcy(p); let text = fmtPremiumAmt(preview.total_received, ccy) + " " + ccy; if (preview.covered_sheets != null) { text += " · 覆盖 " + preview.covered_sheets + "张"; } if (preview.uncovered_sheets > 0) { text += " · 缺 " + preview.uncovered_sheets + "张"; } return text; } function fmtPreviewLevels(preview, p) { const levels = (preview && preview.levels) || []; if (!levels.length) return "暂无可用买盘深度"; const ccy = posPremiumCcy(p); return levels.map(function (x) { return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + fmtPremiumAmt(x.received, ccy) + " " + ccy; }).join("\n"); } function pnlCls(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return ""; const n = Number(v); if (n > 0) return "pos-pnl-profit"; if (n < 0) return "pos-pnl-loss"; return ""; } function expiryIntrinsicPerUnit(optType, strike, targetIdx) { const tgt = Number(targetIdx); const k = Number(strike); if (!Number.isFinite(tgt) || !Number.isFinite(k)) return null; const o = (optType || "").toUpperCase(); if (o === "C") return Math.max(0, tgt - k); if (o === "P") return Math.max(0, k - tgt); return null; } function estimateExpiryValue(optType, strike, targetIdx, ethAmount) { const amt = Number(ethAmount); const intrinsic = expiryIntrinsicPerUnit(optType, strike, targetIdx); if (intrinsic == null || !Number.isFinite(amt) || amt <= 0) return null; return Math.round(intrinsic * amt * 100) / 100; } function estimateExpiryProfit(optType, strike, targetIdx, ethAmount, totalPremium, indexPx) { const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount); let prem = Number(totalPremium); if (value == null || !Number.isFinite(prem)) return null; // 币本位权利金为币:与到期美元实值对比时先×指数 if (isCoinMarginMode()) { const idx = Number(indexPx); if (!Number.isFinite(idx) || idx <= 0) return null; prem = prem * idx; } return Math.round((value - prem) * 100) / 100; } /** 盈亏比 = 盈利金额 / 本合约权利金(目标位仅作到期实值参考). */ function estimateProfitRr(profit, totalPremium, indexPx) { const pnl = Number(profit); let prem = Number(totalPremium); if (!Number.isFinite(pnl) || !Number.isFinite(prem) || prem <= 0) return null; if (isCoinMarginMode()) { const idx = Number(indexPx); if (!Number.isFinite(idx) || idx <= 0) return null; prem = prem * idx; } return Math.round((pnl / prem) * 100) / 100; } function fmtProfitRr(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(2); } function isCoinMarginMode() { const ch = state.chain || {}; return ch.margin_mode === "coin" || ch.options_margin_mode === "coin"; } function calcContractLeverage(indexPx, ethAmount, totalPremium) { if (indexPx == null || ethAmount == null || totalPremium == null) return null; const idx = Number(indexPx); const amt = Number(ethAmount); const prem = Number(totalPremium); if (!Number.isFinite(idx) || !Number.isFinite(amt) || !Number.isFinite(prem) || amt <= 0 || prem <= 0) { return null; } // USDC: 名义(U)/权利金(U)=指数×币数/权利金; 币本位权利金为币: 名义(U)/(权利金币×指数)=币数/权利金币 if (isCoinMarginMode()) { return Math.round((amt / prem) * 10) / 10; } return Math.round((idx * amt) / prem * 10) / 10; } function fmtLeverage(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return "约 " + Number(v).toFixed(1) + "×"; } /** 链上展示:USDC=指数÷卖一(美元);币本位卖一为币报价 → 1÷卖一. */ function calcAskLeverage(indexPx, askPx) { if (indexPx == null || askPx == null) return null; const idx = Number(indexPx); const ask = Number(askPx); if (!Number.isFinite(idx) || !Number.isFinite(ask) || ask <= 0) return null; if (isCoinMarginMode()) { return Math.round((1 / ask) * 10) / 10; } return Math.round((idx / ask) * 10) / 10; } function fmtChainLeverage(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(1) + "×"; } function fmtUsdcSigned(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); const sign = n > 0 ? "+" : ""; return sign + fmtUsdc(n) + " USDC"; } function updateOrderEstimates() { const levEl = document.getElementById("opt-order-leverage"); const valueEl = document.getElementById("opt-est-value"); const profitEl = document.getElementById("opt-est-profit"); const rrEl = document.getElementById("opt-est-rr") || document.getElementById("opt-est-leverage"); const targetEl = document.getElementById("opt-target-idx"); const q = state.orderQuote; if (!q || !q.ok || !q.can_open) { if (levEl) levEl.textContent = "—"; if (valueEl) valueEl.textContent = "—"; if (profitEl) { profitEl.textContent = "—"; profitEl.className = "v"; } if (rrEl) { rrEl.textContent = "—"; rrEl.className = "v"; } return; } const sz = q.sizing || {}; const ethAmount = sz.eth_amount; const premium = sz.total_premium; const lev = calcContractLeverage(q.index_px, ethAmount, premium); if (levEl) levEl.textContent = fmtLeverage(lev); if (valueEl && profitEl && targetEl) { const targetRaw = targetEl.value; if (targetRaw === "" || targetRaw == null) { valueEl.textContent = "—"; profitEl.textContent = "—"; profitEl.className = "v"; if (rrEl) { rrEl.textContent = "—"; rrEl.className = "v"; } } else { const value = estimateExpiryValue(q.opt_type, q.strike, Number(targetRaw), ethAmount); const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium, q.index_px); const rr = estimateProfitRr(profit, premium, q.index_px); if (value == null || Number.isNaN(value)) { valueEl.textContent = "—"; } else { valueEl.textContent = isCoinMarginMode() ? (fmtUsdc(value) + " U(估)") : (fmtUsdc(value) + " USDC"); } if (profit == null || Number.isNaN(profit)) { profitEl.textContent = "—"; profitEl.className = "v"; } else { profitEl.textContent = isCoinMarginMode() ? ((Number(profit) > 0 ? "+" : "") + fmtUsdc(profit) + " U(估)") : fmtUsdcSigned(profit); profitEl.className = "v " + pnlCls(profit); } if (rrEl) { if (rr == null || Number.isNaN(rr)) { rrEl.textContent = "—"; rrEl.className = "v"; } else { rrEl.textContent = fmtProfitRr(rr); rrEl.className = "v " + pnlCls(rr); } } } } } function updateEstimatedProfit() { updateOrderEstimates(); } function fmtDist(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); if (n < 0) return n.toFixed(1); return n.toFixed(1); } function distBeClass(v) { if (v === null || v === undefined || Number.isNaN(Number(v))) return ""; const n = Number(v); if (n > 0) return "opt-be-dist-up"; return ""; } function bindStrikePickButtons(tbody) { tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) { btn.addEventListener("click", function () { selectContract(btn.getAttribute("data-inst"), btn); }); }); } function finishStrikeRender(tbody, prevSelected, matchedSelected) { bindStrikePickButtons(tbody); if (matchedSelected && prevSelected) { selectContract(prevSelected, null, true); } else if (!matchedSelected) { state.selectedInst = null; } } function renderStrikes() { syncChainViewUI(); if (state.chainView === "t") renderStrikesT(); else renderStrikesList(); } function renderStrikesList() { const tbody = document.getElementById("opt-strike-tbody"); const expMs = document.getElementById("opt-exp-select").value; const prevSelected = state.selectedInst; const cols = strikeTableColspan(); 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) { state.selectedInst = null; return; } let list = filterChainContracts(exp.contracts); list = sliceListByMoneyness(list); if (!list.length) { const label = moneyFilterLabel(); const suffix = label ? label : optTypeLabel(state.optType); const liqTip = askLiqFilterOn() ? "(卖一深度≥1 时才显示,可在环境配置关闭筛选)" : ""; tbody.innerHTML = '该到期日暂无' + suffix + "合约" + liqTip + ""; state.selectedInst = null; return; } let matchedSelected = false; const indexPx = state.chain && state.chain.index_px; const atmStrike = findAtmStrike( list.map(function (c) { return { strike: c.strike }; }), indexPx ); 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); if (atmStrike != null && Number(c.strike) === Number(atmStrike)) { tr.classList.add("opt-strike-row-atm"); } const chainLev = calcAskLeverage(indexPx, c.ask); tr.innerHTML = "" + c.strike + "" + "" + moneynessBadge(c) + "" + "" + c.inst_id + "" + "" + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated) + "" + '' + fmtChainLeverage(chainLev) + "" + "" + fmtPxSz(c.bid, c.bid_sz) + "" + "" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "" + '' + fmtDist(c.dist_expiry_be) + "" + '' + pickBtnHtml(c.inst_id) + ""; tbody.appendChild(tr); if (c.inst_id === prevSelected) matchedSelected = true; }); if (!state.strikeExpandAll && list.length >= 1) { const hint = document.createElement("tr"); hint.className = "opt-strike-hint-row"; hint.innerHTML = strikeWindowHintHtml(cols); tbody.appendChild(hint); } finishStrikeRender(tbody, prevSelected, matchedSelected); } function renderStrikesT() { const tbody = document.getElementById("opt-strike-tbody"); const expMs = document.getElementById("opt-exp-select").value; const prevSelected = state.selectedInst; const cols = strikeTableColspan(); const indexPx = state.chain && state.chain.index_px; 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) { state.selectedInst = null; return; } let rows = filterStraddleRows(buildStraddleRows(exp.contracts), indexPx); rows = sliceAtmWindow(rows, indexPx); if (!rows.length) { const label = moneyFilterLabel(); const suffix = label ? label + "区" : "匹配"; tbody.innerHTML = '该到期日暂无' + suffix + "行权价"; state.selectedInst = null; return; } const atmStrike = findAtmStrike(rows, indexPx); let matchedSelected = false; rows.forEach(function (row) { const callRaw = row.call; const putRaw = row.put; const call = callRaw && (!askLiqFilterOn() || hasAskLiquidity(callRaw)) ? callRaw : null; const put = putRaw && (!askLiqFilterOn() || hasAskLiquidity(putRaw)) ? putRaw : null; const combined = straddleAskPerUnit(call && call.ask, put && put.ask); const tr = document.createElement("tr"); tr.className = "opt-strike-row opt-strike-row-t"; tr.setAttribute("data-strike", String(row.strike)); if (Number(row.strike) === Number(atmStrike)) tr.classList.add("opt-strike-row-atm"); if (call && call.inst_id) tr.setAttribute("data-call-inst", call.inst_id); if (put && put.inst_id) tr.setAttribute("data-put-inst", put.inst_id); tr.innerHTML = '' + (call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated) : "—") + "" + '' + (call ? moneynessBadge(call) : "—") + "" + '' + pickBtnHtml(call && call.inst_id) + "" + '' + row.strike + "" + '' + formatStraddlePremiumCell(call && call.ask, put && put.ask) + "" + '' + formatStraddleBand(row.strike, combined, call && call.ask, put && put.ask) + "" + '' + (put ? moneynessBadge(put) : "—") + "" + '' + (put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated) : "—") + "" + '' + pickBtnHtml(put && put.inst_id) + ""; tbody.appendChild(tr); if (prevSelected && ((call && call.inst_id === prevSelected) || (put && put.inst_id === prevSelected))) { matchedSelected = true; } }); if (!state.strikeExpandAll && rows.length >= 1) { const hint = document.createElement("tr"); hint.className = "opt-strike-hint-row"; hint.innerHTML = strikeWindowHintHtml(cols); tbody.appendChild(hint); } finishStrikeRender(tbody, prevSelected, matchedSelected); } function fillOrderPanel(d) { syncCompoundFlagsFromPayload(d); state.orderQuote = d && d.ok ? d : null; const sz = d.sizing || {}; const canOpen = !!(d && d.ok && d.can_open); document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || ""; const askEl = document.getElementById("opt-order-ask"); if (askEl) { askEl.textContent = canOpen ? fmtPxSz(d.ask, d.ask_sz) : "—"; } const bidEl = document.getElementById("opt-order-bid"); if (bidEl) bidEl.textContent = fmtPxSz(d.bid, d.bid_sz); const refEl = document.getElementById("opt-order-ref-ask"); if (refEl) { if (canOpen) { refEl.textContent = "—"; } else if (d.ref_ask != null && !Number.isNaN(Number(d.ref_ask))) { refEl.textContent = fmtPxSz(d.ref_ask, null, true) + " (不可开仓)"; } else if (d.mark != null && !Number.isNaN(Number(d.mark))) { refEl.textContent = fmtPxSz(d.mark, null, true) + " (不可开仓)"; } else { refEl.textContent = "—"; } } document.getElementById("opt-order-sheets").textContent = canOpen && sz.sheets != null ? sz.sheets : "—"; document.getElementById("opt-order-eth").textContent = canOpen && sz.eth_amount != null ? sz.eth_amount : "—"; updateUnderlyingLabel(); const coinMode = isCoinMarginMode() || (d && d.options_margin_mode === "coin"); const premCcy = (sz.premium_ccy || (coinMode ? ((d.inst_id || "").split("-")[0] || "ETH") : "USDC")).toUpperCase(); document.getElementById("opt-order-premium").textContent = canOpen && sz.total_premium != null ? (coinMode ? (fmt(sz.total_premium, 6) + " " + premCcy) : (fmtUsdc(sz.total_premium) + " USDC")) : "—"; const beEl = document.getElementById("opt-order-expiry-be"); const distEl = document.getElementById("opt-order-dist-be"); if (beEl) { beEl.textContent = d.expiry_be_px != null ? fmt(d.expiry_be_px, 0) : "—"; } if (distEl) { distEl.textContent = fmtDist(d.dist_expiry_be); distEl.className = "v " + distBeClass(d.dist_expiry_be); } const openBtn = document.getElementById("opt-open-btn"); if (openBtn) { openBtn.disabled = !canOpen || sz.ok === false; const bud = (d && d.coin_budget && d.coin_budget.budget_usdt) || (state.chain && state.chain.coin_budget && state.chain.coin_budget.budget_usdt); if (!canOpen || sz.ok === false) { openBtn.textContent = coinMode ? ((d && d.msg) || (sz && sz.msg) || "无法开仓") : "暂无卖一深度,无法开仓"; } else if (coinMode) { const buyU = sz && sz.buy_usdt != null ? sz.buy_usdt : null; openBtn.textContent = buyU != null ? ("买币并开仓(约 " + Number(buyU).toFixed(2) + " USDT)") : (bud != null ? "买币并开仓(预算上限 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"); } else { openBtn.textContent = "限价买入 @ 卖一"; } } const msgEl = document.getElementById("opt-order-msg"); if (!d.ok) { msgEl.textContent = d.msg || "报价失败"; msgEl.classList.add("opt-error"); } else if (!canOpen) { const ref = d.ref_ask != null ? d.ref_ask : d.mark; let tip = d.msg || d.open_block_msg || "当前无卖一深度,无法按卖一限价买入"; if (ref != null && !Number.isNaN(Number(ref))) { tip += "。参考标记价 ~" + Number(ref).toFixed(4).replace(/\.?0+$/, "") + "(仅供参考,不可用于开仓)"; } else { tip += "。无可用参考标记价"; } msgEl.textContent = tip; msgEl.classList.add("opt-error"); } else if (sz.ok === false) { msgEl.textContent = sz.msg || ""; msgEl.classList.add("opt-error"); } else if (sz.ask_depth_capped) { msgEl.textContent = sz.msg || "已按卖一深度限制张数"; msgEl.classList.remove("opt-error"); } else if (coinMode && sz.est_note) { msgEl.textContent = sz.est_note; msgEl.classList.remove("opt-error"); } else { msgEl.textContent = ""; msgEl.classList.remove("opt-error"); } updateEstimatedProfit(); } async function selectContract(instId, pickBtn, silent) { const seq = ++selectSeq; 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)); if (seq !== selectSeq) return d; fillOrderPanel(d); return d; } finally { if (seq === selectSeq) { syncPickButtons(instId); if (!silent) placeOrderPanelAfter(instId); } } } async function loadChain(opts) { const soft = !!(opts && opts.soft); const uly = state.underlying; const seq = ++chainLoadSeq; const btn = document.getElementById("opt-load-chain"); if (btn && !soft) btn.disabled = true; if (!soft) { setExpirySelectStatus("加载到期日中…"); const tbody = document.getElementById("opt-strike-tbody"); if (tbody) { tbody.innerHTML = '加载期权链…'; } } try { let d = null; let lastMsg = ""; for (let attempt = 0; attempt < 2; attempt++) { if (seq !== chainLoadSeq) return; d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(uly)); if (seq !== chainLoadSeq) return; if (d && d.ok && chainHasExpiries(d)) break; lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日"; const rateLimited = /50011|Too Many Requests|RateLimit/i.test(String(lastMsg || "")); d = null; if (attempt === 0 && !rateLimited) { if (!soft) setExpirySelectStatus("重试加载到期日…"); await new Promise(function (resolve) { setTimeout(resolve, 400); }); } else { break; } } if (seq !== chainLoadSeq) return; if (!d || !d.ok || !chainHasExpiries(d)) { if (chainHasExpiries(state.chain) && state.chain.underlying === uly) { if (!soft) { renderExpiries(); renderStrikes(); } return; } if (soft) return; setExpirySelectStatus("选择到期日"); const tbody = document.getElementById("opt-strike-tbody"); if (tbody) { tbody.innerHTML = '' + (lastMsg || "暂无到期日,请点「刷新链」") + ""; } alert(lastMsg || "加载到期日失败,请点「刷新链」重试"); return; } const keepExp = soft ? (document.getElementById("opt-exp-select") || {}).value : ""; state.chain = d; panelCache.chain = d; panelCache.underlying = uly; panelCache.optType = state.optType; syncAskLiqFilterFromChain(d); if (!soft) { state.selectedInst = null; resetMoneyFilterToAll(); state.strikeExpandAll = false; const expandCb = document.getElementById("opt-strike-expand-all"); if (expandCb) expandCb.checked = false; parkOrderPanel(); } updateUnderlyingLabel(); updateRetrySellCoinUi(lastBalancesPayload); renderExpiries(); if (soft && keepExp) { const sel = document.getElementById("opt-exp-select"); if (sel && Array.from(sel.options).some(function (o) { return o.value === keepExp; })) { sel.value = keepExp; } } // soft 时保留 selectedInst;renderStrikes 会先 park 再按 prevSelected 静默重挂下单面板 renderStrikes(); } catch (e) { if (seq !== chainLoadSeq || soft) return; setExpirySelectStatus("选择到期日"); const tbody = document.getElementById("opt-strike-tbody"); if (tbody) { tbody.innerHTML = '加载失败: ' + String((e && e.message) || e) + ""; } } finally { if (seq === chainLoadSeq && btn) btn.disabled = false; } } async function openPosition() { if (!state.selectedInst) { alert("请先选择合约"); return false; } const q = state.orderQuote; if (!q || !q.ok || !q.can_open) { alert((q && (q.msg || q.open_block_msg)) || "暂无卖一深度,无法按卖一开仓"); return false; } if (q.sizing && q.sizing.ok === false) { alert(q.sizing.msg || "张数无效"); return false; } const btn = document.getElementById("opt-open-btn"); btn.disabled = true; try { updateSizeInputs(); 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) || 1; } else if (mode === "compound_full" && !compoundFullEnabled()) { body.mode = "sheets"; body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10) || 1; } const tgtRaw = (document.getElementById("opt-target-idx").value || "").trim(); if (tgtRaw !== "") { const tgt = parseFloat(tgtRaw); if (!Number.isFinite(tgt) || tgt <= 0) { alert("目标位无效"); return false; } body.target_index = tgt; } const peEnabled = !!(document.getElementById("opt-profit-exit-enabled") || {}).checked; if (peEnabled) { const multRaw = (document.getElementById("opt-profit-exit-mult") || {}).value; const mult = parseFloat(multRaw); if (!Number.isFinite(mult) || mult <= 0) { alert("翻倍倍数无效"); return false; } body.profit_exit_enabled = true; body.profit_exit_mult = mult; } 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 ? "下单已提交,可在「当前委托」查看/撤销" : (d.msg || "失败"); msgEl.classList.toggle("opt-error", !d.ok); if (d.ok) { refreshPendingOrders(); startPendingOrdersPoll(); refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); closeOrderDialog(); setOptionsPosTab("pending"); return true; } alert(d.msg || "下单失败"); return false; } finally { const latest = state.orderQuote; const coinMode = isCoinMarginMode() || (latest && latest.options_margin_mode === "coin"); const bud = (latest && latest.coin_budget && latest.coin_budget.budget_usdt) || (state.chain && state.chain.coin_budget && state.chain.coin_budget.budget_usdt); btn.disabled = !(latest && latest.ok && latest.can_open && !(latest.sizing && latest.sizing.ok === false)); if (!(latest && latest.can_open) || (latest && latest.sizing && latest.sizing.ok === false)) { btn.textContent = coinMode ? ((latest && (latest.msg || (latest.sizing && latest.sizing.msg))) || "无法开仓") : "暂无卖一深度,无法开仓"; } else if (coinMode) { const buyU = latest && latest.sizing && latest.sizing.buy_usdt != null ? latest.sizing.buy_usdt : null; btn.textContent = buyU != null ? ("买币并开仓(约 " + Number(buyU).toFixed(2) + " USDT)") : (bud != null ? "买币并开仓(预算上限 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"); } else { btn.textContent = "限价买入 @ 卖一"; } } } function renderPositionCardInner(p) { const net = netPnlFromPos(p); const roi = netRoiFromPos(p, net); const uplCls = pnlCls(net); const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long"; const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time; const expAttr = expMs != null && expMs !== "" ? String(expMs) : ""; const closePreview = p.close_preview || {}; const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos; const tickSz = p.tick_sz; const premCcy = posPremiumCcy(p); const coinPos = isCoinPos(p); const premTxt = fmtDisplay( p.premium_paid_fmt, p.premium_paid != null ? fmtPremiumAmt(p.premium_paid, premCcy) : null ); // 优先用数值+tick 现算,避免接口侧 mark_px_fmt 带着浮点毛刺直出 const avgTxt = p.avg_px != null ? fmtOptionPx(p.avg_px, tickSz) : fmtDisplay(p.avg_px_fmt); const markTxt = p.mark_px != null ? fmtOptionPx(p.mark_px, tickSz) : fmtDisplay(p.mark_px_fmt); const netTxt = closePreview.bid_invalid || net == null ? "—" : fmtNetPnlDual(net, p); return ( '
' + '
' + (p.inst_id || "") + '' + '' + optTypeLabel(p.opt_type) + "" + sourceBadgeHtml(p) + "
" + '
' + '' + "
" + '
' + '持仓来源: ' + sourceText(p) + "" + '行权价: ' + fmt(p.strike, 0) + "" + '张数: ' + fmt(p.pos, 0) + " · 币量 " + fmt(p.eth_amount, 4) + "" + (expAttr ? '到期倒计时: ' : "") + "
" + '
' + '
权利金' + premTxt + " " + premCcy + "
" + '
开仓均价' + avgTxt + "
" + '
标记价' + markTxt + "
" + '
指数价' + fmt(p.idx_px, 0) + "
" + '
到期平衡' + fmt(p.expiry_be_px, 0) + "
" + '
平掉回本' + fmt(p.close_be_px, 0) + "
" + '
净盈亏' + netTxt + "
" + '
收益率' + (closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "
" + '
买盘深度' + fmtCloseLevels(closePreview, tickSz) + "
" + '
按买盘回收' + (closePreview.bid_invalid ? '暂无有效买盘' : fmtClosePreview(closePreview, p.premium_paid, p)) + "
" + "
" + (function () { const hint = closeGateHint(closePreview); return hint ? '
' + hint + "
" : ""; })() + renderTargetDelegateRow(p) + renderProfitExitRow(p) ); } function formatProfitExitMultLabel(mult) { const n = Number(mult); if (!Number.isFinite(n) || n <= 0) return "1倍"; if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n)) + "倍"; return fmt(n, 2) + "倍"; } function renderProfitExitRow(p) { const inst = p.inst_id || ""; if (p.hedge_plan_target) { return ""; } const enabled = !!p.profit_exit_enabled; const serverMult = p.profit_exit_mult != null && Number(p.profit_exit_mult) > 0 ? Number(p.profit_exit_mult) : 1; const draft = state.profitExitDraftByInst[inst]; const multDisp = draft != null && String(draft).trim() !== "" ? String(draft) : String(serverMult); const multNum = Number(multDisp); const multLabel = formatProfitExitMultLabel( Number.isFinite(multNum) && multNum > 0 ? multNum : serverMult ); const statePe = String(p.profit_exit_state || (enabled ? "active" : "idle")); const req = p.profit_exit_required_recycle; const premCcy = posPremiumCcy(p); let statusTxt = enabled ? ("监控中 · " + multLabel) : "未开启"; if (enabled && statePe === "closing") statusTxt = "平仓挂单中 · " + multLabel; return ( '
' + '翻倍' + '" + '' + '" + '' + statusTxt + "" + '' + (enabled ? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtPremiumAmt(req, premCcy) + " " + premCcy) : "")) : "开启后自选倍数;达标按买一限价平;可随时关闭") + "" + "
" ); } function posEthAmount(p) { if (p.eth_amount != null && Number(p.eth_amount) > 0) return Number(p.eth_amount); const sheets = Number(p.avail_pos != null ? p.avail_pos : p.pos); const ct = Number(p.ct_mult != null ? p.ct_mult : 0.01); if (Number.isFinite(sheets) && sheets > 0 && Number.isFinite(ct) && ct > 0) return sheets * ct; return null; } function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx, p) { const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount); const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx); const rr = estimateProfitRr(profit, premiumPaid, indexPx); if (value == null && profit == null && rr == null) return ""; const coinPos = isCoinPos(p); const valueUnit = coinPos ? " U(估)" : " USDC"; const profitTxt = profit == null ? "—" : (coinPos ? ((Number(profit) > 0 ? "+" : "") + fmtUsdc(profit) + " U(估)") : fmtUsdcSigned(profit)); let html = ''; html += '价值' + (value == null ? "—" : fmtUsdc(value) + valueUnit) + ""; html += '预估盈利' + profitTxt + ""; html += '盈亏比' + (rr == null ? "—" : fmtProfitRr(rr)) + ""; html += ""; return html; } function renderTargetDelegateRow(p) { const inst = p.inst_id || ""; const hedgeTarget = p.hedge_plan_target || null; if (hedgeTarget) { const rr = hedgeTarget.profit_rr != null ? Number(hedgeTarget.profit_rr) : null; if (rr != null && rr > 0) { return ( '
' + '对冲计划' + '计划 #' + hedgeTarget.plan_id + " · 盈亏比 " + fmt(rr, 2) + "" + '进行中 · 盈利达总权利金×盈亏比仅平盈利腿;亏损腿按本合约残值平或到期平' + "
" ); } if (Number(hedgeTarget.target_index) > 0) { const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; return ( '
' + '对冲计划' + '计划 #' + hedgeTarget.plan_id + " · " + side + " " + fmt(hedgeTarget.target_index, 1) + "" + '进行中 · 由对冲计划监控,到位后仅平盈利腿' + "
" ); } } const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null; const armed = tgt != null && Number.isFinite(tgt) && tgt > 0; const ethAmt = posEthAmount(p); const prem = p.premium_paid; const estHtml = armed ? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem, p.idx_px, p) : ''; return ( '
' + '委托' + '' + '' + '" + (armed ? '目标 ' + fmt(tgt, 1) + "" : "") + estHtml + '' + (armed ? "监控中 · 目标位参考 · 到位按买一限价平" : "目标位参考(到期实值估盈亏比) · 到位按买一限价平 · 到期即止损") + "" + "
" ); } function updatePosTargetEstimate(row) { if (!row) return; const est = row.querySelector(".opt-target-est"); if (!est) return; const inp = row.querySelector(".opt-pos-target-input"); const typed = inp ? String(inp.value || "").trim() : ""; const armed = row.getAttribute("data-armed-target") || ""; const targetRaw = typed !== "" ? typed : armed; if (targetRaw === "") { est.className = "opt-target-est opt-target-est--idle"; est.innerHTML = ""; return; } const html = formatTargetEstimateHtml( row.getAttribute("data-opt-type"), row.getAttribute("data-strike"), targetRaw, row.getAttribute("data-eth"), row.getAttribute("data-prem"), row.getAttribute("data-idx"), { premium_ccy: row.getAttribute("data-prem-ccy"), margin_mode: row.getAttribute("data-prem-ccy") === "USDC" ? "usdc" : "coin", inst_id: row.getAttribute("data-inst") } ); if (!html) { est.className = "opt-target-est opt-target-est--idle"; est.innerHTML = ""; return; } const tmp = document.createElement("div"); tmp.innerHTML = html; const node = tmp.firstChild; est.className = "opt-target-est"; est.innerHTML = node ? node.innerHTML : ""; } function renderPositionCard(p) { return ( '
' + renderPositionCardInner(p) + "
" ); } function renderPositionAccordionItem(p, expanded) { const net = netPnlFromPos(p); const roi = netRoiFromPos(p, net); const uplCls = pnlCls(net); const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long"; const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time; const expAttr = expMs != null && expMs !== "" ? String(expMs) : ""; const inst = p.inst_id || ""; return ( '
' + '" + '
' + '
' + renderPositionCardInner(p) + "
" ); } function applyAccordionState() { const wrap = document.getElementById("opt-pos-cards"); if (!wrap) return; wrap.querySelectorAll(".opt-pos-accordion-item").forEach(function (el) { const open = el.getAttribute("data-inst") === state.expandedPosInst; el.classList.toggle("is-expanded", open); const btn = el.querySelector(".opt-pos-bar"); const body = el.querySelector(".opt-pos-accordion-body"); if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false"); if (body) body.hidden = !open; }); } function bindPositionActions(container) { if (!container) return; container.querySelectorAll(".opt-close-btn").forEach(function (btn) { btn.addEventListener("click", function (e) { e.stopPropagation(); closePosition(btn.getAttribute("data-inst"), btn); }); }); container.querySelectorAll(".opt-target-set-btn").forEach(function (btn) { btn.addEventListener("click", function (e) { e.stopPropagation(); setPositionTarget(btn.getAttribute("data-inst"), btn); }); }); container.querySelectorAll(".opt-target-cancel-btn").forEach(function (btn) { btn.addEventListener("click", function (e) { e.stopPropagation(); cancelPositionTarget(btn.getAttribute("data-inst"), btn); }); }); container.querySelectorAll(".opt-profit-exit-save-btn").forEach(function (btn) { btn.addEventListener("click", function (e) { e.stopPropagation(); savePositionProfitExit(btn.getAttribute("data-inst"), btn); }); }); container.querySelectorAll(".opt-pos-profit-exit-enabled").forEach(function (cb) { cb.addEventListener("click", function (e) { e.stopPropagation(); }); }); container.querySelectorAll(".opt-pos-profit-exit-mult").forEach(function (inp) { // 倍数随时可改;「开启/应用」只控制是否监控,不再因未勾选而 disabled inp.disabled = false; inp.removeAttribute("readonly"); inp.addEventListener("click", function (e) { e.stopPropagation(); }); inp.addEventListener("mousedown", function (e) { e.stopPropagation(); }); inp.addEventListener("focus", function (e) { e.stopPropagation(); }); inp.addEventListener("input", function () { const id = inp.getAttribute("data-inst") || ""; if (!id) return; const draft = String(inp.value || ""); if (draft.trim() === "") delete state.profitExitDraftByInst[id]; else state.profitExitDraftByInst[id] = draft; }); inp.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); savePositionProfitExit(inp.getAttribute("data-inst"), null); } }); }); container.querySelectorAll(".opt-pos-target-input").forEach(function (inp) { inp.addEventListener("click", function (e) { e.stopPropagation(); }); inp.addEventListener("input", function () { const instId = inp.getAttribute("data-inst") || ""; const draft = String(inp.value || ""); if (instId) { if (draft.trim() === "") delete state.targetDraftByInst[instId]; else state.targetDraftByInst[instId] = draft; } updatePosTargetEstimate(inp.closest(".opt-target-row")); }); inp.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); setPositionTarget(inp.getAttribute("data-inst"), null); } }); // 重绘后恢复预估展示(草稿或已设定目标) updatePosTargetEstimate(inp.closest(".opt-target-row")); }); container.querySelectorAll(".opt-pos-bar").forEach(function (bar) { bar.addEventListener("click", function () { const item = bar.closest(".opt-pos-accordion-item"); if (!item) return; const inst = item.getAttribute("data-inst"); state.expandedPosInst = state.expandedPosInst === inst ? null : inst; applyAccordionState(); if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) { OptionsExpiryCountdown.ensureTimer(); } }); }); } async function setPositionTarget(inst, btn) { if (!inst) return; const card = document.querySelector('.opt-pos-card[data-inst="' + inst + '"]') || document.querySelector('.opt-pos-accordion-item[data-inst="' + inst + '"]'); const row = card ? card.querySelector(".opt-target-row") : null; const inp = card ? card.querySelector(".opt-pos-target-input") : null; const raw = inp ? String(inp.value || "").trim() : ""; const tgt = parseFloat(raw); if (!Number.isFinite(tgt) || tgt <= 0) { alert("请输入有效目标指数价"); return; } if (btn) btn.disabled = true; try { const d = await apiJson("/api/options/target", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inst_id: inst, target_index: tgt }), }); if (!d.ok) { alert(d.msg || "设定失败"); return; } delete state.targetDraftByInst[inst]; if (inp) inp.value = ""; if (row) { row.setAttribute("data-armed-target", String(tgt)); updatePosTargetEstimate(row); } await refreshAllPositions(); } finally { if (btn) btn.disabled = false; } } async function cancelPositionTarget(inst, btn) { if (!inst) return; if (btn) btn.disabled = true; try { const d = await apiJson("/api/options/target/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inst_id: inst }), }); if (!d.ok) { alert(d.msg || "取消失败"); return; } delete state.targetDraftByInst[inst]; await refreshAllPositions(); } finally { if (btn) btn.disabled = false; } } async function savePositionProfitExit(inst, btn) { if (!inst) return; const card = document.querySelector('.opt-pos-card[data-inst="' + inst + '"]') || document.querySelector('.opt-pos-accordion-item[data-inst="' + inst + '"]'); const row = card ? card.querySelector(".opt-profit-exit-pos-row") : null; const enabledEl = row ? row.querySelector(".opt-pos-profit-exit-enabled") : null; const multEl = row ? row.querySelector(".opt-pos-profit-exit-mult") : null; const mode = btn && btn.getAttribute("data-mode"); let enabled = !!(enabledEl && enabledEl.checked); if (mode === "cancel") enabled = false; if (mode === "apply") { enabled = true; if (enabledEl) enabledEl.checked = true; } let mult = 1; if (enabled) { mult = parseFloat(multEl ? multEl.value : "1"); if (!Number.isFinite(mult) || mult <= 0) { alert("翻倍倍数无效"); return; } } if (btn) btn.disabled = true; try { const d = await apiJson("/api/options/profit-exit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inst_id: inst, enabled: enabled, mult: mult }), }); if (!d.ok) { alert(d.msg || "保存失败"); return; } delete state.profitExitDraftByInst[inst]; await refreshAllPositions(); } finally { if (btn) btn.disabled = false; } } function paintTargetMonitors(list) { const box = document.getElementById("opt-target-monitors"); const host = document.getElementById("opt-target-monitors-list"); if (!box || !host) return; const rows = Array.isArray(list) ? list.filter(function (t) { return t && t.inst_id; }) : []; if (!rows.length) { box.hidden = true; host.innerHTML = ""; return; } box.hidden = false; host.innerHTML = rows.map(function (t) { const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; const managed = t.managed_by === "hedge_plan"; return ( '
' + '' + (t.inst_id || "") + "" + '' + side + " " + fmt(t.target_index, 1) + "" + (managed ? '对冲计划 #' + (t.plan_id || "") + " · 进行中" : '') + "
" ); }).join(""); host.querySelectorAll(".opt-target-mon-cancel").forEach(function (btn) { btn.addEventListener("click", function () { cancelPositionTarget(btn.getAttribute("data-inst"), btn); }); }); } async function closePosition(inst, btn) { const sheets = btn && btn.getAttribute("data-sheets") ? parseInt(btn.getAttribute("data-sheets"), 10) : null; let url = "/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=close_preview"; if (sheets && sheets > 0) url += "&sheets=" + encodeURIComponent(sheets); const q = await apiJson(url); if (!q.ok) { alert(q.msg || "获取买一价失败"); return; } const preview = q.close_preview || {}; if (preview.bid_invalid || preview.manual_close_blocked) { alert(preview.bid_invalid_reason || "当前买一为无效残档,禁止买一平仓。"); return; } if (!preview.covered_sheets || preview.covered_sheets <= 0) { alert("暂无有效买一深度,请稍后重试或到 OKX App 挂限价"); return; } const lv = (preview.levels && preview.levels[0]) || {}; const posLike = { inst_id: inst, premium_ccy: q.premium_ccy || (preview.close_gate && preview.close_gate.premium_ccy) || null, margin_mode: q.options_margin_mode || q.margin_mode || null, }; const premCcy = posPremiumCcy(posLike); const msg = [ "按买一限价卖出本轮可平张数?", "合约: " + inst, "锁定买一: " + (lv.px != null ? lv.px : "—") + " × " + (lv.sheets != null ? lv.sheets : preview.covered_sheets) + " 张", "预计收回: " + fmtClosePreviewText(preview, posLike), preview.estimated_pnl != null ? ("预估盈亏: " + fmtPremiumAmtSigned(preview.estimated_pnl, premCcy)) : "", preview.uncovered_sheets > 0 ? "\n注意: 买一深度不足,预计仍剩 " + preview.uncovered_sheets + " 张,需下次再平。" : "" ].filter(function (x) { return x !== ""; }).join("\n"); if (!confirm(msg)) 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, mode: "bid1", sheets: sheets }), }); if (r.ok) { let okMsg = "买一平仓已提交 " + (r.submitted_sheets || 0) + " 张"; if (r.locked_bid_px != null) okMsg += "\n锁定买一: " + r.locked_bid_px; if (r.premium_received != null) { okMsg += "\n预估收回: " + fmtPremiumAmt(r.premium_received, premCcy) + " " + premCcy; } if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)"; if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason; const spotSell = r.spot_sell; if (spotSell && spotSell.ok === false) { okMsg += "\n卖回 " + premCcy + " 失败: " + (spotSell.msg || "未知错误"); okMsg += "\n可点「重试卖回 " + premCcy + "」手动卖出交易户全部可用币。"; } else if (spotSell && spotSell.ok && spotSell.sell && spotSell.sell.coin_sold != null) { okMsg += "\n已卖回: " + fmt(Number(spotSell.sell.coin_sold), 6) + " " + premCcy; } alert(okMsg); } else { alert(r.msg || "平仓失败"); } refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); void refreshBridgeSellUi(true); } finally { if (btn) btn.disabled = false; } } function setOptionsPosTab(tabId) { const tab = tabId || "live"; state.posTab = tab; document.querySelectorAll(".opt-pos-tab").forEach(function (btn) { const on = btn.getAttribute("data-opt-pos-tab") === tab; btn.classList.toggle("active", on); btn.setAttribute("aria-selected", on ? "true" : "false"); }); document.querySelectorAll("[data-opt-pos-pane]").forEach(function (pane) { const on = pane.getAttribute("data-opt-pos-pane") === tab; pane.classList.toggle("is-active", on); pane.hidden = !on; }); if (tab === "live" && window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) { OptionsExpiryCountdown.ensureTimer(); } if (tab === "pending") { refreshPendingOrders(); startPendingOrdersPoll(); } } function bindOptionsPosTabs() { document.querySelectorAll(".opt-pos-tab").forEach(function (btn) { btn.addEventListener("click", function () { setOptionsPosTab(btn.getAttribute("data-opt-pos-tab")); }); }); setOptionsPosTab(state.posTab); } function resolvePositionsList(d) { const now = Date.now(); const list = (d && d.ok && d.positions) ? d.positions : []; if (d && d.ok) { if (list.length) { lastGoodPositions = list; lastGoodPositionsAt = now; return list; } lastGoodPositions = null; lastGoodPositionsAt = 0; return list; } if (lastGoodPositions && lastGoodPositions.length && now - lastGoodPositionsAt < POSITIONS_STALE_MS) { return lastGoodPositions; } return []; } function paintPositions(list) { const wrap = document.getElementById("opt-pos-cards"); const empty = document.getElementById("opt-pos-empty"); const livePane = document.getElementById("opt-pos-live"); if (!wrap) return; const active = document.activeElement; // 正在输入目标指数:先落到草稿,本轮不重绘整卡,避免数字往回退 if (active && active.classList && active.classList.contains("opt-pos-target-input")) { const focusInst = active.getAttribute("data-inst") || ""; if (focusInst) { state.targetDraftByInst[focusInst] = String(active.value || ""); } return; } // 正在输入翻倍倍数:同样跳过重绘,避免被默认 1 冲掉 if (active && active.classList && active.classList.contains("opt-pos-profit-exit-mult")) { const focusInst = active.getAttribute("data-inst") || ""; if (focusInst) { state.profitExitDraftByInst[focusInst] = String(active.value || ""); } return; } // 未聚焦时也同步可见输入,防止漏掉 input 事件 wrap.querySelectorAll(".opt-pos-target-input").forEach(function (inp) { const id = inp.getAttribute("data-inst") || ""; if (!id) return; const v = String(inp.value || ""); if (v.trim() === "") delete state.targetDraftByInst[id]; else state.targetDraftByInst[id] = v; }); wrap.querySelectorAll(".opt-pos-profit-exit-mult").forEach(function (inp) { const id = inp.getAttribute("data-inst") || ""; if (!id) return; const v = String(inp.value || ""); if (v.trim() === "") delete state.profitExitDraftByInst[id]; else state.profitExitDraftByInst[id] = v; }); wrap.innerHTML = ""; if (!list.length) { if (empty) empty.style.display = ""; state.expandedPosInst = null; if (livePane) livePane.classList.remove("options-pos-live-pane--accordion"); return; } if (empty) empty.style.display = "none"; const multi = list.length >= 2; wrap.classList.toggle("opt-pos-cards--accordion", multi); if (livePane) livePane.classList.toggle("options-pos-live-pane--accordion", multi); if (multi) { const ids = list.map(function (p) { return p.inst_id; }); if (state.expandedPosInst && ids.indexOf(state.expandedPosInst) < 0) { state.expandedPosInst = null; } list.forEach(function (p) { const div = document.createElement("div"); div.innerHTML = renderPositionAccordionItem(p, p.inst_id === state.expandedPosInst); wrap.appendChild(div.firstChild); }); } else { state.expandedPosInst = null; list.forEach(function (p) { const div = document.createElement("div"); div.innerHTML = renderPositionCard(p); wrap.appendChild(div.firstChild); }); } bindPositionActions(wrap); if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) { OptionsExpiryCountdown.ensureTimer(); } } async function refreshPositions() { const seq = ++positionsRefreshSeq; const d = await apiJson("/api/options/positions"); if (seq !== positionsRefreshSeq) return; const list = resolvePositionsList(d); paintPositions(list); const fromPos = list.reduce(function (targets, p) { if (!p) return targets; if (p.target_index != null) { targets.push({ id: p.target_monitor_id, inst_id: p.inst_id, opt_type: p.opt_type, target_index: p.target_index, }); } const hedgeTarget = p.hedge_plan_target; if (hedgeTarget && (hedgeTarget.target_index != null || hedgeTarget.profit_rr != null)) { targets.push({ inst_id: p.inst_id, opt_type: p.opt_type || hedgeTarget.opt_type, target_index: hedgeTarget.target_index, profit_rr: hedgeTarget.profit_rr, plan_id: hedgeTarget.plan_id, managed_by: hedgeTarget.managed_by, exit_mode: hedgeTarget.exit_mode, }); } return targets; }, []); if (fromPos.length) { paintTargetMonitors(fromPos); } else { const t = await apiJson("/api/options/targets"); if (seq !== positionsRefreshSeq) return; paintTargetMonitors((t && t.ok && t.targets) ? t.targets : []); } } function statsPnlUnit(d) { const u = String((d && d.pnl_unit) || "").trim().toUpperCase(); if (u === "U" || u === "USDT") return "U"; if (u === "ETH" || u === "BTC") return u; return "USDC"; } function paintPnlStat(el, value, unit) { if (!el) return; if (value == null || value === "" || Number.isNaN(Number(value))) { el.textContent = "—"; el.classList.remove("pos-pnl-profit", "pos-pnl-loss"); return; } const n = Number(value); const label = unit || "USDC"; const decimals = label === "ETH" || label === "BTC" ? 6 : 2; el.textContent = (n > 0 ? "+" : "") + fmt(n, decimals) + " " + label; el.classList.toggle("pos-pnl-profit", n > 0); el.classList.toggle("pos-pnl-loss", n < 0); } async function refreshStats() { const d = await apiJson("/api/options/stats"); const winEl = document.getElementById("opt-stats-winrate"); const plrEl = document.getElementById("opt-stats-plr"); const closedEl = document.getElementById("opt-stats-closed"); const profitEl = document.getElementById("opt-stats-profit"); const lossEl = document.getElementById("opt-stats-loss"); const avgHoldEl = document.getElementById("opt-stats-avg-hold"); const winHoldEl = document.getElementById("opt-stats-win-hold"); const lossHoldEl = document.getElementById("opt-stats-loss-hold"); const openHoldEl = document.getElementById("opt-stats-open-hold"); const totalPnlEl = document.getElementById("opt-stats-total-pnl"); const netRealizedEl = document.getElementById("opt-stats-net-realized"); const openFloatEl = document.getElementById("opt-stats-open-float"); const statEls = [winEl, plrEl, closedEl, profitEl, lossEl, avgHoldEl, winHoldEl, lossHoldEl, openHoldEl]; if (!d.ok) { statEls.forEach(function (el) { if (el) el.textContent = "—"; }); paintPnlStat(totalPnlEl, null); paintPnlStat(netRealizedEl, null); paintPnlStat(openFloatEl, null); paintStatsCharts(null); return; } const unit = statsPnlUnit(d); paintPnlStat(totalPnlEl, d.total_pnl, unit); paintPnlStat(netRealizedEl, d.net_realized_pnl, unit); paintPnlStat(openFloatEl, d.open_float_pnl, unit); if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%"; if (plrEl) { plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—"; } if (closedEl) closedEl.textContent = String(d.total_closed || 0); if (profitEl) { profitEl.textContent = d.avg_win != null && d.avg_win > 0 ? fmt(d.avg_win, 2) + " " + unit : (d.win_count ? "0 " + unit : "—"); } if (lossEl) { lossEl.textContent = d.avg_loss != null && d.avg_loss > 0 ? fmt(d.avg_loss, 2) + " " + unit : (d.loss_count ? "0 " + unit : "—"); } if (avgHoldEl) avgHoldEl.textContent = fmtDuration(d.avg_hold_sec); if (winHoldEl) winHoldEl.textContent = fmtDuration(d.avg_win_hold_sec); if (lossHoldEl) lossHoldEl.textContent = fmtDuration(d.avg_loss_hold_sec); if (openHoldEl) { const cnt = Number(d.open_count) || 0; if (!cnt) { openHoldEl.textContent = "0 笔"; } else { openHoldEl.textContent = cnt + " 笔 · " + fmtDuration(d.avg_open_hold_sec); } } paintStatsCharts(d); } function fmtDuration(sec) { if (sec == null || sec === "" || Number.isNaN(Number(sec))) return "—"; let s = Math.max(0, Math.round(Number(sec))); if (s < 60) return s + "秒"; const m = Math.floor(s / 60); if (m < 60) { const rs = s % 60; return rs ? m + "分" + rs + "秒" : m + "分"; } const h = Math.floor(m / 60); const rm = m % 60; if (h < 24) return rm ? h + "时" + rm + "分" : h + "时"; const d = Math.floor(h / 24); const rh = h % 24; return rh ? d + "天" + rh + "时" : d + "天"; } function setBarFill(el, pct) { if (!el) return; const n = Math.max(0, Math.min(100, Number(pct) || 0)); el.style.width = n + "%"; } function paintStatsCharts(d) { const ring = document.getElementById("opt-stats-ring"); const ringLabel = document.getElementById("opt-stats-ring-label"); const profitBar = document.getElementById("opt-stats-bar-profit"); const lossBar = document.getElementById("opt-stats-bar-loss"); const profitBarLabel = document.getElementById("opt-stats-bar-profit-label"); const lossBarLabel = document.getElementById("opt-stats-bar-loss-label"); const winHoldBar = document.getElementById("opt-stats-bar-win-hold"); const lossHoldBar = document.getElementById("opt-stats-bar-loss-hold"); const winHoldBarLabel = document.getElementById("opt-stats-win-hold-label"); const lossHoldBarLabel = document.getElementById("opt-stats-loss-hold-label"); if (!d || !d.ok) { if (ring) ring.style.setProperty("--win-pct", "0"); if (ringLabel) ringLabel.textContent = "—"; [profitBar, lossBar, winHoldBar, lossHoldBar].forEach(function (el) { setBarFill(el, 0); }); [profitBarLabel, lossBarLabel, winHoldBarLabel, lossHoldBarLabel].forEach(function (el) { if (el) el.textContent = "—"; }); return; } const winRate = d.total_closed ? Number(d.win_rate) || 0 : 0; if (ring) ring.style.setProperty("--win-pct", String(winRate)); if (ringLabel) ringLabel.textContent = d.total_closed ? winRate.toFixed(0) + "%" : "0%"; const profit = Math.max(0, Number(d.avg_win) || 0); const loss = Math.max(0, Number(d.avg_loss) || 0); const unit = statsPnlUnit(d); const pnlTotal = profit + loss; if (pnlTotal > 0) { setBarFill(profitBar, (profit / pnlTotal) * 100); setBarFill(lossBar, (loss / pnlTotal) * 100); if (profitBarLabel) profitBarLabel.textContent = fmt(profit, 2) + " " + unit; if (lossBarLabel) lossBarLabel.textContent = fmt(loss, 2) + " " + unit; } else { setBarFill(profitBar, 0); setBarFill(lossBar, 0); if (profitBarLabel) profitBarLabel.textContent = d.win_count ? "0 " + unit : "—"; if (lossBarLabel) lossBarLabel.textContent = d.loss_count ? "0 " + unit : "—"; } const winHold = Number(d.avg_win_hold_sec) || 0; const lossHold = Number(d.avg_loss_hold_sec) || 0; const holdMax = Math.max(winHold, lossHold); if (holdMax > 0) { setBarFill(winHoldBar, (winHold / holdMax) * 100); setBarFill(lossHoldBar, (lossHold / holdMax) * 100); if (winHoldBarLabel) winHoldBarLabel.textContent = fmtDuration(d.avg_win_hold_sec); if (lossHoldBarLabel) lossHoldBarLabel.textContent = fmtDuration(d.avg_loss_hold_sec); } else { setBarFill(winHoldBar, 0); setBarFill(lossHoldBar, 0); if (winHoldBarLabel) winHoldBarLabel.textContent = "—"; if (lossHoldBarLabel) lossHoldBarLabel.textContent = "—"; } } async function deleteHistoryRow(key, status, instId, closedAt) { const warn = status === "open" ? "该记录仍为持仓中,仅从列表隐藏,不影响交易所持仓.确认删除?" : "确认从列表隐藏该条历史记录?(期权复盘页也会同步隐藏)"; if (!confirm(warn)) return; const body = {}; if (instId) body.inst_id = instId; if (closedAt) body.closed_at = closedAt; const r = await apiJson("/api/options/history/" + encodeURIComponent(key), { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) { alert(r.msg || "删除失败"); return; } refreshAllPositions(); } function optHistoryStatus(h) { if (h.status_label) return h.status_label; if (h.status === "open") return "持仓中"; if (h.status !== "closed") return "持仓中"; return "已平"; } function optHistoryStatusHtml(h) { const s = optHistoryStatus(h); let cls = "opt-hist-status"; if (s === "已平") cls += " opt-hist-status--closed"; else if (s === "到期" || s === "强平") cls += " opt-hist-status--expired"; else cls += " opt-hist-status--open"; return '' + s + ""; } async function refreshHistory() { const d = await apiJson("/api/options/history"); const tbody = document.getElementById("opt-history-tbody"); tbody.innerHTML = ""; const list = (d.ok && d.history) || []; if (!list.length) { tbody.innerHTML = '暂无历史记录'; return; } list.forEach(function (h) { const tr = document.createElement("tr"); const ccy = posPremiumCcy(h); const premTxt = h.premium_paid != null && !Number.isNaN(Number(h.premium_paid)) ? fmtPremiumAmt(h.premium_paid, ccy) + (ccy !== "USDC" ? " " + ccy : "") : "—"; const isOpen = h.status === "open"; const pnl = isOpen ? null : h.realized_pnl; let pnlTxt = "—"; let pnlCls = ""; if (pnl != null && !Number.isNaN(Number(pnl))) { const n = Number(pnl); pnlCls = n > 0 ? "pos-pnl-profit" : n < 0 ? "pos-pnl-loss" : ""; if (ccy === "ETH" || ccy === "BTC") { const sign = n > 0 ? "+" : n < 0 ? "-" : ""; pnlTxt = sign + fmtPremiumAmt(Math.abs(n), ccy) + " " + ccy; } else { pnlTxt = (n > 0 ? "+" : "") + fmt(n, 2); } } const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19); const histKey = h.history_key || ""; tr.innerHTML = '' + (h.inst_id || "") + "" + "" + fmt(h.sheets, 0) + "" + "" + premTxt + "" + "" + optHistoryStatusHtml(h) + "" + '' + pnlTxt + "" + '' + timeTxt + "" + ''; tbody.appendChild(tr); }); tbody.querySelectorAll(".opt-history-del").forEach(function (btn) { btn.addEventListener("click", function () { deleteHistoryRow( btn.getAttribute("data-key"), btn.getAttribute("data-status"), btn.getAttribute("data-inst"), btn.getAttribute("data-closed") ); }); }); } function refreshAllPositions() { if (refreshAllTimer) clearTimeout(refreshAllTimer); refreshAllTimer = setTimeout(function () { refreshAllTimer = null; refreshPositions(); refreshStats(); refreshHistory(); }, 120); } function onExpiryChange() { resetMoneyFilterToAll(); state.strikeExpandAll = false; const expandCb = document.getElementById("opt-strike-expand-all"); if (expandCb) expandCb.checked = false; renderStrikes(); } function bootOptionsPanel() { applyBudgetBuffer(state.budgetBuffer); updateSizeInputs(); void (async function syncLiveCompoundFlag() { await refreshBridgeSellUi(false); })(); syncMoneyFilterButtons(); syncChainViewUI(); updateUnderlyingLabel(); refreshPendingOrders(); startPendingOrdersPoll(); const hasCache = chainHasExpiries(panelCache.chain) && panelCache.underlying === state.underlying && panelCache.optType === state.optType; if (hasCache) { state.chain = panelCache.chain; renderExpiries(); renderStrikes(); refreshAllPositions(); // 后台静默刷新,避免缓存过期后到期日变空 loadChain({ soft: true }); return; } requestAnimationFrame(function () { loadChain(); refreshAllPositions(); }); } 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"); updateRetrySellCoinUi(lastBalancesPayload); loadChain(); }); }); document.querySelectorAll(".opt-view-btn").forEach(function (btn) { btn.addEventListener("click", function () { const view = btn.getAttribute("data-view") || "list"; if (view === state.chainView) return; state.chainView = view; if (view === "t") { state.strikeExpandAll = false; const expandCb = document.getElementById("opt-strike-expand-all"); if (expandCb) expandCb.checked = false; } syncChainViewUI(); renderStrikes(); }); }); const expandAllCb = document.getElementById("opt-strike-expand-all"); if (expandAllCb) { expandAllCb.addEventListener("change", function () { state.strikeExpandAll = !!expandAllCb.checked; // 实值/虚值筛选下档位本来就少,展开几乎不变;勾选时切回「全部」才有意义 if (state.strikeExpandAll && state.moneyFilter !== "all") { state.moneyFilter = "all"; syncMoneyFilterButtons(); } renderStrikes(); }); } 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"); resetMoneyFilterToAll(); renderExpiryOptions(true); renderStrikes(); }); }); document.querySelectorAll(".opt-money-btn").forEach(function (btn) { btn.addEventListener("click", function () { state.moneyFilter = btn.getAttribute("data-money") || "all"; syncMoneyFilterButtons(); renderStrikes(); }); }); document.getElementById("opt-exp-select").addEventListener("change", onExpiryChange); document.getElementById("opt-load-chain").addEventListener("click", loadChain); document.getElementById("opt-refresh-positions").addEventListener("click", refreshAllPositions); const retrySellBtn = document.getElementById("opt-retry-sell-coin"); if (retrySellBtn) retrySellBtn.addEventListener("click", retrySellBackCoin); document.getElementById("opt-open-btn").addEventListener("click", openPosition); const pendingRefreshBtn = document.getElementById("opt-pending-refresh"); if (pendingRefreshBtn) { pendingRefreshBtn.addEventListener("click", function () { refreshPendingOrders(); }); } bindOptionsPosTabs(); bindPosTransferControls(); hardenOrderAutofill(); (function bindProfitExitOpenControls() { const peCb = document.getElementById("opt-profit-exit-enabled"); const peMult = document.getElementById("opt-profit-exit-mult"); if (!peCb || !peMult) return; // 倍数始终可手输;勾选只决定开仓是否带上翻倍出场 peMult.disabled = false; peMult.removeAttribute("readonly"); peCb.addEventListener("change", function () { if (peCb.checked && (!peMult.value || Number(peMult.value) <= 0)) peMult.value = "1"; }); })(); document.querySelectorAll('input[name="opt-size-mode"]').forEach(function (r) { r.addEventListener("change", function () { updateSizeInputs(); if (state.selectedInst) selectContract(state.selectedInst, null, true); }); }); function bindOrderDialogChrome() { const host = orderPanelHost(); const closeBtn = document.getElementById("opt-order-close-btn"); const cancelBtn = document.getElementById("opt-order-cancel-btn"); if (closeBtn) closeBtn.addEventListener("click", closeOrderDialog); if (cancelBtn) cancelBtn.addEventListener("click", closeOrderDialog); if (host) { host.addEventListener("click", function (ev) { if (ev.target === host) closeOrderDialog(); }); } document.addEventListener("keydown", function (ev) { if (ev.key !== "Escape") return; const h = orderPanelHost(); if (h && !h.hidden) closeOrderDialog(); }); } bindOrderDialogChrome(); ["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) { const el = document.getElementById(id); if (!el) return; el.addEventListener("change", function () { if (id === "opt-target-idx") { updateEstimatedProfit(); return; } if (state.selectedInst) selectContract(state.selectedInst, null, true); }); if (id === "opt-target-idx") { el.addEventListener("input", updateEstimatedProfit); } }); bootOptionsPanel(); window.OptionsPanelLive = { refreshSoft: function () { refreshAllPositions(); }, refreshChain: loadChain, }; })();