Files
crypto_monitor/lib/common/static/hedge_plan.js
T
2026-07-19 00:43:04 +08:00

1268 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
};
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 '<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() {
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;
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 =
"<strong>" +
(d.base || state.underlying) +
"</strong> · " +
acctLabel +
"可用 <strong>" +
fmt(d.available_usdt, 2) +
"</strong> USDT<br/>标记 <strong>" +
fmt(d.mark, 2) +
"</strong> · 最新 " +
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(
"止盈预期 <strong class=\"" +
(p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT</strong>"
);
} else {
parts.push("止盈预期 —");
}
if (sl > 0) {
const p = pnlAt(sl);
parts.push(
"止损预期 <strong class=\"" +
(p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT</strong>"
);
} else {
parts.push("止损预期 —");
}
el.innerHTML = parts.join(" · ");
}
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) {
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 = '<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;
});
pickContract(c);
});
});
}
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() {
function fill(tag, c, infoId, sheetsId) {
const info = $(infoId);
const sheets = $(sheetsId);
if (!info) return;
if (!c) {
info.textContent = tag + ": 尚未选用";
if (sheets) {
sheets.disabled = true;
sheets.value = "1";
}
return;
}
info.innerHTML =
tag +
": <strong>" +
c.opt_type +
" K" +
c.strike +
"</strong> " +
(c.moneyness_label || "") +
" · 卖一 " +
fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) +
" <code>" +
c.inst_id +
"</code>";
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 =
"止盈合计 <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 () {
const next = b.getAttribute("data-tab") || pickDefaultTab();
if (next === "perp_options" && !showPerp) return;
if (next === "options_options" && !showOo) return;
state.tab = next;
syncTabUI();
if (state.tab === "perp_options" || state.tab === "options_options") {
void loadGates();
} else if (state.tab === "active") {
void loadActivePlans();
} else if (state.tab === "history") {
void loadHistory();
} else if (state.tab === "stats") {
void loadStats();
} else {
const el = $("hp-gate-line");
if (el) el.textContent = "";
}
});
});
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();
});
});
}
["hp-entry", "hp-tp", "hp-sl", "hp-contracts"].forEach(function (id) {
const el = $(id);
if (el) el.addEventListener("input", updatePerpPnlHint);
});
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);
["hp-oo-sheets-a", "hp-oo-sheets-b"].forEach(function (id) {
const el = $(id);
if (el) el.addEventListener("input", updateOoPremiumLine);
});
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();
});
if ($("hp-start-btn"))
$("hp-start-btn").addEventListener("click", function () {
void startPlan("perp_options");
});
if ($("hp-start-btn-oo"))
$("hp-start-btn-oo").addEventListener("click", function () {
void startPlan("options_options");
});
if ($("hp-detail-close")) $("hp-detail-close").addEventListener("click", closeModal);
const modal = $("hp-detail-modal");
if (modal) {
modal.addEventListener("click", function (ev) {
if (ev.target === modal) closeModal();
});
}
}
async function loadHistory() {
const tbody = $("hp-history-tbody");
if (!tbody) return;
try {
const d = await apiJson("/api/hedge-plan/history");
const rows = d.plans || [];
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="10" class="muted">暂无已结束计划</td></tr>';
return;
}
tbody.innerHTML = "";
rows.forEach(function (p) {
const tr = document.createElement("tr");
const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期";
const contracts = p.contracts_summary || "—";
const pnl = p.realized_pnl_total;
const pnlCls =
pnl == null || Number.isNaN(Number(pnl)) ? "" : Number(pnl) >= 0 ? "hp-pnl-pos" : "hp-pnl-neg";
tr.innerHTML =
"<td>#" +
p.id +
"</td><td>" +
typeLabel +
"</td><td>" +
(p.underlying || "") +
"</td><td class=\"hp-contracts-cell\" title=\"" +
String(contracts).replace(/"/g, "&quot;") +
"\"><code>" +
contracts +
"</code></td><td>" +
(p.status || "") +
'</td><td class="' +
pnlCls +
'">' +
fmt(pnl) +
"</td><td>" +
reasonLabel(p.close_reason) +
"</td><td>" +
(p.opened_at || "—") +
"</td><td>" +
(p.closed_at || "—") +
'</td><td class="hp-hist-actions">' +
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
p.id +
'">成交细节</button> ' +
'<button type="button" class="btn-secondary hp-btn-del" data-id="' +
p.id +
'">删除</button>' +
"</td>";
tbody.appendChild(tr);
});
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
btn.addEventListener("click", function () {
void showPlanDetail(Number(btn.getAttribute("data-id")));
});
});
tbody.querySelectorAll(".hp-btn-del").forEach(function (btn) {
btn.addEventListener("click", function () {
void deletePlan(Number(btn.getAttribute("data-id")));
});
});
} catch (e) {
tbody.innerHTML = '<tr><td colspan="10" class="err">' + (e.message || e) + "</td></tr>";
}
}
function activeTargetLabel(p) {
if (p.plan_type === "perp_options") {
return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl);
}
return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price);
}
async function loadActivePlans() {
const tbody = $("hp-active-tbody");
if (!tbody) return;
try {
const d = await apiJson("/api/hedge-plan/active");
const rows = d.plans || [];
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="8" class="muted">暂无进行中的计划</td></tr>';
return;
}
tbody.innerHTML = "";
rows.forEach(function (p) {
const tr = document.createElement("tr");
const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期";
const contracts = p.contracts_summary || "—";
tr.innerHTML =
"<td>#" +
p.id +
"</td><td>" +
typeLabel +
"</td><td>" +
(p.underlying || "") +
"</td><td class=\"hp-contracts-cell\" title=\"" +
String(contracts).replace(/"/g, "&quot;") +
"\"><code>" +
contracts +
"</code></td><td>" +
'<span class="hp-plan-active">进行中</span>' +
"</td><td>" +
activeTargetLabel(p) +
"</td><td>" +
(p.opened_at || "—") +
'</td><td class="hp-hist-actions"><button type="button" class="btn-secondary hp-btn-detail" data-id="' +
p.id +
'">成交细节</button></td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
btn.addEventListener("click", function () {
void showPlanDetail(Number(btn.getAttribute("data-id")));
});
});
} catch (e) {
tbody.innerHTML = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
}
}
function reasonLabel(r) {
const map = {
perp_tp: "永续止盈",
perp_sl: "永续止损",
oo_expiry_loss: "期期到期亏损",
oo_expiry_win: "期期到期盈利",
target_win_leg: "期期平盈利腿",
target_up_win_leg: "期期上破·平盈利腿",
target_down_win_leg: "期期下破·平盈利腿",
expiry: "到期",
manual: "人工结束",
partial_fail: "半腿失败",
cancelled: "已取消",
};
return map[r] || r || "—";
}
function roleLabel(role) {
const map = {
perp: "永续腿",
option_hedge: "保险期权",
option_a: "期期腿A",
option_b: "期期腿B",
};
return map[role] || role || "—";
}
function closeModal() {
const m = $("hp-detail-modal");
if (m) m.hidden = true;
}
async function showPlanDetail(planId) {
const modal = $("hp-detail-modal");
const body = $("hp-detail-body");
const title = $("hp-detail-title");
if (!modal || !body) return;
modal.hidden = false;
body.innerHTML = '<p class="muted">加载中…</p>';
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 += '<div class="hp-detail-summary">';
html += "<div><span class=\"muted\">类型</span> " + typeLabel + "</div>";
html += "<div><span class=\"muted\">标的</span> " + (p.underlying || "—");
if (p.direction) html += " · " + (p.direction === "long" ? "做多" : "做空");
html += "</div>";
html += "<div><span class=\"muted\">状态</span> " + (p.status || "—") + " / " + reasonLabel(p.close_reason) + "</div>";
html += "<div><span class=\"muted\">时间</span> " + (p.opened_at || "—") + " → " + (p.closed_at || "—") + "</div>";
html +=
"<div><span class=\"muted\">盈亏</span> 永续 " +
fmt(p.realized_pnl_perp) +
" · 期权 " +
fmt(p.realized_pnl_options) +
" · 合计 <strong class=\"" +
(Number(p.realized_pnl_total) >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
'">' +
fmt(p.realized_pnl_total) +
"</strong> ≈U</div>";
if (p.plan_type === "perp_options") {
html +=
"<div><span class=\"muted\">参考价</span> 开 " +
fmt(p.entry_mark) +
" · 止盈 " +
fmt(p.tp) +
" · 止损 " +
fmt(p.sl) +
" · 杠杆 " +
fmt(p.leverage, 0) +
"x · 张数 " +
fmt(p.perp_size, 4) +
"</div>";
} else {
html +=
"<div><span class=\"muted\">目标价</span> 上破 " +
fmt(p.target_price_up || p.target_price) +
" · 下破 " +
fmt(p.target_price_down || p.target_price) +
"</div>";
}
html +=
"<div><span class=\"muted\">权利金合计</span> " +
fmt(p.premium_total, 4) +
" USDC</div>";
html +=
"<div><span class=\"muted\">合约摘要</span> <code>" +
(d.contracts_summary || "—") +
"</code></div>";
html += "</div>";
html += '<table class="options-strike-table hp-detail-legs"><thead><tr>';
html +=
"<th>角色</th><th>合约名称</th><th>方向/类型</th><th>数量</th><th>开仓价</th><th>权利金</th><th>状态</th><th>腿盈亏</th><th>成交号</th><th>平仓原因</th>";
html += "</tr></thead><tbody>";
if (!legs.length) {
html += '<tr><td colspan="10" class="muted">无腿记录</td></tr>';
} else {
legs.forEach(function (leg) {
const contract =
leg.leg_role === "perp"
? leg.symbol || "—"
: leg.inst_id || "—";
const side =
leg.leg_role === "perp"
? leg.side || "—"
: (leg.opt_type || "") + (leg.strike != null ? " K" + fmt(leg.strike, 0) : "");
html += "<tr>";
html += "<td>" + roleLabel(leg.leg_role) + "</td>";
html += "<td><code>" + contract + "</code></td>";
html += "<td>" + side + "</td>";
html += "<td>" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + "</td>";
html += "<td>" + fmt(leg.avg_open, 4) + "</td>";
html += "<td>" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + "</td>";
html += "<td>" + (leg.status || "—") + "</td>";
html += "<td>" + fmt(leg.realized_pnl, 4) + "</td>";
html += "<td><code class=\"hp-ord\">" + (leg.exchange_ord_id || "—") + "</code></td>";
html += "<td>" + reasonLabel(leg.close_reason) + "</td>";
html += "</tr>";
});
}
html += "</tbody></table>";
if (p.note) {
html += '<p class="hp-detail-note"><span class="muted">备注</span> ' + String(p.note) + "</p>";
}
body.innerHTML = html;
} catch (e) {
body.innerHTML = '<p class="err">' + (e.message || e) + "</p>";
}
}
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 (
'<div class="hp-stats-card"><h3>' +
title +
'</h3><p class="muted">暂无已结束样本</p></div>'
);
}
const wr = m.win_rate == null ? "—" : (Number(m.win_rate) * 100).toFixed(1) + "%";
let pf = "—";
if (m.profit_factor_infinite) pf = "∞";
else if (m.profit_factor != null) pf = fmt(m.profit_factor, 2);
return (
'<div class="hp-stats-card"><h3>' +
title +
"</h3><ul class=\"hp-stats-list\">" +
"<li><span>笔数</span><strong>" +
m.count +
"</strong></li>" +
"<li><span>胜率</span><strong>" +
wr +
"</strong> <span class=\"muted\">(" +
m.wins +
"/" +
m.count +
")</span></li>" +
"<li><span>净盈亏≈U</span><strong class=\"" +
(Number(m.net_pnl) >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
'">' +
fmt(m.net_pnl) +
"</strong></li>" +
"<li><span>盈亏比</span><strong>" +
pf +
"</strong> <span class=\"muted\">毛利/|毛亏|</span></li>" +
"<li><span>最大盈利</span><strong class=\"hp-pnl-pos\">" +
fmt(m.max_profit) +
"</strong></li>" +
"<li><span>最大亏损</span><strong class=\"hp-pnl-neg\">" +
fmt(m.max_loss) +
"</strong></li>" +
"<li><span>最大回撤</span><strong>" +
fmt(m.max_drawdown) +
"</strong></li>" +
"<li><span>平均保费</span><strong>" +
fmt(m.avg_premium, 4) +
"</strong></li>" +
"</ul></div>"
);
}
async function loadStats() {
const box = $("hp-stats-box");
if (!box) return;
try {
const d = await apiJson("/api/hedge-plan/stats");
const by = d.by_type || {};
let html = '<div class="hp-stats-grid">';
html +=
'<div class="hp-stats-card hp-stats-card--overview"><h3>总览</h3><p>活跃 <strong>' +
(d.active || 0) +
"</strong> · 已结 <strong>" +
(d.closed_count || 0) +
'</strong> · 合计 <strong class="' +
(Number(d.closed_pnl_total) >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
'">' +
fmt(d.closed_pnl_total) +
"</strong> ≈U</p></div>";
html += metricCard("永期对冲", by.perp_options);
html += metricCard("期期对冲", by.options_options);
html += "</div>";
const poB = (by.perp_options && by.perp_options.buckets) || {};
const ooB = (by.options_options && by.options_options.buckets) || {};
if ((poB.tp && poB.tp.count) || (poB.sl && poB.sl.count) || (ooB.expiry_loss && ooB.expiry_loss.count)) {
html += '<div class="hp-stats-grid hp-stats-grid--sub">';
if (poB.tp && poB.tp.count) html += metricCard("永期·止盈桶", poB.tp);
if (poB.sl && poB.sl.count) html += metricCard("永期·止损桶", poB.sl);
if (ooB.expiry_loss && ooB.expiry_loss.count) html += metricCard("期期·到期亏损", ooB.expiry_loss);
if (ooB.expiry_win && ooB.expiry_win.count) html += metricCard("期期·到期盈利", ooB.expiry_win);
html += "</div>";
}
box.innerHTML = html;
} catch (e) {
box.textContent = e.message || String(e);
}
}
async function startPlan(planType) {
const isOo = planType === "options_options";
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",
underlying: state.underlying,
target_price_up: up,
target_price_down: down,
target_price: up,
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",
underlying: state.underlying,
direction: ($("hp-direction") && $("hp-direction").value) || "long",
entry: entry,
tp: tp,
sl: sl,
contracts: contracts,
sheets: sheets,
opt_inst_id: state.selected.inst_id,
opt_type: state.selected.opt_type,
strike: state.selected.strike,
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
leverage: 10,
margin: state.market && state.market.full_margin_sizing && state.market.full_margin_sizing.margin_capital,
};
}
if (!window.confirm("确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)")) return;
const d = await apiJson("/api/hedge-plan/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
setGateLine(d.gates);
alert("计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : ""));
void loadGates();
} catch (e) {
alert(e.message || String(e));
}
}
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();
})();