/**
* 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);
// 仅裁小数尾零;整数 tick(BTC=5)时绝不能把 1370 裁成 137
if (decimals > 0) s = s.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 '' + label + "";
}
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", "active", "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");
const startOo = $("hp-start-btn-oo");
if ((gates.plan_type || state.mode) === "options_options") {
if (startOo) startOo.disabled = !gates.can_start;
} else {
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 amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4;
const q = $("hp-perp-quote");
if (q) {
q.innerHTML =
"" +
(d.base || state.underlying) +
" · " +
acctLabel +
"可用 " +
fmt(d.available_usdt, 2) +
" USDT
标记 " +
fmt(d.mark, 2) +
" · 最新 " +
fmt(d.last, 2) +
" · 卖一 " +
fmt(d.ask, 2) +
" · 买一 " +
fmt(d.bid, 2) +
" · 面值 " +
fmt(d.contract_size, 4) +
" · 张精度 " +
amtPrec +
" 位";
}
const contractsInput = $("hp-contracts");
if (contractsInput) {
const step = amtPrec <= 0 ? "1" : String(Math.pow(10, -amtPrec));
contractsInput.step = step;
}
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) +
" USDT × " +
s.leverage +
"x → 名义 " +
fmt(s.notional_value, 2) +
" USDT · 建议 " +
fmt(d.suggest_contracts, amtPrec) +
" 合约张(已按交易所精度)";
} else {
sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓.";
}
}
const entry = $("hp-entry");
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
if (contractsInput && d.suggest_contracts != null && !contractsInput.value) {
contractsInput.value = fmt(d.suggest_contracts, amtPrec);
}
const label = $("hp-opt-type-label");
if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put";
updatePerpPnlHint();
}
function updatePerpPnlHint() {
const el = $("hp-perp-pnl-line");
if (!el) return;
const entry = Number(($("hp-entry") && $("hp-entry").value) || NaN);
const tp = Number(($("hp-tp") && $("hp-tp").value) || NaN);
const sl = Number(($("hp-sl") && $("hp-sl").value) || NaN);
const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || NaN);
const cs = Number((state.market && state.market.contract_size) || 0.01);
const dir = (($("hp-direction") && $("hp-direction").value) || "long").toLowerCase();
if (!(entry > 0) || !(contracts > 0) || !(cs > 0)) {
el.textContent = "填写开仓价与张数后,输入止盈/止损可看永续盈亏金额(USDT)";
return;
}
function pnlAt(exitPx) {
const coins = contracts * cs;
if (dir === "short") return (entry - exitPx) * coins;
return (exitPx - entry) * coins;
}
const parts = [];
if (tp > 0) {
const p = pnlAt(tp);
parts.push(
"止盈预期 = 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT"
);
} else {
parts.push("止盈预期 —");
}
if (sl > 0) {
const p = pnlAt(sl);
parts.push(
"止损预期 = 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT"
);
} else {
parts.push("止损预期 —");
}
el.innerHTML = parts.join(" · ");
}
function fillExpSelect(sel, chain) {
if (!sel) return;
const prev = sel.value;
sel.innerHTML = '';
(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) {
const idx = Number(d.index_px);
if ($("hp-target-up") && !$("hp-target-up").value) {
$("hp-target-up").value = String(Math.round(idx * 1.03));
}
if ($("hp-target-down") && !$("hp-target-down").value) {
$("hp-target-down").value = String(Math.round(idx * 0.97));
}
}
}
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 pickContract(c) {
if (!c) return;
state.selected = c;
const el = $("hp-sel-inst");
if (el) el.textContent = c.inst_id;
const tbody = $("hp-strike-tbody");
if (tbody) {
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === c.inst_id);
});
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
b.classList.toggle("active", b.getAttribute("data-inst") === c.inst_id);
});
}
updatePremiumLine();
}
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 = '
" +
c.inst_id +
"";
if (sheets) sheets.disabled = false;
}
fill("腿A", state.legA, "hp-oo-leg-a-info", "hp-oo-sheets-a");
fill("腿B", state.legB, "hp-oo-leg-b-info", "hp-oo-sheets-b");
updateOoPremiumLine();
}
function ooSheets(id) {
const n = Number(($(id) && $(id).value) || 1);
return n > 0 ? n : 1;
}
function updateOoPremiumLine() {
const line = $("hp-oo-prem-line");
if (!line) return;
if (!state.legA && !state.legB) {
line.textContent = "";
return;
}
function prem(c, sheets) {
if (!c) return 0;
return Number(c.ask || 0) * sheets * Number(c.ct_mult || 0.01);
}
const a = prem(state.legA, ooSheets("hp-oo-sheets-a"));
const b = prem(state.legB, ooSheets("hp-oo-sheets-b"));
line.textContent =
"预估权利金 A " +
fmt(a, 4) +
" + B " +
fmt(b, 4) +
" ≈ " +
fmt(a + b, 4) +
" USDC";
}
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-up")) $("hp-target-up").value = "";
if ($("hp-target-down")) $("hp-target-down").value = "";
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
if ($("hp-oo-sheets-a")) {
$("hp-oo-sheets-a").value = "1";
$("hp-oo-sheets-a").disabled = true;
}
if ($("hp-oo-sheets-b")) {
$("hp-oo-sheets-b").value = "1";
$("hp-oo-sheets-b").disabled = true;
}
renderOoLegs();
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 up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
if (!up || !down) throw new Error("请填写上破与下破目标价");
if (up <= down) throw new Error("上破目标价必须大于下破目标价");
body = {
plan_type: "options_options",
target_price_up: up,
target_price_down: down,
target_price: up,
index_px: (state.chain && state.chain.index_px) || (up + down) / 2,
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
};
} 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 =
"止盈合计 " +
fmt(s.tp_total) +
" · 止损合计 " +
fmt(s.sl_total) +
" · 保费 " +
fmt(s.premium_paid) +
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
} else {
summary.innerHTML =
"目标价合计 " +
fmt(s.at_target_total) +
" · 到期现价 " +
fmt(s.expiry_flat_total) +
" · 保费 " +
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 =
"" +
contracts +
"" +
contracts +
"加载中…
'; if (title) title.textContent = "成交细节 #" + planId; try { const d = await apiJson("/api/hedge-plan/" + planId); const p = d.plan || {}; const legs = d.legs || []; const typeLabel = p.plan_type === "perp_options" ? "永期对冲" : "期期对冲"; let html = ""; html += '" +
(d.contracts_summary || "—") +
"| 角色 | 合约名称 | 方向/类型 | 数量 | 开仓价 | 权利金 | 状态 | 腿盈亏 | 成交号 | 平仓原因 | "; html += "
|---|---|---|---|---|---|---|---|---|---|
| 无腿记录 | |||||||||
| " + roleLabel(leg.leg_role) + " | "; html += "" + contract + " | ";
html += "" + side + " | "; html += "" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + " | "; html += "" + fmt(leg.avg_open, 4) + " | "; html += "" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + " | "; html += "" + (leg.status || "—") + " | "; html += "" + fmt(leg.realized_pnl, 4) + " | "; html += "" + (leg.exchange_ord_id || "—") + " | ";
html += "" + reasonLabel(leg.close_reason) + " | "; html += "
备注 ' + String(p.note) + "
"; } body.innerHTML = html; } catch (e) { body.innerHTML = '' + (e.message || e) + "
"; } } async function deletePlan(planId) { if (!window.confirm("确认删除历史计划 #" + planId + "?此操作不可恢复。")) return; try { await apiJson("/api/hedge-plan/" + planId, { method: "DELETE" }); await loadHistory(); if (state.tab === "stats") await loadStats(); } catch (e) { window.alert(e.message || String(e)); } } function metricCard(title, m) { if (!m || !m.count) { return ( '暂无已结束样本
活跃 ' + (d.active || 0) + " · 已结 " + (d.closed_count || 0) + ' · 合计 ' + fmt(d.closed_pnl_total) + " ≈U