/**
* OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁.
*/
(function () {
const root = document.getElementById("hedge-plan-root");
if (!root) return;
function flagOn(attr) {
return root.getAttribute(attr) !== "0";
}
const showPerp = flagOn("data-show-perp");
const showOo = flagOn("data-show-oo");
function pickDefaultTab() {
if (showPerp) return "perp_options";
if (showOo) return "options_options";
return "active";
}
const state = {
tab: pickDefaultTab(),
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
underlying: root.getAttribute("data-default-underly") || "ETH",
moneyFilter: "all",
chain: null,
selected: null,
legA: null,
legB: null,
market: null,
ooSheetsMode: "same_sheets",
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
ooCloseMode: "close_all",
tradingUsdc: null,
tradeBudgetUsdc: null,
budgetBuffer: 0.95,
};
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() {
let tab = state.tab || pickDefaultTab();
if (tab === "perp_options" && !showPerp) tab = pickDefaultTab();
if (tab === "options_options" && !showOo) tab = pickDefaultTab();
state.tab = tab;
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;
}
const hint = $("hp-acct-hint");
if (hint) {
if (tab === "options_options") {
hint.textContent = "期期双腿→期权账户";
} else if (tab === "perp_options") {
hint.textContent = "永续腿→合约账户 · 期权腿→期权账户";
} else {
hint.textContent = "进行中/历史含永期与期期;显示开关只影响新建测算 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;
if (acct.trading_usdc != null && acct.trading_usdc !== "") {
state.tradingUsdc = Number(acct.trading_usdc);
}
if (chain && chain.trade_budget_usdc != null && chain.trade_budget_usdc !== "") {
state.tradeBudgetUsdc = Number(chain.trade_budget_usdc);
}
if (chain && chain.budget_buffer != null && chain.budget_buffer !== "") {
const buf = Number(chain.budget_buffer);
if (!Number.isNaN(buf) && buf > 0) state.budgetBuffer = buf;
}
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;
autoFillOoSheets();
}
function resolveOoBudget() {
const buf = state.budgetBuffer > 0 ? state.budgetBuffer : 0.95;
const trading = state.tradingUsdc;
const cap = state.tradeBudgetUsdc;
let tradingCap = null;
let tradeCap = null;
if (trading != null && !Number.isNaN(Number(trading))) {
tradingCap = Math.max(0, Number(trading) * buf);
}
if (cap != null && !Number.isNaN(Number(cap))) {
tradeCap = Math.max(0, Number(cap));
}
if (tradingCap == null && tradeCap == null) {
return { ok: false, budget: 0, tradingCap: null, tradeCap: null, buf: buf, msg: "缺少交易户余额与单笔预算" };
}
let budget = 0;
if (tradingCap == null) budget = tradeCap;
else if (tradeCap == null) budget = tradingCap;
else budget = Math.min(tradingCap, tradeCap);
budget = Math.floor(budget * 1e6 + 1e-12) / 1e6;
return {
ok: budget > 0,
budget: budget,
tradingCap: tradingCap,
tradeCap: tradeCap,
buf: buf,
msg: budget > 0 ? "" : "可用预算为 0",
};
}
function unitCost(c) {
if (!c) return 0;
const ask = Number(c.ask || 0);
if (!(ask > 0)) return 0;
return ask * Number(c.ct_mult || 0.01);
}
function capByDepth(n, askSz) {
let out = Math.max(0, Math.floor(Number(n) || 0));
if (askSz == null || askSz === "") return out;
const d = Number(askSz);
if (Number.isNaN(d)) return out;
if (d <= 0) return 0;
return Math.min(out, Math.floor(d + 1e-12));
}
function suggestOoSheetsLocal(budget, mode) {
const costA = unitCost(state.legA);
const costB = unitCost(state.legB);
if (!(budget > 0)) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "可用预算为 0" };
}
if (!(costA > 0) || !(costB > 0)) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" };
}
let nA = 0;
let nB = 0;
if (mode === "split_budget") {
const half = budget / 2;
nA = Math.floor(half / costA + 1e-12);
nB = Math.floor(half / costB + 1e-12);
} else {
const pair = costA + costB;
const n = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0;
nA = n;
nB = n;
}
nA = capByDepth(nA, state.legA && state.legA.ask_sz);
nB = capByDepth(nB, state.legB && state.legB.ask_sz);
if (mode !== "split_budget") {
const n = Math.min(nA, nB);
nA = n;
nB = n;
}
const premium = costA * nA + costB * nB;
const ok = nA >= 1 && nB >= 1;
return {
sheetsA: nA,
sheetsB: nB,
premium: premium,
ok: ok,
msg: ok ? "" : "预算不够开 1+1(或卖一深度不足)",
};
}
function syncOoSizeModeUI() {
document.querySelectorAll(".hp-oo-size-mode").forEach(function (b) {
const on = b.getAttribute("data-oo-size") === state.ooSheetsMode;
b.classList.toggle("active", on);
});
}
function syncOoCloseModeUI() {
const enabled = !!state.ooCloseModeEnabled;
const row = $("hp-oo-close-mode-row");
const hint = $("hp-oo-close-mode-hint");
if (row) row.classList.toggle("hidden", !enabled);
if (hint) hint.classList.toggle("hidden", !enabled);
if (!enabled) state.ooCloseMode = "hold_expiry";
document.querySelectorAll(".hp-oo-close-mode").forEach(function (b) {
const on = b.getAttribute("data-oo-close") === state.ooCloseMode;
b.classList.toggle("active", on);
});
}
function updateOoBudgetLine(extra) {
const line = $("hp-oo-budget-line");
if (!line) return;
const b = resolveOoBudget();
const modeLabel = state.ooSheetsMode === "split_budget" ? "均分预算" : "同张数";
const parts = [
"预算 min(交易×" + fmt(b.buf, 2) + ", 单笔) ≈ " + fmt(b.budget, 2) + " USDC",
"模式:" + modeLabel,
];
if (b.tradingCap != null) parts.push("交易×缓冲 " + fmt(b.tradingCap, 2));
if (b.tradeCap != null) parts.push("单笔上限 " + fmt(b.tradeCap, 2));
if (extra && extra.msg) parts.push(extra.msg);
else if (b.msg) parts.push(b.msg);
line.textContent = parts.join(" · ");
line.classList.toggle("hp-oo-budget-warn", !!(extra && extra.msg) || !b.ok);
}
function autoFillOoSheets() {
syncOoSizeModeUI();
if (!state.legA || !state.legB) {
updateOoBudgetLine(null);
return;
}
const b = resolveOoBudget();
const sug = suggestOoSheetsLocal(b.budget, state.ooSheetsMode);
const a = $("hp-oo-sheets-a");
const bb = $("hp-oo-sheets-b");
if (a && !a.disabled) a.value = String(sug.sheetsA);
if (bb && !bb.disabled) bb.value = String(sug.sheetsB);
updateOoBudgetLine(sug.ok ? null : sug);
updateOoPremiumLine();
}
async function loadGates() {
try {
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
if (d.oo_close_mode_enabled != null) {
state.ooCloseModeEnabled = !!d.oo_close_mode_enabled;
}
if (!state.ooCloseModeEnabled) {
state.ooCloseMode = "hold_expiry";
} else if (d.oo_close_mode_default && !state._ooCloseModeTouched) {
state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all";
}
syncOoCloseModeUI();
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");
autoFillOoSheets();
}
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