Files
crypto_monitor/lib/common/static/options_panel.js
T

2517 lines
98 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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: {},
};
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 = '<div class="muted opt-pending-empty">暂无未成交委托</div>';
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 (
'<div class="opt-pending-item" data-ord="' + (o.ord_id || "") + '" data-inst="' + (o.inst_id || "") + '">' +
'<div class="opt-pending-item-top">' +
'<span class="opt-pending-side ' + sideCls + '">' + kind + " · " + (o.side_label || side || "—") + "</span>" +
'<button type="button" class="btn-secondary opt-pending-cancel" data-ord="' + (o.ord_id || "") +
'" data-inst="' + (o.inst_id || "") + '">撤销</button>' +
"</div>" +
'<div class="opt-pending-inst" title="' + (o.inst_id || "") + '">' + (o.inst_id || "—") + "</div>" +
'<div class="opt-pending-meta">价 ' + 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 +
"</div></div>"
);
}).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 = '<div class="muted opt-pending-empty">' + (d.msg || "获取委托失败") + "</div>";
return;
}
paintPendingOrders(d.orders || [], d.pending_ttl_seconds);
} catch (e) {
host.innerHTML = '<div class="muted opt-pending-empty">获取委托失败</div>';
}
}
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();
} finally {
if (btn) btn.disabled = false;
}
}
function currentSizeMode() {
const el = document.querySelector('input[name="opt-size-mode"]:checked');
return el ? el.value : "sheets";
}
function compoundFullEnabled() {
return !!(root && String(root.dataset.compoundFullEnabled || "1") === "1");
}
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;
const radio = budgetWrap.querySelector('input[name="opt-size-mode"]');
if (radio) radio.disabled = compoundOn;
}
if (compoundWrap) {
compoundWrap.hidden = !compoundOn;
const radio = compoundWrap.querySelector('input[name="opt-size-mode"]');
if (radio) radio.disabled = !compoundOn;
}
if (compoundOn && mode === "budget_full") {
const compoundRadio = document.querySelector('input[name="opt-size-mode"][value="compound_full"]');
if (compoundRadio) compoundRadio.checked = true;
} else if (!compoundOn && mode === "compound_full") {
const sheetsRadio = document.querySelector('input[name="opt-size-mode"][value="sheets"]');
if (sheetsRadio) 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"]');
chip.classList.toggle("is-selected", !!(radio && radio.checked));
chip.classList.toggle("active", !!(radio && radio.checked));
});
}
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) {
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 += "&eth_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 (
'<td colspan="' +
cols +
'" class="muted opt-strike-hint">默认显示平值 + 实值3档 + 虚值3档 · 勾选「展开全部」查看该到期全部行权价</td>'
);
}
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 '<span class="muted">不可双买</span>';
return fmtUsdc(per) + " USDC";
}
function pickBtnHtml(instId) {
if (!instId) return "—";
return '<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + instId + '">选择</button>';
}
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 '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
}
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 '<span class="' + cls + '" title="持仓来源">' + sourceText(p) + "</span>";
}
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 = '<option value="">选择到期日</option>';
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 || "可回收需≥2×权利金并持续2分钟");
}
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 fmtClosePreview(preview, premiumPaid) {
if (!preview || preview.total_received == null) return "—";
const recvTxt = fmtUsdc(preview.total_received);
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 '<span class="opt-close-value' + cls + '">' + recvTxt + " USDC</span>";
}
function fmtClosePreviewText(preview) {
if (!preview || preview.total_received == null) return "—";
let text = fmt(preview.total_received, 4) + " USDC";
if (preview.covered_sheets != null) {
text += " · 覆盖 " + preview.covered_sheets + "张";
}
if (preview.uncovered_sheets > 0) {
text += " · 缺 " + preview.uncovered_sheets + "张";
}
return text;
}
function fmtPreviewLevels(preview) {
const levels = (preview && preview.levels) || [];
if (!levels.length) return "暂无可用买盘深度";
return levels.map(function (x) {
return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + fmt(x.received, 4) + " USDC";
}).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) {
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
const prem = Number(totalPremium);
if (value == null || !Number.isFinite(prem)) return null;
return Math.round((value - prem) * 100) / 100;
}
/** 盈亏比 = 盈利金额 / 本合约权利金(目标位仅作到期实值参考). */
function estimateProfitRr(profit, totalPremium) {
const pnl = Number(profit);
const prem = Number(totalPremium);
if (!Number.isFinite(pnl) || !Number.isFinite(prem) || prem <= 0) return null;
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 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;
}
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) + "×";
}
/** 链上展示:指数 ÷ 卖一(每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;
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);
const rr = estimateProfitRr(profit, premium);
if (value == null || Number.isNaN(value)) {
valueEl.textContent = "—";
} else {
valueEl.textContent = fmtUsdc(value) + " USDC";
}
if (profit == null || Number.isNaN(profit)) {
profitEl.textContent = "—";
profitEl.className = "v";
} else {
profitEl.textContent = 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);
const sign = n > 0 ? "+" : "";
return sign + 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";
if (n < 0) return "opt-be-dist-down";
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 = '<tr><td colspan="' + cols + '" class="muted">请选择到期日</td></tr>';
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 = '<tr><td colspan="' + cols + '" class="muted">该到期日暂无' + suffix + "合约" + liqTip + "</td></tr>";
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 =
"<td>" + c.strike + "</td>" +
"<td>" + moneynessBadge(c) + "</td>" +
"<td><code>" + c.inst_id + "</code></td>" +
"<td class=\"opt-px-sz\">" + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated) + "</td>" +
'<td class="opt-chain-lev">' + fmtChainLeverage(chainLev) + "</td>" +
"<td class=\"opt-px-sz\">" + fmtPxSz(c.bid, c.bid_sz) + "</td>" +
"<td>" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "</td>" +
'<td class="' + distBeClass(c.dist_expiry_be) + '">' + fmtDist(c.dist_expiry_be) + "</td>" +
'<td class="opt-row-actions">' +
pickBtnHtml(c.inst_id) +
"</td>";
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 = '<tr><td colspan="' + cols + '" class="muted">请选择到期日</td></tr>';
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 = '<tr><td colspan="' + cols + '" class="muted">该到期日暂无' + suffix + "行权价</td></tr>";
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 =
'<td class="opt-t-call opt-px-sz">' + (call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated) : "—") + "</td>" +
'<td class="opt-t-call">' + (call ? moneynessBadge(call) : "—") + "</td>" +
'<td class="opt-t-call opt-row-actions">' + pickBtnHtml(call && call.inst_id) + "</td>" +
'<td class="opt-t-mid opt-t-strike"><strong>' + row.strike + "</strong></td>" +
'<td class="opt-t-mid opt-t-straddle-prem">' + formatStraddlePremiumCell(call && call.ask, put && put.ask) + "</td>" +
'<td class="opt-t-mid opt-t-straddle-band">' + formatStraddleBand(row.strike, combined) + "</td>" +
'<td class="opt-t-put">' + (put ? moneynessBadge(put) : "—") + "</td>" +
'<td class="opt-t-put opt-px-sz">' + (put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated) : "—") + "</td>" +
'<td class="opt-t-put opt-row-actions">' + pickBtnHtml(put && put.inst_id) + "</td>";
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) {
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();
document.getElementById("opt-order-premium").textContent =
canOpen && sz.total_premium != null ? 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;
openBtn.textContent = canOpen ? "限价买入 @ 卖一" : "暂无卖一深度,无法开仓";
}
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 {
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 =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">加载期权链…</td></tr>';
}
}
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 =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">' +
(lastMsg || "暂无到期日,请点「刷新链」") +
"</td></tr>";
}
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();
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 时保留 selectedInstrenderStrikes 会先 park 再按 prevSelected 静默重挂下单面板
renderStrikes();
} catch (e) {
if (seq !== chainLoadSeq || soft) return;
setExpirySelectStatus("选择到期日");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
tbody.innerHTML =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">加载失败: ' +
String((e && e.message) || e) +
"</td></tr>";
}
} 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 {
const mode = currentSizeMode();
const body = {
inst_id: state.selectedInst,
mode: mode,
signal_note: document.getElementById("opt-signal-note").value || "",
};
if (mode === "eth_amount") {
body.eth_amount = parseFloat(document.getElementById("opt-eth-amount").value);
} else if (mode === "sheets") {
body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10);
}
const 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;
btn.disabled = !(latest && latest.ok && latest.can_open && !(latest.sizing && latest.sizing.ok === false));
btn.textContent = (latest && latest.can_open) ? "限价买入 @ 卖一" : "暂无卖一深度,无法开仓";
}
}
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 premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : 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);
return (
'<div class="pos-card-head">' +
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span>" +
sourceBadgeHtml(p) +
"</div>" +
'<div class="pos-head-actions">' +
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '" data-sheets="' + closeSheets + '">买一平仓</button>' +
"</div></div>" +
'<div class="pos-meta">' +
'<span class="pos-meta-item">持仓来源: ' + sourceText(p) + "</span>" +
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</span>" +
'<span class="pos-meta-item">张数: ' + fmt(p.pos, 0) + " · 币量 " + fmt(p.eth_amount, 4) + "</span>" +
(expAttr
? '<span class="pos-meta-item">到期倒计时: <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
: "") +
"</div>" +
'<div class="pos-grid">' +
'<div class="pos-cell"><span class="pos-label">权利金</span><span class="pos-value">' + premTxt + " USDC</span></div>" +
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + avgTxt + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + markTxt + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">指数价</span><span class="pos-value">' + fmt(p.idx_px, 0) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
(closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' +
(closePreview.bid_invalid
? '<span class="muted">暂无有效买盘</span>'
: fmtClosePreview(closePreview, p.premium_paid)) + "</span></div>" +
"</div>" +
(function () {
const hint = closeGateHint(closePreview);
return hint ? '<div class="muted opt-bid-invalid-hint">' + hint + "</div>" : "";
})() +
renderTargetDelegateRow(p) +
renderProfitExitRow(p)
);
}
function renderProfitExitRow(p) {
const inst = p.inst_id || "";
if (p.hedge_plan_target) {
return "";
}
const enabled = !!p.profit_exit_enabled;
const mult = p.profit_exit_mult != null && Number(p.profit_exit_mult) > 0
? Number(p.profit_exit_mult)
: 1;
const state = String(p.profit_exit_state || (enabled ? "active" : "idle"));
const req = p.profit_exit_required_recycle;
let statusTxt = enabled
? ("监控中 · " + fmt(mult, 2) + "倍")
: "未开启";
if (enabled && state === "closing") statusTxt = "平仓挂单中 · " + fmt(mult, 2) + "倍";
return (
'<div class="opt-target-row opt-profit-exit-pos-row" data-inst="' + inst + '">' +
'<span class="opt-target-row-label">翻倍</span>' +
'<label class="opt-profit-exit-toggle"><input type="checkbox" class="opt-pos-profit-exit-enabled" data-inst="' +
inst + '"' + (enabled ? " checked" : "") + "> 开启</label>" +
'<input type="number" class="opt-pos-profit-exit-mult" data-inst="' + inst +
'" min="0.1" step="0.1" value="' + mult + '"' + (enabled ? "" : " disabled") + ">" +
'<button type="button" class="btn-secondary opt-profit-exit-save-btn" data-inst="' +
inst + '">应用</button>' +
'<span class="opt-target-armed">' + statusTxt + "</span>" +
'<span class="muted opt-target-row-hint">' +
(enabled
? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtUsdc(req)) : ""))
: "开启后自选倍数;达标按买一限价平;可随时关闭") +
"</span>" +
"</div>"
);
}
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) {
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid);
const rr = estimateProfitRr(profit, premiumPaid);
if (value == null && profit == null && rr == null) return "";
let html = '<span class="opt-target-est">';
html += '<span class="opt-target-est-item"><span class="k">价值</span><span class="v">' +
(value == null ? "—" : fmtUsdc(value) + " USDC") + "</span></span>";
html += '<span class="opt-target-est-item"><span class="k">预估盈利</span><span class="v ' + pnlCls(profit) + '">' +
(profit == null ? "—" : fmtUsdcSigned(profit)) + "</span></span>";
html += '<span class="opt-target-est-item"><span class="k">盈亏比</span><span class="v ' + pnlCls(rr) + '">' +
(rr == null ? "—" : fmtProfitRr(rr)) + "</span></span>";
html += "</span>";
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 (
'<div class="opt-target-row opt-target-row--managed">' +
'<span class="opt-target-row-label">对冲计划</span>' +
'<span class="opt-target-armed">计划 #' +
hedgeTarget.plan_id +
" · 盈亏比 " +
fmt(rr, 2) +
"</span>" +
'<span class="muted opt-target-row-hint">进行中 · 盈利达总权利金×盈亏比仅平盈利腿;亏损腿按本合约残值平或到期平</span>' +
"</div>"
);
}
if (Number(hedgeTarget.target_index) > 0) {
const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
return (
'<div class="opt-target-row opt-target-row--managed">' +
'<span class="opt-target-row-label">对冲计划</span>' +
'<span class="opt-target-armed">计划 #' +
hedgeTarget.plan_id +
" · " +
side +
" " +
fmt(hedgeTarget.target_index, 1) +
"</span>" +
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控,到位后仅平盈利腿</span>' +
"</div>"
);
}
}
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)
: '<span class="opt-target-est opt-target-est--idle"></span>';
return (
'<div class="opt-target-row" data-inst="' + inst + '"' +
' data-opt-type="' + (p.opt_type || "") + '"' +
' data-strike="' + (p.strike != null ? p.strike : "") + '"' +
' data-eth="' + (ethAmt != null ? ethAmt : "") + '"' +
' data-prem="' + (prem != null ? prem : "") + '"' +
' data-armed-target="' + (armed ? tgt : "") + '">' +
'<span class="opt-target-row-label">委托</span>' +
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="监控目标指数" value="' +
(state.targetDraftByInst[inst] != null ? String(state.targetDraftByInst[inst]) : "") + '">' +
'<button type="button" class="btn-secondary opt-target-set-btn" data-inst="' + inst + '">设定</button>' +
'<button type="button" class="btn-secondary opt-target-cancel-btn" data-inst="' + inst + '"' + (armed ? "" : " disabled") + ">取消</button>" +
(armed
? '<span class="opt-target-armed">目标 ' + fmt(tgt, 1) + "</span>"
: "") +
estHtml +
'<span class="muted opt-target-row-hint">' +
(armed ? "监控中 · 目标位参考 · 到位按买一限价平" : "目标位参考(到期实值估盈亏比) · 到位按买一限价平 · 到期即止损") +
"</span>" +
"</div>"
);
}
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")
);
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 (
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
renderPositionCardInner(p) +
"</div>"
);
}
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 (
'<div class="opt-pos-accordion-item' + (expanded ? " is-expanded" : "") + '" data-inst="' + inst + '">' +
'<button type="button" class="opt-pos-bar" aria-expanded="' + (expanded ? "true" : "false") + '">' +
'<span class="opt-pos-bar-main">' +
'<span class="opt-pos-bar-chevron" aria-hidden="true">▶</span>' +
'<span class="opt-pos-bar-id-group">' +
'<strong class="opt-pos-bar-title" title="' + inst + '">' + inst + "</strong>" +
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span>" +
sourceBadgeHtml(p) +
"</span>" +
'<span class="opt-pos-bar-meta">行权 ' + fmt(p.strike, 0) + " · " + fmt(p.pos, 0) + "张</span>" +
"</span>" +
'<span class="opt-pos-bar-side">' +
(expAttr
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
: "") +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + (net == null ? "—" : fmt(net, 2) + " USDC") + "</span>" +
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span>" +
"</span>" +
"</button>" +
'<div class="opt-pos-accordion-body"' + (expanded ? "" : ' hidden') + '>' +
'<div class="pos-card opt-pos-card opt-pos-card--inline" data-inst="' + inst + '">' +
renderPositionCardInner(p) +
"</div></div></div>"
);
}
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(); });
cb.addEventListener("change", function () {
const row = cb.closest(".opt-profit-exit-pos-row");
const multInp = row && row.querySelector(".opt-pos-profit-exit-mult");
if (multInp) multInp.disabled = !cb.checked;
});
});
container.querySelectorAll(".opt-pos-profit-exit-mult").forEach(function (inp) {
inp.addEventListener("click", function (e) { e.stopPropagation(); });
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 enabled = !!(enabledEl && enabledEl.checked);
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;
}
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 (
'<div class="opt-target-mon-item' + (managed ? " opt-target-mon-item--managed" : "") + '">' +
'<code class="opt-target-mon-inst" title="' + (t.inst_id || "") + '">' + (t.inst_id || "") + "</code>" +
'<span class="opt-target-mon-rule">' + side + " " + fmt(t.target_index, 1) + "</span>" +
(managed
? '<span class="opt-target-mon-managed">对冲计划 #' + (t.plan_id || "") + " · 进行中</span>"
: '<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>') +
"</div>"
);
}).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 msg = [
"按买一限价卖出本轮可平张数?",
"合约: " + inst,
"锁定买一: " + (lv.px != null ? lv.px : "—") + " × " + (lv.sheets != null ? lv.sheets : preview.covered_sheets) + " 张",
"预计收回: " + fmtClosePreviewText(preview),
preview.estimated_pnl != null ? "预估盈亏: " + fmt(preview.estimated_pnl, 4) + " USDC" : "",
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预估收回: " + fmt(r.premium_received, 4) + " USDC";
if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)";
if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason;
alert(okMsg);
} else {
alert(r.msg || "平仓失败");
}
refreshAllPositions();
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
} 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;
}
// 未聚焦时也同步可见输入,防止漏掉 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.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 paintPnlStat(el, value) {
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);
el.textContent = (n > 0 ? "+" : "") + fmt(n, 2) + " USDC";
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;
}
paintPnlStat(totalPnlEl, d.total_pnl);
paintPnlStat(netRealizedEl, d.net_realized_pnl);
paintPnlStat(openFloatEl, d.open_float_pnl);
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) + " USDC" : (d.win_count ? "0 USDC" : "—");
}
if (lossEl) {
lossEl.textContent = d.avg_loss != null && d.avg_loss > 0
? fmt(d.avg_loss, 2) + " USDC" : (d.loss_count ? "0 USDC" : "—");
}
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 pnlTotal = profit + loss;
if (pnlTotal > 0) {
setBarFill(profitBar, (profit / pnlTotal) * 100);
setBarFill(lossBar, (loss / pnlTotal) * 100);
if (profitBarLabel) profitBarLabel.textContent = fmt(profit, 2) + " USDC";
if (lossBarLabel) lossBarLabel.textContent = fmt(loss, 2) + " USDC";
} else {
setBarFill(profitBar, 0);
setBarFill(lossBar, 0);
if (profitBarLabel) profitBarLabel.textContent = d.win_count ? "0 USDC" : "—";
if (lossBarLabel) lossBarLabel.textContent = d.loss_count ? "0 USDC" : "—";
}
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 '<span class="' + cls + '">' + s + "</span>";
}
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 = '<tr><td colspan="7" class="muted">暂无历史记录</td></tr>';
return;
}
list.forEach(function (h) {
const tr = document.createElement("tr");
const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmtUsdc(h.premium_paid) : null);
const isOpen = h.status === "open";
const pnl = isOpen ? null : h.realized_pnl;
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19);
const histKey = h.history_key || "";
tr.innerHTML =
'<td class="opt-hist-inst"><code>' + (h.inst_id || "") + "</code></td>" +
"<td>" + fmt(h.sheets, 0) + "</td>" +
"<td>" + premTxt + "</td>" +
"<td>" + optHistoryStatusHtml(h) + "</td>" +
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
'<td class="opt-hist-time">' + timeTxt + "</td>" +
'<td><button type="button" class="btn-secondary btn-sm opt-history-del" data-key="' +
histKey +
'" data-status="' +
(h.status || "") +
'" data-inst="' +
(h.inst_id || "") +
'" data-closed="' +
(h.closed_at || h.created_at || "") +
'">删除</button></td>';
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();
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");
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);
document.getElementById("opt-open-btn").addEventListener("click", openPosition);
const pendingRefreshBtn = document.getElementById("opt-pending-refresh");
if (pendingRefreshBtn) {
pendingRefreshBtn.addEventListener("click", function () {
refreshPendingOrders();
});
}
bindOptionsPosTabs();
hardenOrderAutofill();
(function bindProfitExitOpenControls() {
const peCb = document.getElementById("opt-profit-exit-enabled");
const peMult = document.getElementById("opt-profit-exit-mult");
if (!peCb || !peMult) return;
function sync() {
peMult.disabled = !peCb.checked;
if (peCb.checked && (!peMult.value || Number(peMult.value) <= 0)) peMult.value = "1";
}
peCb.addEventListener("change", sync);
sync();
})();
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,
};
})();