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