diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 0c85550..90e3fdb 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3124,6 +3124,7 @@ html[data-theme="light"] .opt-be-dist-down { .opt-uly-btn.active, .opt-type-btn.active, .opt-money-btn.active, +.opt-view-btn.active, .opt-pos-tab.active { border-color: #5b8cff; color: #cfe0ff; @@ -3142,6 +3143,98 @@ html[data-theme="light"] .opt-be-dist-down { .opt-strike-row.opt-row-selected td:first-child { box-shadow: inset 3px 0 0 #5b8cff; } +.opt-chain-view-group { + display: inline-flex; + gap: 4px; +} +.opt-type-btn-group { + display: inline-flex; + gap: 4px; +} +.opt-strike-expand-label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.78rem; + color: var(--text-soft, #9aa4b2); + white-space: nowrap; + cursor: pointer; + user-select: none; +} +.opt-strike-expand-label input { + margin: 0; +} +.options-strike-table-wrap--t { + max-height: 380px; +} +.options-strike-table--t thead th { + text-align: center; +} +.options-strike-table--t .opt-t-head-call { + text-align: center; + color: #8ec5ff; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} +.options-strike-table--t .opt-t-head-mid { + text-align: center; + color: #ffd48a; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} +.options-strike-table--t .opt-t-head-put { + text-align: center; + color: #ff9f9f; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} +.options-strike-table--t .opt-t-strike { + text-align: center; + font-variant-numeric: tabular-nums; + background: rgba(255, 255, 255, 0.03); +} +.options-strike-table--t .opt-t-mid { + text-align: center; + background: rgba(255, 212, 138, 0.04); +} +.options-strike-table--t .opt-t-straddle-prem { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} +.options-strike-table--t .opt-t-straddle-band { + font-variant-numeric: tabular-nums; + white-space: nowrap; + font-size: 0.72rem; + color: var(--text-soft, #9aa4b2); +} +.options-strike-table--t .opt-t-call, +.options-strike-table--t .opt-t-put { + text-align: center; +} +.options-strike-table--t .opt-strike-row-atm td { + background: rgba(255, 212, 138, 0.08); +} +.options-strike-table--t .opt-strike-row-atm .opt-t-strike strong { + color: #ffd48a; +} +.options-strike-table--t .opt-strike-row.opt-row-selected td { + background: rgba(74, 124, 255, 0.12); +} +.opt-strike-hint-row td { + text-align: center; + font-size: 0.72rem; + padding: 8px 4px; + border-bottom: none; +} +html[data-theme="light"] .options-strike-table--t .opt-t-head-call { + color: #1565c0; +} +html[data-theme="light"] .options-strike-table--t .opt-t-head-mid { + color: #e65100; +} +html[data-theme="light"] .options-strike-table--t .opt-t-head-put { + color: #c62828; +} +html[data-theme="light"] .options-strike-table--t .opt-strike-row-atm td { + background: rgba(255, 152, 0, 0.08); +} .opt-order-inline-row td { padding: 14px 16px !important; background: rgba(74, 124, 255, 0.07); diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 2d7d3c4..bc7efd3 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -12,6 +12,8 @@ underlying: root.dataset.defaultUnderly || "ETH", optType: "C", moneyFilter: "all", + chainView: "list", + strikeExpandAll: false, chain: panelCache.chain || null, selectedInst: null, orderQuote: null, @@ -84,7 +86,10 @@ function placeOrderPanelAfter(instId) { const panel = orderPanel(); if (!panel || !instId) return; - const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]'); + 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) + '"]'); if (!row) return; document.querySelectorAll(".opt-strike-row").forEach(function (r) { r.classList.toggle("opt-row-selected", r === row); @@ -100,7 +105,7 @@ const tr = document.createElement("tr"); tr.className = "opt-order-inline-row"; const td = document.createElement("td"); - td.colSpan = 8; + td.colSpan = strikeTableColspan(); td.appendChild(panel); tr.appendChild(td); row.after(tr); @@ -135,6 +140,31 @@ return url; } + function strikeTableColspan() { + return state.chainView === "t" ? 9 : 8; + } + + 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 = !isT; + 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); + if (headT) headT.classList.toggle("hidden", !isT); + if (headTCols) headTCols.classList.toggle("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; @@ -149,11 +179,105 @@ } function countContractsForType(contracts) { + if (state.chainView === "t") { + return countStraddleStrikes(contracts); + } return (contracts || []).filter(function (c) { return c.opt_type === state.optType; }).length; } + function countStraddleStrikes(contracts) { + const strikes = new Set(); + (contracts || []).forEach(function (c) { + if (c.strike != null) strikes.add(String(c.strike)); + }); + return strikes.size; + } + + 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) { + return matchesStrikeRowFilter(row.strike, indexPx, atmStrike); + }); + } + + 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); }); + if (idx < 0) return rows.slice(0, Math.min(rows.length, 11)); + const start = Math.max(0, idx - 5); + const end = Math.min(rows.length, idx + 6); + return rows.slice(start, end); + } + + 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) { + const per = combinedAsk; + if (strike == null || per == null) return "—"; + const k = Number(strike); + const d = Number(per); + if (!Number.isFinite(k) || !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); @@ -417,14 +541,38 @@ 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(); tbody.innerHTML = ""; if (!expMs || !state.chain) { parkOrderPanel(); - tbody.innerHTML = '