09cd680e4a
Co-authored-by: Cursor <cursoragent@cursor.com>
679 lines
23 KiB
JavaScript
679 lines
23 KiB
JavaScript
/**
|
||
* OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁.
|
||
*/
|
||
(function () {
|
||
const root = document.getElementById("hedge-plan-root");
|
||
if (!root) return;
|
||
|
||
const state = {
|
||
tab: "perp_options",
|
||
mode: "perp_options",
|
||
underlying: root.getAttribute("data-default-underly") || "ETH",
|
||
moneyFilter: "all",
|
||
chain: null,
|
||
selected: null,
|
||
legA: null,
|
||
legB: null,
|
||
market: null,
|
||
};
|
||
|
||
function $(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
async function apiJson(url, opts) {
|
||
const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
|
||
const data = await res.json().catch(function () {
|
||
return {};
|
||
});
|
||
if (!res.ok) throw new Error(data.msg || res.statusText || "请求失败");
|
||
return data;
|
||
}
|
||
|
||
function fmt(v, d) {
|
||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||
return Number(v).toFixed(d == null ? 2 : d);
|
||
}
|
||
|
||
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) {
|
||
return String(n).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.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).replace(/\.?0+$/, "");
|
||
return s || "0";
|
||
}
|
||
|
||
/** 价格/流动性(张),价格按 tick_sz 对齐交易所精度. */
|
||
function fmtPxSz(px, sz, estimated, tickSz) {
|
||
if (px === null || px === undefined || Number.isNaN(Number(px))) return "—";
|
||
let price = fmtOptionPx(px, tickSz);
|
||
if (price === "—") return "—";
|
||
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 moneynessBadge(c) {
|
||
const m = (c && c.moneyness) || "";
|
||
const label = (c && c.moneyness_label) || "—";
|
||
return '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
|
||
}
|
||
|
||
function matchesMoneyFilter(c) {
|
||
const f = state.moneyFilter || "all";
|
||
if (f === "all") return true;
|
||
const m = (c.moneyness || "").toLowerCase();
|
||
if (f === "itm") return m === "itm" || m === "atm";
|
||
if (f === "otm") return m === "otm";
|
||
return true;
|
||
}
|
||
|
||
function optTypeForDirection(dir) {
|
||
return dir === "short" ? "C" : "P";
|
||
}
|
||
|
||
function syncUnderlyingUI() {
|
||
const uly = state.underlying || "ETH";
|
||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||
const on = b.getAttribute("data-uly") === uly;
|
||
b.classList.toggle("active", on);
|
||
b.setAttribute("aria-pressed", on ? "true" : "false");
|
||
});
|
||
const lab = $("hp-perp-uly-label");
|
||
if (lab) lab.textContent = uly;
|
||
const ooLab = $("hp-oo-uly-label");
|
||
if (ooLab) ooLab.textContent = uly;
|
||
}
|
||
|
||
function syncMoneyUI() {
|
||
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
|
||
const on = b.getAttribute("data-money") === state.moneyFilter;
|
||
b.classList.toggle("active", on);
|
||
});
|
||
}
|
||
|
||
function syncTabUI() {
|
||
const tab = state.tab || "perp_options";
|
||
document.querySelectorAll(".hp-tab").forEach(function (b) {
|
||
const on = b.getAttribute("data-tab") === tab;
|
||
b.classList.toggle("active", on);
|
||
b.setAttribute("aria-selected", on ? "true" : "false");
|
||
});
|
||
["perp_options", "options_options", "history", "stats"].forEach(function (id) {
|
||
const panel = $("hp-tab-" + id);
|
||
if (!panel) return;
|
||
const on = id === tab;
|
||
panel.classList.toggle("hidden", !on);
|
||
if (on) panel.removeAttribute("hidden");
|
||
else panel.setAttribute("hidden", "");
|
||
});
|
||
if (tab === "perp_options" || tab === "options_options") {
|
||
state.mode = tab;
|
||
}
|
||
}
|
||
|
||
function setGateLine(gates) {
|
||
const el = $("hp-gate-line");
|
||
if (!el) return;
|
||
if (!gates) {
|
||
el.textContent = "";
|
||
return;
|
||
}
|
||
const parts = [
|
||
"计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"),
|
||
"测算:" + (gates.can_preview ? "可" : "否"),
|
||
"开仓:" + (gates.can_start ? "可" : "否"),
|
||
];
|
||
if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; "));
|
||
el.textContent = parts.join(" · ");
|
||
const start = $("hp-start-btn");
|
||
if (start) start.disabled = !gates.can_start;
|
||
}
|
||
|
||
function setOptionsBalance(chain) {
|
||
const acct = (chain && chain.options_account) || {};
|
||
const label = (chain && chain.account_label) || acct.label || "期权账户";
|
||
const tag = $("hp-opt-acct-tag");
|
||
if (tag) tag.textContent = label;
|
||
const line =
|
||
label +
|
||
" · 交易 USDC " +
|
||
fmt(acct.trading_usdc, 2) +
|
||
" · 资金 USDC " +
|
||
fmt(acct.funding_usdc, 2);
|
||
const el = $("hp-opt-bal-line");
|
||
if (el) el.textContent = line;
|
||
const oo = $("hp-oo-bal-line");
|
||
if (oo) oo.textContent = line;
|
||
}
|
||
|
||
async function loadGates() {
|
||
try {
|
||
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
|
||
setGateLine(d);
|
||
} catch (e) {
|
||
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
|
||
}
|
||
}
|
||
|
||
async function loadMarket() {
|
||
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
|
||
const d = await apiJson(
|
||
"/api/hedge-plan/market?base=" +
|
||
encodeURIComponent(state.underlying) +
|
||
"&direction=" +
|
||
encodeURIComponent(dir)
|
||
);
|
||
state.market = d;
|
||
setGateLine(d.gates);
|
||
const acctLabel = d.account_label || "合约账户";
|
||
const tag = $("hp-perp-acct-tag");
|
||
if (tag) tag.textContent = acctLabel;
|
||
const q = $("hp-perp-quote");
|
||
if (q) {
|
||
q.innerHTML =
|
||
"<strong>" +
|
||
(d.base || state.underlying) +
|
||
"</strong> · " +
|
||
acctLabel +
|
||
"可用 <strong>" +
|
||
fmt(d.available_usdt, 2) +
|
||
"</strong> U<br/>标记 <strong>" +
|
||
fmt(d.mark, 2) +
|
||
"</strong> · 最新 " +
|
||
fmt(d.last, 2) +
|
||
" · 卖一 " +
|
||
fmt(d.ask, 2) +
|
||
" · 买一 " +
|
||
fmt(d.bid, 2) +
|
||
" · 面值 " +
|
||
fmt(d.contract_size, 4);
|
||
}
|
||
const sz = $("hp-sizing-line");
|
||
if (sz) {
|
||
if (d.full_margin_sizing) {
|
||
const s = d.full_margin_sizing;
|
||
sz.textContent =
|
||
"全仓建议(" +
|
||
acctLabel +
|
||
"):保证金 " +
|
||
fmt(s.margin_capital, 2) +
|
||
"U × " +
|
||
s.leverage +
|
||
"x → 名义 " +
|
||
fmt(s.notional_value, 2) +
|
||
"U · 建议约 " +
|
||
fmt(d.suggest_contracts, 4) +
|
||
" 张";
|
||
} else {
|
||
sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓.";
|
||
}
|
||
}
|
||
const entry = $("hp-entry");
|
||
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
|
||
const contracts = $("hp-contracts");
|
||
if (contracts && d.suggest_contracts != null && !contracts.value) {
|
||
contracts.value = d.suggest_contracts;
|
||
}
|
||
const label = $("hp-opt-type-label");
|
||
if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put";
|
||
}
|
||
|
||
function fillExpSelect(sel, chain) {
|
||
if (!sel) return;
|
||
const prev = sel.value;
|
||
sel.innerHTML = '<option value="">选择到期日</option>';
|
||
(chain.expiries || []).forEach(function (e) {
|
||
const opt = document.createElement("option");
|
||
opt.value = String(e.exp_time);
|
||
const dt = new Date(Number(e.exp_time));
|
||
opt.textContent = dt.toLocaleString();
|
||
sel.appendChild(opt);
|
||
});
|
||
if (prev) sel.value = prev;
|
||
if (!sel.value && chain.expiries && chain.expiries[0]) {
|
||
sel.value = String(chain.expiries[0].exp_time);
|
||
}
|
||
}
|
||
|
||
async function loadChain() {
|
||
const d = await apiJson(
|
||
"/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying)
|
||
);
|
||
state.chain = d;
|
||
const uly = d.underlying || state.underlying;
|
||
const idx = $("hp-index-line");
|
||
if (idx) {
|
||
idx.textContent =
|
||
"指数 " + uly + " " + fmt(d.index_px, 2) + " · " + (d.inst_family || "") + " · 实值含平值 · 虚值=价外";
|
||
}
|
||
const ooIdx = $("hp-oo-index");
|
||
if (ooIdx) ooIdx.textContent = "指数 " + uly + " " + fmt(d.index_px, 2);
|
||
setOptionsBalance(d);
|
||
fillExpSelect($("hp-exp-select"), d);
|
||
fillExpSelect($("hp-oo-exp-select"), d);
|
||
renderListStrikes();
|
||
renderTStrikes();
|
||
if (d.index_px && $("hp-target") && !$("hp-target").value) {
|
||
$("hp-target").value = d.index_px;
|
||
}
|
||
}
|
||
|
||
function currentExp(selectId) {
|
||
const sel = $(selectId);
|
||
const expMs = sel && sel.value;
|
||
if (!expMs || !state.chain) return null;
|
||
return (state.chain.expiries || []).find(function (e) {
|
||
return String(e.exp_time) === String(expMs);
|
||
});
|
||
}
|
||
|
||
function renderListStrikes() {
|
||
const tbody = $("hp-strike-tbody");
|
||
if (!tbody) return;
|
||
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
|
||
const want = optTypeForDirection(dir);
|
||
const exp = currentExp("hp-exp-select");
|
||
const prevInst = state.selected && state.selected.inst_id;
|
||
tbody.innerHTML = "";
|
||
if (!exp) {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
|
||
return;
|
||
}
|
||
const list = (exp.contracts || []).filter(function (c) {
|
||
return String(c.opt_type || "").toUpperCase() === want && matchesMoneyFilter(c);
|
||
});
|
||
if (!list.length) {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="muted">无匹配合约</td></tr>';
|
||
return;
|
||
}
|
||
list.forEach(function (c) {
|
||
const tr = document.createElement("tr");
|
||
tr.className = "opt-strike-row" + (c.moneyness ? " opt-row-" + c.moneyness : "");
|
||
if (prevInst && c.inst_id === prevInst) tr.classList.add("opt-row-selected");
|
||
tr.setAttribute("data-inst", c.inst_id);
|
||
tr.innerHTML =
|
||
"<td>" +
|
||
c.strike +
|
||
"</td><td>" +
|
||
moneynessBadge(c) +
|
||
'</td><td class="opt-px-sz">' +
|
||
fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) +
|
||
'</td><td class="opt-px-sz">' +
|
||
fmtPxSz(c.bid, c.bid_sz, false, c.tick_sz) +
|
||
'</td><td><button type="button" class="btn-secondary hp-pick' +
|
||
(prevInst && c.inst_id === prevInst ? " active" : "") +
|
||
'" data-inst="' +
|
||
c.inst_id +
|
||
'">选用</button></td>';
|
||
tbody.appendChild(tr);
|
||
});
|
||
tbody.querySelectorAll(".hp-pick").forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
const inst = btn.getAttribute("data-inst");
|
||
const c = list.find(function (x) {
|
||
return x.inst_id === inst;
|
||
});
|
||
if (!c) return;
|
||
state.selected = c;
|
||
const el = $("hp-sel-inst");
|
||
if (el) el.textContent = c.inst_id;
|
||
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
|
||
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === inst);
|
||
});
|
||
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
|
||
b.classList.toggle("active", b.getAttribute("data-inst") === inst);
|
||
});
|
||
updatePremiumLine();
|
||
});
|
||
});
|
||
}
|
||
|
||
function updatePremiumLine() {
|
||
const line = $("hp-premium-line");
|
||
if (!line || !state.selected) {
|
||
if (line) line.textContent = "";
|
||
return;
|
||
}
|
||
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
|
||
const ct = Number(state.selected.ct_mult || 0.01);
|
||
const ask = Number(state.selected.ask || 0);
|
||
const prem = ask * sheets * ct;
|
||
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC(期权账户)";
|
||
}
|
||
|
||
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 renderTStrikes() {
|
||
const tbody = $("hp-oo-tbody");
|
||
if (!tbody) return;
|
||
const exp = currentExp("hp-oo-exp-select");
|
||
tbody.innerHTML = "";
|
||
if (!exp) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="muted">请选择到期日</td></tr>';
|
||
return;
|
||
}
|
||
const rows = buildStraddleRows(exp.contracts);
|
||
rows.forEach(function (row) {
|
||
const tr = document.createElement("tr");
|
||
const call = row.call;
|
||
const put = row.put;
|
||
const callAsk = call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated, call.tick_sz) : "—";
|
||
const putAsk = put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated, put.tick_sz) : "—";
|
||
tr.innerHTML =
|
||
'<td class="opt-t-call opt-px-sz">' +
|
||
callAsk +
|
||
'</td><td class="opt-t-call">' +
|
||
(call ? moneynessBadge(call) : "—") +
|
||
"</td><td>" +
|
||
(call
|
||
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="C" data-inst="' +
|
||
call.inst_id +
|
||
'">Call</button>'
|
||
: "—") +
|
||
'</td><td class="opt-t-strike"><strong>' +
|
||
row.strike +
|
||
'</strong></td><td class="opt-t-put">' +
|
||
(put ? moneynessBadge(put) : "—") +
|
||
'</td><td class="opt-t-put opt-px-sz">' +
|
||
putAsk +
|
||
"</td><td>" +
|
||
(put
|
||
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="P" data-inst="' +
|
||
put.inst_id +
|
||
'">Put</button>'
|
||
: "—") +
|
||
"</td>";
|
||
tbody.appendChild(tr);
|
||
});
|
||
tbody.querySelectorAll(".hp-oo-pick").forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
const inst = btn.getAttribute("data-inst");
|
||
const exp2 = currentExp("hp-oo-exp-select");
|
||
const c = (exp2.contracts || []).find(function (x) {
|
||
return x.inst_id === inst;
|
||
});
|
||
if (!c) return;
|
||
if (!state.legA) state.legA = c;
|
||
else if (!state.legB || state.legB.inst_id === state.legA.inst_id) state.legB = c;
|
||
else {
|
||
state.legA = c;
|
||
state.legB = null;
|
||
}
|
||
renderOoLegs();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderOoLegs() {
|
||
const el = $("hp-oo-legs");
|
||
if (!el) return;
|
||
function one(tag, c) {
|
||
if (!c) return tag + ": —";
|
||
return (
|
||
tag +
|
||
": " +
|
||
c.opt_type +
|
||
" K" +
|
||
c.strike +
|
||
" " +
|
||
(c.moneyness_label || "") +
|
||
" ask=" +
|
||
fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) +
|
||
" (" +
|
||
c.inst_id +
|
||
")"
|
||
);
|
||
}
|
||
el.innerHTML = one("腿A", state.legA) + "<br/>" + one("腿B", state.legB);
|
||
}
|
||
|
||
function legPayload(c, sheets) {
|
||
return {
|
||
opt_type: c.opt_type,
|
||
strike: c.strike,
|
||
sheets: sheets,
|
||
ct_mult: c.ct_mult || 0.01,
|
||
ask: c.ask,
|
||
inst_id: c.inst_id,
|
||
};
|
||
}
|
||
|
||
function setUnderlying(uly, forceReload) {
|
||
const next = (uly || "ETH").toUpperCase();
|
||
const changed = next !== state.underlying;
|
||
state.underlying = next;
|
||
syncUnderlyingUI();
|
||
if (!changed && !forceReload) return;
|
||
state.selected = null;
|
||
state.legA = null;
|
||
state.legB = null;
|
||
if ($("hp-entry")) $("hp-entry").value = "";
|
||
if ($("hp-contracts")) $("hp-contracts").value = "";
|
||
if ($("hp-tp")) $("hp-tp").value = "";
|
||
if ($("hp-sl")) $("hp-sl").value = "";
|
||
if ($("hp-target")) $("hp-target").value = "";
|
||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
|
||
if ($("hp-oo-legs")) $("hp-oo-legs").textContent = "尚未选用两腿";
|
||
void refreshAll();
|
||
}
|
||
|
||
async function runPreview() {
|
||
const isOo = state.mode === "options_options";
|
||
const tbody = $(isOo ? "hp-result-tbody-oo" : "hp-result-tbody");
|
||
const summary = $(isOo ? "hp-summary-oo" : "hp-summary");
|
||
try {
|
||
let body;
|
||
if (isOo) {
|
||
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
||
const target = Number(($("hp-target") && $("hp-target").value) || 0);
|
||
if (!target) throw new Error("请填写目标价");
|
||
body = {
|
||
plan_type: "options_options",
|
||
target_price: target,
|
||
index_px: (state.chain && state.chain.index_px) || target,
|
||
leg_a: legPayload(state.legA, 1),
|
||
leg_b: legPayload(state.legB, 1),
|
||
};
|
||
} else {
|
||
if (!state.selected) throw new Error("请选用期权腿");
|
||
const entry = Number(($("hp-entry") && $("hp-entry").value) || 0);
|
||
const tp = Number(($("hp-tp") && $("hp-tp").value) || 0);
|
||
const sl = Number(($("hp-sl") && $("hp-sl").value) || 0);
|
||
const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || 0);
|
||
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
|
||
if (!entry || !tp || !sl || !contracts) throw new Error("请完整填写开仓/止盈/止损/张数");
|
||
body = {
|
||
plan_type: "perp_options",
|
||
direction: ($("hp-direction") && $("hp-direction").value) || "long",
|
||
entry: entry,
|
||
tp: tp,
|
||
sl: sl,
|
||
contracts: contracts,
|
||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||
opt_type: state.selected.opt_type,
|
||
strike: state.selected.strike,
|
||
sheets: sheets,
|
||
ct_mult: state.selected.ct_mult || 0.01,
|
||
ask: state.selected.ask,
|
||
index_px: state.chain && state.chain.index_px,
|
||
};
|
||
}
|
||
const d = await apiJson("/api/hedge-plan/preview", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
setGateLine(d.gates);
|
||
const s = d.summary || {};
|
||
if (summary) {
|
||
if (d.plan_type === "perp_options") {
|
||
summary.innerHTML =
|
||
"止盈合计 <strong>" +
|
||
fmt(s.tp_total) +
|
||
"</strong> · 止损合计 <strong>" +
|
||
fmt(s.sl_total) +
|
||
"</strong> · 保费 " +
|
||
fmt(s.premium_paid) +
|
||
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
|
||
} else {
|
||
summary.innerHTML =
|
||
"目标价合计 <strong>" +
|
||
fmt(s.at_target_total) +
|
||
"</strong> · 到期现价 <strong>" +
|
||
fmt(s.expiry_flat_total) +
|
||
"</strong> · 保费 " +
|
||
fmt(s.premium_paid) +
|
||
(s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
|
||
}
|
||
}
|
||
if (!tbody) return;
|
||
tbody.innerHTML = "";
|
||
(d.scenarios || []).forEach(function (sc) {
|
||
const tr = document.createElement("tr");
|
||
let mid;
|
||
if (sc.perp_pnl != null) {
|
||
mid = "永续 " + fmt(sc.perp_pnl);
|
||
} else {
|
||
mid = "A " + fmt(sc.leg_a_pnl) + " / B " + fmt(sc.leg_b_pnl);
|
||
}
|
||
const optCol = sc.options_pnl != null ? fmt(sc.options_pnl) : "—";
|
||
tr.innerHTML =
|
||
"<td>" +
|
||
(sc.label || sc.id) +
|
||
"</td><td>" +
|
||
fmt(sc.spot) +
|
||
"</td><td>" +
|
||
mid +
|
||
"</td><td>" +
|
||
optCol +
|
||
"</td><td><strong>" +
|
||
fmt(sc.total) +
|
||
"</strong></td><td class=\"muted\">" +
|
||
(sc.note || "") +
|
||
"</td>";
|
||
tbody.appendChild(tr);
|
||
});
|
||
} catch (e) {
|
||
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="err">' + (e.message || e) + "</td></tr>";
|
||
if (summary) summary.textContent = "";
|
||
}
|
||
}
|
||
|
||
function bind() {
|
||
document.querySelectorAll(".hp-tab").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
state.tab = b.getAttribute("data-tab") || "perp_options";
|
||
syncTabUI();
|
||
if (state.tab === "perp_options" || state.tab === "options_options") {
|
||
void loadGates();
|
||
} else {
|
||
const el = $("hp-gate-line");
|
||
if (el) {
|
||
el.textContent =
|
||
state.tab === "history"
|
||
? "历史记录:独立对冲表,与普通交易记录分离"
|
||
: "统计:止盈=盈利−保费;止损=期权盈利−永续亏损";
|
||
}
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
|
||
});
|
||
});
|
||
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
|
||
b.addEventListener("click", function () {
|
||
state.moneyFilter = b.getAttribute("data-money") || "all";
|
||
syncMoneyUI();
|
||
renderListStrikes();
|
||
});
|
||
});
|
||
const dir = $("hp-direction");
|
||
if (dir) {
|
||
dir.addEventListener("change", function () {
|
||
state.selected = null;
|
||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||
void loadMarket().then(function () {
|
||
renderListStrikes();
|
||
});
|
||
});
|
||
}
|
||
if ($("hp-refresh"))
|
||
$("hp-refresh").addEventListener("click", function () {
|
||
void refreshAll();
|
||
});
|
||
if ($("hp-load-chain"))
|
||
$("hp-load-chain").addEventListener("click", function () {
|
||
void loadChain();
|
||
});
|
||
if ($("hp-oo-load-chain"))
|
||
$("hp-oo-load-chain").addEventListener("click", function () {
|
||
void loadChain();
|
||
});
|
||
if ($("hp-exp-select")) $("hp-exp-select").addEventListener("change", renderListStrikes);
|
||
if ($("hp-oo-exp-select")) $("hp-oo-exp-select").addEventListener("change", renderTStrikes);
|
||
if ($("hp-sheets")) $("hp-sheets").addEventListener("input", updatePremiumLine);
|
||
if ($("hp-preview-btn"))
|
||
$("hp-preview-btn").addEventListener("click", function () {
|
||
state.mode = "perp_options";
|
||
void runPreview();
|
||
});
|
||
if ($("hp-preview-btn-oo"))
|
||
$("hp-preview-btn-oo").addEventListener("click", function () {
|
||
state.mode = "options_options";
|
||
void runPreview();
|
||
});
|
||
}
|
||
|
||
async function refreshAll() {
|
||
if (state.tab === "perp_options" || state.tab === "options_options") {
|
||
await loadGates();
|
||
}
|
||
try {
|
||
await loadMarket();
|
||
} catch (e) {
|
||
const q = $("hp-perp-quote");
|
||
if (q) q.textContent = e.message || String(e);
|
||
}
|
||
try {
|
||
await loadChain();
|
||
} catch (e) {
|
||
const tbody = $("hp-strike-tbody");
|
||
if (tbody) tbody.innerHTML = '<tr><td colspan="5" class="err">' + (e.message || e) + "</td></tr>";
|
||
}
|
||
}
|
||
|
||
syncTabUI();
|
||
syncUnderlyingUI();
|
||
syncMoneyUI();
|
||
bind();
|
||
void refreshAll();
|
||
})();
|