" +
(opt.inst_id || "—") +
"" +
'" +
(perp.symbol || "—") +
"" +
'/**
* 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 "perp_options";
}
const poMode = (root.getAttribute("data-po-mode") || "eth").toLowerCase();
const optionUnderly = (root.getAttribute("data-option-underly") || root.getAttribute("data-default-underly") || "ETH").toUpperCase();
const perpUnderly = (root.getAttribute("data-perp-underly") || optionUnderly).toUpperCase();
const crossAsset = root.getAttribute("data-cross-asset") === "1";
const lockedRatio = (function () {
const raw = root.getAttribute("data-option-perp-ratio");
const n = raw != null && raw !== "" ? Number(raw) : NaN;
return !Number.isNaN(n) && n > 0 ? n : 2;
})();
const state = {
tab: pickDefaultTab(),
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
posTab: "live",
underlying: optionUnderly,
poMode: poMode,
optionUnderly: optionUnderly,
perpUnderly: perpUnderly,
crossAsset: crossAsset,
moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "all" : "itm",
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
ooRecommend: null, // atm_straddle | double_otm | null
ooStrikeExpandAll: false, // 默认 Call/Put 各 3 档
poStrikeExpandAll: false, // 永期手动选约:默认平值+实3+虚3
chain: null,
selected: null,
orderDialogOpen: false,
legA: null,
legB: null,
market: null,
ooSheetsMode: "same_sheets",
ooBiasSplitBy: "budget",
ooBiasRatio: 0.7,
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
ooCloseMode: "close_all",
direction: "long",
optionPrimary: root.getAttribute("data-option-primary") !== "0",
exitMode: "profit", // 实例模式强制组合盈利金额(U)
opLevTouched: false,
opRatioTouched: crossAsset, // 跨币锁比例,视为已设定
perpLevTouched: false,
tradingUsdc: null,
fundingUsdc: null,
tradeBudgetUsdc: null,
budgetBuffer: (function () {
const raw = root.getAttribute("data-budget-buffer");
const n = raw != null && raw !== "" ? Number(raw) : NaN;
return !Number.isNaN(n) && n > 0 ? n : 0.95;
})(),
previewOk: false,
canStart: false,
previewPlanType: null,
};
function lockPoUnderlyingUI() {
const opt = state.optionUnderly || "ETH";
const perp = state.perpUnderly || opt;
const modeLabel = root.getAttribute("data-po-mode-label") || poMode;
// 实例模式锁定:手动选约只显示一种标的文案(ETH 或 BTC;跨币写 BTC→ETH)
const single = $("hp-uly-single");
if (single) {
single.textContent = crossAsset ? opt + "→" + perp : opt;
single.title = "本实例模式 " + modeLabel + ",标的固定不可切换";
}
// 彻底去掉永期页残留双钮(若有)
document.querySelectorAll("#hp-tab-perp_options .hp-uly-btn, #hp-po-layout .hp-uly-btn").forEach(function (b) {
b.remove();
});
const badge = $("hp-po-mode-badge");
if (badge) {
badge.textContent =
modeLabel + (crossAsset ? " · 期权" + opt + "/永续" + perp : "");
}
const ratioEl = $("hp-opt-perp-ratio");
if (ratioEl && lockedRatio > 0) {
ratioEl.value = String(lockedRatio);
if (crossAsset) {
ratioEl.readOnly = true;
ratioEl.title = "跨币模式由 HEDGE_CROSS_NOTIONAL_K 锁定";
}
}
}
function getDirection() {
return (state.direction || "long").toLowerCase() === "short" ? "short" : "long";
}
function syncPoDirUI() {
const dir = getDirection();
document.querySelectorAll(".hp-po-dir").forEach(function (b) {
const on = (b.getAttribute("data-dir") || "") === dir;
b.classList.toggle("is-selected", on);
b.classList.toggle("active", on);
});
const hint = $("hp-po-pair-hint");
if (hint) {
if (isOptionPrimary()) {
hint.textContent =
(dir === "short" ? "买 Put + 永续多" : "买 Call + 永续空") +
" · 点「选择」弹窗下单;比例默认 2(期权 1 → 永续 0.5)。";
} else {
hint.textContent = dir === "short" ? "永续空 + 保险 Call" : "永续多 + 保险 Put";
}
}
const typeLab = $("hp-opt-type-label");
if (typeLab && isOptionPrimary()) {
typeLab.textContent = dir === "short" ? "Put" : "Call";
}
}
function setDirection(dir, forceReload) {
const next = (dir || "long").toLowerCase() === "short" ? "short" : "long";
const changed = next !== getDirection();
state.direction = next;
syncPoDirUI();
if (!changed && !forceReload) return;
if (state.orderDialogOpen) closeOrderDialog();
else {
state.selected = null;
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
}
void loadMarket().then(function () {
renderListStrikes();
});
}
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 pnlClass(v) {
if (v == null || v === "") return "";
const n = Number(v);
if (Number.isNaN(n)) return "";
return n >= 0 ? "hp-pnl-pos" : "hp-pnl-neg";
}
function fmtPnlHtml(v, digits) {
if (v == null || v === "" || Number.isNaN(Number(v))) return "—";
const cls = pnlClass(v);
return '' + fmt(v, digits) + "";
}
function fmtRr(v) {
if (v == null || Number.isNaN(Number(v))) return "—";
return fmt(v, 2) + ":1";
}
/** 条件单杠杆门槛;手动下单传 0 则不筛. */
function optionPrimaryMinLev() {
const minLev = numInput("hp-opt-leverage", 0);
if (!(minLev > 0)) return 0;
if (opMoneyKind() === "otm") return Math.max(minLev, 180);
return minLev;
}
function optionPrimaryLevOk(c) {
const idx = indexPx();
const ask = Number(c && c.ask);
const floor = optionPrimaryMinLev();
if (!(floor > 0)) return true;
if (!(idx > 0) || !(ask > 0)) return false;
return idx / ask >= floor - 1e-9;
}
function matchesMoneyFilter(c) {
const f = state.moneyFilter || "all";
const m = (c.moneyness || "").toLowerCase();
// 保险模式不可选虚值
if (!isOptionPrimary() && f === "otm") return false;
// 手动下单:仅按 全部/实值/虚值 筛选,不做间隔/杠杆条件门槛
if (isOptionPrimary()) {
if (f === "all" || f === "any") return true;
if (f === "itm") return m === "itm" || m === "atm";
if (f === "atm") return m === "atm";
if (f === "otm") return m === "otm";
return true;
}
if (f === "itm") return m === "itm" || m === "atm";
if (f === "atm") return m === "atm";
if (f === "otm") return m === "otm";
return m === "itm" || m === "atm";
}
// 默认共约 5 档:平值 + 实值2 + 虚值2
const PO_ATM_SIDE = 2;
function slicePoListByMoneyness(list) {
if (state.poStrikeExpandAll || !list.length) return list;
const sorted = list.slice().sort(function (a, b) {
return Number(a.strike) - Number(b.strike);
});
const itm = [];
const atm = [];
const otm = [];
sorted.forEach(function (c) {
const m = String(c.moneyness || "").toLowerCase();
if (m === "atm") atm.push(c);
else if (m === "itm") itm.push(c);
else if (m === "otm") otm.push(c);
});
const n = PO_ATM_SIDE;
const isPut = optTypeForDirection(getDirection()) === "P";
const pickedItm = isPut ? itm.slice(0, n) : itm.slice(-n);
const pickedOtm = isPut ? otm.slice(-n) : otm.slice(0, n);
return pickedItm.concat(atm, pickedOtm).sort(function (a, b) {
return Number(a.strike) - Number(b.strike);
});
}
function matchesOoMoneyFilter(c) {
// 期期:仅平值/虚值(禁实值)
if (!c) return false;
const m = (c.moneyness || "").toLowerCase();
const f = state.ooMoneyFilter || "atm_otm";
if (f === "atm") return m === "atm";
if (f === "otm") return m === "otm";
return m === "atm" || m === "otm";
}
function indexPx() {
const fromChain = state.chain && Number(state.chain.index_px);
if (fromChain && !Number.isNaN(fromChain) && fromChain > 0) return fromChain;
const fromMkt = state.market && Number(state.market.index_px || state.market.mark);
if (fromMkt && !Number.isNaN(fromMkt) && fromMkt > 0) return fromMkt;
return null;
}
function currentContracts(expSelectId) {
const exp = currentExp(expSelectId);
return (exp && exp.contracts) || [];
}
function pickClosestItmAtm(contracts, want) {
const idx = indexPx();
if (!idx) return null;
const list = (contracts || []).filter(function (c) {
return (
String(c.opt_type || "").toUpperCase() === want &&
matchesMoneyFilter(c)
);
});
if (!list.length) return null;
list.sort(function (a, b) {
return Math.abs(Number(a.strike) - idx) - Math.abs(Number(b.strike) - idx);
});
return list[0];
}
function pickOoTemplate(template) {
const idx = indexPx();
const contracts = currentContracts("hp-oo-exp-select");
if (!idx || !contracts.length) return null;
const preferOtm = template === "double_otm";
function pickSide(want) {
const list = contracts.filter(function (c) {
if (String(c.opt_type || "").toUpperCase() !== want) return false;
if (!matchesOoMoneyFilter(c)) return false;
const m = (c.moneyness || "").toLowerCase();
if (preferOtm) return m === "otm";
return m === "atm" || m === "otm";
});
if (!list.length) return null;
list.sort(function (a, b) {
const ma = (a.moneyness || "").toLowerCase();
const mb = (b.moneyness || "").toLowerCase();
if (!preferOtm) {
if (ma === "atm" && mb !== "atm") return -1;
if (mb === "atm" && ma !== "atm") return 1;
}
return Math.abs(Number(a.strike) - idx) - Math.abs(Number(b.strike) - idx);
});
return list[0];
}
const call = pickSide("C");
const put = pickSide("P");
if (!call || !put) return null;
return { call: call, put: put };
}
function isOptionPrimary() {
return !!state.optionPrimary;
}
function optTypeForDirection(dir) {
if (isOptionPrimary()) {
return dir === "short" ? "P" : "C";
}
return dir === "short" ? "C" : "P";
}
function opMoneyKind() {
const f = state.moneyFilter || "all";
if (f === "otm") return "otm";
if (f === "atm") return "atm";
if (f === "all" || f === "any") return "all";
return "itm";
}
function applyOpDefaultsFromMoney(force) {
const ratioEl = $("hp-opt-perp-ratio");
if (ratioEl && (force || !state.opRatioTouched)) {
// 默认 2:期权名义÷比例=永续名义(例 1 BTC 期权 → 0.5 BTC 永续)
ratioEl.value = "2";
}
}
function syncOptionPrimaryUI() {
const on = isOptionPrimary();
const ins = $("hp-po-fields-insurance");
const op = $("hp-po-fields-option-primary");
// 保险模式只显示开仓价/张数/止盈止损;以期权为主显示三组参数(env 切换,页内不可改)
if (ins) {
ins.classList.toggle("hidden", on);
if (on) ins.setAttribute("hidden", "hidden");
else ins.removeAttribute("hidden");
ins.style.display = on ? "none" : "";
}
if (op) {
op.classList.toggle("hidden", !on);
if (!on) op.setAttribute("hidden", "hidden");
else op.removeAttribute("hidden");
op.style.display = on ? "" : "none";
}
const insMoney = $("hp-po-ins-money");
if (insMoney) {
if (on) {
insMoney.setAttribute("hidden", "hidden");
insMoney.classList.add("hidden");
} else {
insMoney.removeAttribute("hidden");
insMoney.classList.remove("hidden");
}
}
const mktMoney = $("hp-po-mkt-money");
if (mktMoney) {
if (on) {
mktMoney.removeAttribute("hidden");
mktMoney.classList.remove("hidden");
} else {
mktMoney.setAttribute("hidden", "hidden");
mktMoney.classList.add("hidden");
}
}
if (on && (state.moneyFilter === "atm" || !state.moneyFilter)) {
state.moneyFilter = "all";
syncMoneyUI();
}
if (!on && (state.moneyFilter === "otm" || state.moneyFilter === "all")) {
state.moneyFilter = "itm";
syncMoneyUI();
}
if (on) applyOpDefaultsFromMoney(false);
const badge = $("hp-po-mode-badge");
if (badge) {
badge.textContent = on ? "手动下单" : "保险模式";
badge.classList.toggle("is-insurance", !on);
}
const title = $("hp-po-card-title");
if (title) title.textContent = on ? "下单" : "执行参数";
const dirLong = document.querySelector('.hp-po-dir[data-dir="long"]');
const dirShort = document.querySelector('.hp-po-dir[data-dir="short"]');
if (dirLong) {
dirLong.title = on ? "买 Call → 永续空" : "做多永续";
if (on) {
dirLong.innerHTML = '看涨 Call';
} else {
dirLong.innerHTML = '做多';
}
}
if (dirShort) {
dirShort.title = on ? "买 Put → 永续多" : "做空永续";
if (on) {
dirShort.innerHTML = '看跌 Put';
} else {
dirShort.innerHTML = '做空';
}
}
syncPoDirUI();
syncPoActionBtn();
}
function hoursFromExpMs(expMs) {
const n = Number(expMs);
if (!n || Number.isNaN(n)) return null;
const ms = n < 1e12 ? n * 1000 : n;
return (ms - Date.now()) / 3600000;
}
function numInput(id, fallback) {
const el = $(id);
const n = Number(el && el.value);
if (Number.isNaN(n)) return fallback;
return n;
}
function computeOpSizing(ask, ctMult, sheetsOverride) {
const budget = numInput("hp-premium-budget", 0);
const ratio = numInput("hp-opt-perp-ratio", lockedRatio || 2);
const cs = Number((state.market && state.market.contract_size) || 0.01);
const usable = budget * 0.95;
const a = Number(ask || 0);
const ct = Number(ctMult || 0.01);
if (!(ct > 0) || !(ratio > 0) || !(cs > 0)) {
return null;
}
let sheets;
let eth;
if (sheetsOverride != null && Number(sheetsOverride) > 0) {
sheets = Math.floor(Number(sheetsOverride) + 1e-12);
if (!(sheets > 0)) return null;
eth = Math.round(sheets * ct * 100) / 100;
} else {
if (!(budget > 0) || !(a > 0)) return null;
eth = Math.floor((usable / a) * 100 + 1e-12) / 100;
if (!(eth > 0)) return null;
sheets = Math.floor(eth / ct + 1e-12);
if (!(sheets > 0)) return null;
eth = Math.round(sheets * ct * 100) / 100;
}
// 跨币:期权币×期权指数×(1/ratio) / 永续指数 = 永续币
let perpQty = eth / ratio;
let contracts = perpQty / cs;
let optNotional = null;
let perpNotional = null;
if (state.crossAsset) {
const optIdx = Number((state.chain && state.chain.index_px) || 0);
const perpIdx = Number((state.market && (state.market.index_px || state.market.mark)) || 0);
if (optIdx > 0 && perpIdx > 0) {
optNotional = eth * optIdx;
perpNotional = optNotional / ratio;
perpQty = perpNotional / perpIdx;
contracts = perpQty / cs;
}
}
return {
usable: usable,
eth_qty: eth,
perp_qty: perpQty,
sheets: sheets,
contracts: contracts,
premium_est: a > 0 ? a * sheets * ct : 0,
ratio: ratio,
option_notional_usdt: optNotional,
perp_notional_usdt: perpNotional,
cross_asset: !!state.crossAsset,
};
}
function orderSheets() {
const right = numInput("hp-order-sheets", 0);
if (right > 0) return Math.floor(right);
const left = numInput("hp-sheets", 0);
return left > 0 ? Math.floor(left) : 0;
}
function syncSheetsInputs(fromId) {
const n = numInput(fromId, 0);
if (!(n > 0)) return;
const v = String(Math.floor(n));
if (fromId !== "hp-sheets" && $("hp-sheets")) $("hp-sheets").value = v;
if (fromId !== "hp-order-sheets" && $("hp-order-sheets")) $("hp-order-sheets").value = v;
}
function currentOpSizing() {
if (!state.selected) return null;
const sheets = orderSheets();
return computeOpSizing(
state.selected.ask,
state.selected.ct_mult || 0.01,
sheets > 0 ? sheets : null
);
}
function formatOpSizingHtml(sized, amtPrec) {
if (!sized) return "";
const optCoin = (state.optionUnderly || state.underlying || "ETH").toUpperCase();
const perpCoin = (state.perpUnderly || state.underlying || "ETH").toUpperCase();
const prec = amtPrec != null ? amtPrec : perpCoin === "BTC" ? 4 : 2;
let html =
"期权 " +
fmt(sized.eth_qty, 2) +
" " +
optCoin +
" / " +
sized.sheets +
" 张 → 永续 " +
fmt(sized.perp_qty, 4) +
" " +
perpCoin +
" (" +
fmt(sized.contracts, prec) +
" 张) · 比例 " +
fmt(sized.ratio, 2);
if (sized.cross_asset && sized.option_notional_usdt != null) {
html +=
" · 名义U " +
fmt(sized.option_notional_usdt, 0) +
"/" +
fmt(sized.perp_notional_usdt, 0);
}
return html;
}
function defaultPerpLeverage(uly) {
const u = (uly || "ETH").toUpperCase();
const btc = Number(root.getAttribute("data-btc-leverage") || 10);
const eth = Number(root.getAttribute("data-eth-leverage") || 5);
if (u === "BTC") return Number.isFinite(btc) && btc > 0 ? btc : 10;
return Number.isFinite(eth) && eth > 0 ? eth : 5;
}
/**
* 期权开仓最大张数:OKX 交易账户 USDT 共用.
* 每张开仓需:买币(权利金×1.05×指数) + 永续保证金(对冲名义÷杠杆);
* 选最大 N 时须保证扣完买币后剩余仍够永续保证金.
*/
function computeMaxSheets() {
const ask = state.selected && Number(state.selected.ask);
const ct = Number((state.selected && state.selected.ct_mult) || 0.01);
const ratio = numInput("hp-opt-perp-ratio", 2);
const mark = Number(
(state.market && (state.market.mark || state.market.index_px)) || indexPx() || 0
);
const lev = defaultPerpLeverage(state.underlying);
const tradingUsdt = Number(
state.tradingUsdt != null ? state.tradingUsdt : state.tradingUsdc
);
if (!(ask > 0) || !(ct > 0) || !(ratio > 0) || !(mark > 0) || !(lev > 0)) {
return null;
}
if (!(tradingUsdt > 0)) {
return {
max_sheets: 0,
lev: lev,
trading_usdt: tradingUsdt,
buy_usdt_per_sheet: 0,
margin_per_sheet: 0,
total_per_sheet: 0,
};
}
// 买币 USDT/张 = 权利金币 × 1.05 × 指数
const buyUsdtPerSheet = ask * ct * 1.05 * mark;
// 永续名义/张 = (期权币/比例)×指数; 保证金 = 名义÷杠杆
const perpNotionalPerSheet = (ct / ratio) * mark;
const marginPerSheet = perpNotionalPerSheet / lev;
const totalPerSheet = buyUsdtPerSheet + marginPerSheet;
if (!(totalPerSheet > 0)) return null;
const maxSheets = Math.max(0, Math.floor(tradingUsdt / totalPerSheet + 1e-12));
return {
max_sheets: maxSheets,
lev: lev,
trading_usdt: tradingUsdt,
buy_usdt_per_sheet: buyUsdtPerSheet,
margin_per_sheet: marginPerSheet,
total_per_sheet: totalPerSheet,
};
}
function updatePoOpenSummary() {
const maxEl = $("hp-po-max-line");
const perpEl = $("hp-po-perp-auto");
const maxInfo = computeMaxSheets();
const sized = currentOpSizing();
const coin = (state.underlying || "ETH").toUpperCase();
const lev = defaultPerpLeverage(state.underlying);
const mark = Number(
(state.market && (state.market.mark || state.market.index_px)) || indexPx() || 0
);
if (maxEl) {
if (maxInfo) {
maxEl.innerHTML =
"期权开仓最大 " +
maxInfo.max_sheets +
" 张 · 交易 USDT " +
fmt(maxInfo.trading_usdt, 2) +
" · 每张约买币 " +
fmt(maxInfo.buy_usdt_per_sheet, 2) +
" + 永续保证金 " +
fmt(maxInfo.margin_per_sheet, 2) +
" (杠杆 " +
lev +
"x)";
} else {
maxEl.textContent =
"选择期权并刷新后显示:期权开仓最大张数(扣买币后剩余须够永续保证金)";
}
}
if (perpEl) {
if (sized) {
const buyUsdt = sized.premium_est * 1.05 * (mark > 0 ? mark : 0);
const marginUsdt =
sized.perp_qty * (mark > 0 ? mark : 0) / (lev > 0 ? lev : 1);
const remain =
Number(state.tradingUsdt != null ? state.tradingUsdt : state.tradingUsdc) -
buyUsdt;
const marginOk = remain + 1e-9 >= marginUsdt;
perpEl.innerHTML =
"永续自动开 " +
fmt(sized.perp_qty, 4) +
" " +
coin +
" / " +
fmt(sized.contracts, coin === "BTC" ? 4 : 2) +
" 张 · 买币约 " +
fmt(sized.premium_est * 1.05, 4) +
" " +
coin +
"(≈" +
fmt(buyUsdt, 2) +
" U含5%) · 扣后剩 " +
fmt(remain, 2) +
" U · 永续保证金约 " +
fmt(marginUsdt, 2) +
" U" +
(marginOk ? "" : ' 不足');
} else {
perpEl.textContent = "永续开仓 — (填张数并选择期权后按比例自动算)";
}
}
}
function syncExitModeUI() {
const mode = state.exitMode === "target" ? "target" : "profit";
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
const on = (b.getAttribute("data-exit") || "") === mode;
b.classList.toggle("is-selected", on);
b.classList.toggle("active", on);
});
const profitBox = $("hp-po-exit-profit");
const targetBox = $("hp-po-exit-target");
if (profitBox) {
const show = mode === "profit";
profitBox.classList.toggle("hidden", !show);
if (show) {
profitBox.removeAttribute("hidden");
profitBox.style.display = "";
} else {
profitBox.setAttribute("hidden", "hidden");
profitBox.style.display = "none";
}
}
if (targetBox) {
const show = mode === "target";
targetBox.classList.toggle("hidden", !show);
if (show) {
targetBox.removeAttribute("hidden");
targetBox.style.display = "";
} else {
targetBox.setAttribute("hidden", "hidden");
targetBox.style.display = "none";
}
}
}
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;
// 永续开仓杠杆跟随 env(BTC_LEVERAGE / ALT_LEVERAGE=ETH)
const levEl = $("hp-perp-leverage");
if (levEl && !state.perpLevTouched) {
levEl.value = String(defaultPerpLeverage(uly));
}
}
function syncMoneyUI() {
const moneySel = $("hp-money-select");
if (moneySel && isOptionPrimary()) {
moneySel.value = state.moneyFilter || "all";
}
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
const on = b.getAttribute("data-money") === state.moneyFilter;
b.classList.toggle("active", on);
b.classList.toggle("is-selected", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
document.querySelectorAll(".hp-oo-money-btn").forEach(function (b) {
const on = b.getAttribute("data-oo-money") === state.ooMoneyFilter;
b.classList.toggle("active", on);
b.classList.toggle("is-selected", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
const expandCb = $("hp-strike-expand-all");
if (expandCb) expandCb.checked = !!state.poStrikeExpandAll;
}
function syncPoActionBtn() {
const btn = $("hp-preview-btn");
if (!btn) return;
if (isOptionPrimary()) {
btn.textContent = "手动下单";
btn.title = "弹窗内确认:按已选合约卖一限价开仓(买期权+对冲永续);非交易所市价单";
} else {
btn.textContent = "计算";
btn.title = "情景测算后再启动";
}
}
function isPoOptionPrimaryPlan(p) {
if (!p || p.plan_type !== "perp_options") return false;
return p.option_primary == 1 || p.option_primary === true || Number(p.option_primary) === 1;
}
function setPoStrategyStatus(kind, planId) {
const el = $("hp-po-strategy-status");
if (!el) return;
el.classList.remove("is-watching", "is-holding", "is-idle");
if (!isOptionPrimary()) {
el.textContent = "";
return;
}
const idPart = planId ? " #" + planId : "";
if (kind === "watching") {
el.textContent = "盯盘中" + idPart;
el.classList.add("is-watching");
} else if (kind === "holding") {
el.textContent = "持仓中" + idPart;
el.classList.add("is-holding");
} else {
el.textContent = "";
el.classList.add("is-idle");
}
}
async function refreshPoStrategyStatus() {
const el = $("hp-po-strategy-status");
if (!el) return;
if (!isOptionPrimary()) {
setPoStrategyStatus("", null);
return;
}
try {
const d = await apiJson("/api/hedge-plan/active");
const uly = (state.underlying || "ETH").toUpperCase();
const rows = (d.plans || []).filter(function (p) {
return isPoOptionPrimaryPlan(p) && String(p.underlying || "").toUpperCase() === uly;
});
if (!rows.length) {
setPoStrategyStatus("idle", null);
return;
}
rows.sort(function (a, b) {
return Number(b.id || 0) - Number(a.id || 0);
});
const p = rows[0];
const st = String(p.status || "");
if (st === "watching") setPoStrategyStatus("watching", p.id);
else if (st === "active" || st === "opening" || st === "partial") setPoStrategyStatus("holding", p.id);
else setPoStrategyStatus("idle", null);
} catch (e) {
/* ignore status refresh errors */
}
}
function syncOoRecommendUI() {
const cur = state.ooRecommend || "";
document.querySelectorAll(".hp-oo-recommend-btn").forEach(function (b) {
const on = (b.getAttribute("data-oo-rec") || "") === cur;
b.classList.toggle("active", on);
b.classList.toggle("is-selected", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
}
function syncOoExpandUI() {
const btn = $("hp-oo-expand-all");
if (!btn) return;
const on = !!state.ooStrikeExpandAll;
btn.classList.toggle("active", on);
btn.classList.toggle("is-selected", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
}
function calcStrikeAskLeverage(strike, ask) {
const k = Number(strike);
const a = Number(ask);
if (!Number.isFinite(k) || k <= 0 || !Number.isFinite(a) || a <= 0) return "—";
return (Math.round((k / a) * 10) / 10).toFixed(1) + "×";
}
/** CoinOX 币本位:卖一为币报价 → 杠杆≈1÷卖一(与期权页一致). */
function calcIndexAskLeverage(ask) {
const a = Number(ask);
if (!(a > 0)) return "—";
return (Math.round((1 / a) * 10) / 10).toFixed(1) + "×";
}
const orderPanelHome = (function () {
const host = document.getElementById("hp-order-panel-host");
return host ? host.parentElement : null;
})();
function orderPanel() {
return $("hp-order-panel");
}
function orderPanelHost() {
return $("hp-order-panel-host");
}
function parkOrderPanel() {
const panel = orderPanel();
const host = orderPanelHost();
if (panel && host && panel.parentElement !== host) host.appendChild(panel);
if (host) {
host.hidden = true;
host.setAttribute("aria-hidden", "true");
if (orderPanelHome && host.parentElement !== orderPanelHome) {
orderPanelHome.appendChild(host);
}
}
if (panel) panel.style.display = "none";
state.orderDialogOpen = false;
const tbody = $("hp-strike-tbody");
if (tbody) {
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.remove("opt-row-selected");
});
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
b.classList.remove("active");
if (b.dataset.origText) b.textContent = b.dataset.origText;
});
}
}
function openOrderDialog(instId) {
const panel = orderPanel();
const host = orderPanelHost();
if (!panel || !host) return false;
if (panel.parentElement !== host) host.appendChild(panel);
if (host.parentElement !== document.body) document.body.appendChild(host);
host.hidden = false;
host.setAttribute("aria-hidden", "false");
panel.style.display = "";
state.orderDialogOpen = true;
const tbody = $("hp-strike-tbody");
if (tbody && instId) {
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === instId);
});
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
if (!b.dataset.origText) b.dataset.origText = "选择";
const on = b.getAttribute("data-inst") === instId;
b.classList.toggle("active", on);
b.textContent = on ? "已选" : b.dataset.origText;
});
}
return true;
}
function closeOrderDialog() {
state.selected = null;
parkOrderPanel();
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
updatePremiumLine();
updatePerpPnlHint();
}
function clearPoSelection() {
state.selected = null;
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
if (state.orderDialogOpen) parkOrderPanel();
updatePremiumLine();
updatePerpPnlHint();
}
function setHpPosTab(tab) {
const allowed = { live: 1, plans: 1, pending: 1, history: 1, stats: 1 };
const next = allowed[tab] ? tab : "live";
state.posTab = next;
document.querySelectorAll(".hp-pos-tab").forEach(function (btn) {
const on = btn.getAttribute("data-hp-pos-tab") === next;
btn.classList.toggle("active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
document.querySelectorAll("[data-hp-pos-pane]").forEach(function (pane) {
const on = pane.getAttribute("data-hp-pos-pane") === next;
pane.classList.toggle("is-active", on);
if (on) pane.removeAttribute("hidden");
else pane.setAttribute("hidden", "");
});
if (next === "live") void loadLivePositions();
else if (next === "plans" || next === "pending") void loadActivePlans();
else if (next === "history") void loadHistory();
else if (next === "stats") void loadStats();
}
function refreshHpPosPanel() {
void loadLivePositions();
void loadActivePlans();
void loadHistory();
if (state.posTab === "stats") void loadStats();
}
function pnlClsNum(v) {
if (v == null || Number.isNaN(Number(v))) return "";
const n = Number(v);
if (n > 0) return "hp-pnl-pos";
if (n < 0) return "hp-pnl-neg";
return "";
}
function fmtSigned(v, d) {
if (v == null || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const s = fmt(n, d == null ? 2 : d);
return n > 0 ? "+" + s : s;
}
function fmtDuration(sec) {
if (sec == null || Number.isNaN(Number(sec))) return "—";
let s = Math.max(0, Math.floor(Number(sec)));
const d = Math.floor(s / 86400);
s -= d * 86400;
const h = Math.floor(s / 3600);
s -= h * 3600;
const m = Math.floor(s / 60);
if (d > 0) return d + "天 " + h + "时" + m + "分";
if (h > 0) return h + "时" + m + "分";
return m + "分";
}
function formatExitLine(ex) {
const e = ex || {};
const mode = String(e.mode || "").toLowerCase() === "target" ? "target" : "profit";
if (mode === "profit") {
return (
"出场 · 盈利金额 " +
(e.profit_target_usdt != null ? fmt(e.profit_target_usdt, 2) + " U" : "—") +
""
);
}
return (
"出场 · 目标位 期权±" +
(e.option_target_points != null ? fmt(e.option_target_points, 0) : "—") +
" / 永续±" +
(e.perp_target_points != null ? fmt(e.perp_target_points, 0) : "—") +
""
);
}
function renderLivePosCard(card) {
const opt = card.option || {};
const perp = card.perp || {};
const combo = card.combo || {};
const exit = card.exit || {};
const optType = String(opt.opt_type || "").toUpperCase() === "P" ? "Put" : "Call";
const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多";
const comboCls = pnlClsNum(combo.net_usdt);
const liqWarn = opt.liq_ok === false;
const perpLiqWarn = perp.liq_ok === false;
const unflat = !!card.unflat || String(card.status || "") === "closed";
const bidTxt =
opt.bid != null
? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "")
: "—";
const bookLab = String(perp.book_side || "") === "asks" ? "卖一" : "买一";
const perpTopTxt =
perp.top_px != null
? fmt(perp.top_px, 2) + (perp.top_sz != null ? "/" + fmt(perp.top_sz, 0) : "")
: "—";
const canEditExit = !unflat;
return (
'" +
(opt.inst_id || "—") +
"" +
'" +
(perp.symbol || "—") +
"" +
'
暂无期权/永续对冲持仓
'; return; } box.innerHTML = cards.map(renderLivePosCard).join(""); box.querySelectorAll(".hp-btn-detail").forEach(function (btn) { btn.addEventListener("click", function () { void showPlanDetail(Number(btn.getAttribute("data-id"))); }); }); box.querySelectorAll(".hp-btn-end").forEach(function (btn) { btn.addEventListener("click", function () { void endActivePlan(Number(btn.getAttribute("data-id"))); }); }); box.querySelectorAll(".hp-btn-exit").forEach(function (btn) { btn.addEventListener("click", function () { openExitModal(btn); }); }); } catch (e) { box.innerHTML = '' + (e.message || e) + "
"; } } /** 手动选约:不自动代选;仅在已选失效(换 Call/Put 或筛掉)时清空. */ function autoMatchPoOption(force) { if (!isOptionPrimary()) return; const want = optTypeForDirection(getDirection()); const sel = state.selected; const stillOk = !force && sel && String(sel.opt_type || "").toUpperCase() === want && matchesMoneyFilter(sel); if (stillOk) return; clearPoSelection(); } function findAtmStrikeFromRows(rows, idx) { if (!rows.length) return null; if (idx == null || Number.isNaN(Number(idx))) return rows[0].strike; let best = rows[0].strike; let bestDist = Math.abs(Number(rows[0].strike) - Number(idx)); rows.forEach(function (row) { const d = Math.abs(Number(row.strike) - Number(idx)); if (d < bestDist || (d === bestDist && Number(row.strike) < Number(best))) { bestDist = d; best = row.strike; } }); return best; } function sliceOoTRows(prepared, idx) { // prepared: [{strike, call, put, callOk, putOk}] if (state.ooStrikeExpandAll || !prepared.length) return prepared; const n = 3; const anchor = Number(idx) || Number(findAtmStrikeFromRows(prepared, idx)) || 0; function nearest(list, count) { return list .slice() .sort(function (a, b) { return Math.abs(Number(a.strike) - anchor) - Math.abs(Number(b.strike) - anchor); }) .slice(0, count); } const byStrike = {}; nearest( prepared.filter(function (r) { return r.callOk; }), n ).forEach(function (r) { byStrike[String(r.strike)] = r; }); nearest( prepared.filter(function (r) { return r.putOk; }), n ).forEach(function (r) { byStrike[String(r.strike)] = r; }); // 已选用腿始终保留,避免折叠后看不到选中项 [state.legA, state.legB].forEach(function (leg) { if (!leg) return; const hit = prepared.find(function (r) { return ( (r.call && r.call.inst_id === leg.inst_id) || (r.put && r.put.inst_id === leg.inst_id) ); }); if (hit) byStrike[String(hit.strike)] = hit; }); return Object.keys(byStrike) .map(function (k) { return byStrike[k]; }) .sort(function (a, b) { return Number(a.strike) - Number(b.strike); }); } 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"].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; } if (tab === "perp_options") { refreshHpPosPanel(); } 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 text = (function () { if (!gates) return ""; const parts = [ "计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"), "测算:" + (gates.can_preview ? "可" : "否"), "开仓:" + (gates.can_start ? "可" : "否"), ]; if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; ")); return parts.join(" · "); })(); const el = $("hp-gate-line"); if (el) el.textContent = text; const inline = $("hp-po-gate-inline"); if (inline) inline.textContent = text; state.canStart = !!(gates && gates.can_start); syncPreviewStartBtn(); } function setPoOrderErr(msg, focusId) { const err = $("hp-po-order-err"); if (err) { if (msg) { err.textContent = msg; err.removeAttribute("hidden"); } else { err.textContent = ""; err.setAttribute("hidden", ""); } } if (focusId && $(focusId)) { try { $(focusId).focus(); $(focusId).classList.add("hp-input-invalid"); setTimeout(function () { if ($(focusId)) $(focusId).classList.remove("hp-input-invalid"); }, 2400); } catch (e) {} } } function syncPreviewStartBtn() { const start = $("hp-preview-start"); if (!start) return; start.disabled = !(state.previewOk && state.canStart); } function openPreviewModal() { const modal = $("hp-preview-modal"); if (modal) modal.hidden = false; } function closePreviewModal() { const modal = $("hp-preview-modal"); if (modal) modal.hidden = true; } function applyBudgetBuffer(raw) { if (raw == null || raw === "") return; const buf = Number(raw); if (Number.isNaN(buf) || buf <= 0) return; state.budgetBuffer = buf; const el = $("hp-oo-buf-ratio"); if (el) el.textContent = fmt(buf, 2); } 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 (acct.trading_usdt != null && acct.trading_usdt !== "") { state.tradingUsdtEquity = Number(acct.trading_usdt); } else if (acct.trading_usdc != null && acct.trading_usdc !== "") { state.tradingUsdtEquity = Number(acct.trading_usdc); } if (acct.trading_usdt_avail != null && acct.trading_usdt_avail !== "") { state.tradingUsdt = Number(acct.trading_usdt_avail); } else if (state.tradingUsdtEquity != null) { state.tradingUsdt = state.tradingUsdtEquity; } else if (acct.trading_usdc != null && acct.trading_usdc !== "") { state.tradingUsdt = Number(acct.trading_usdc); } if (acct.funding_usdt != null && acct.funding_usdt !== "") { state.fundingUsdt = Number(acct.funding_usdt); state.fundingUsdc = Number(acct.funding_usdt); } else if (acct.funding_usdc != null && acct.funding_usdc !== "") { state.fundingUsdc = Number(acct.funding_usdc); state.fundingUsdt = Number(acct.funding_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 !== "") { applyBudgetBuffer(chain.budget_buffer); } const equityTxt = fmt( state.tradingUsdtEquity != null ? state.tradingUsdtEquity : acct.trading_usdt != null ? acct.trading_usdt : acct.trading_usdc, 2 ); const line = label + " · 交易 USDT " + equityTxt + " · 资金 USDT " + fmt(acct.funding_usdt != null ? acct.funding_usdt : acct.funding_usdc, 2) + " · 永续全仓/期权逐仓"; const el = $("hp-opt-bal-line"); if (el) el.textContent = line; const fundEl = $("hp-oo-funding-usdc"); const tradeEl = $("hp-oo-trading-usdc"); if (fundEl) fundEl.textContent = fmt(acct.funding_usdt != null ? acct.funding_usdt : acct.funding_usdc, 2); if (tradeEl) tradeEl.textContent = equityTxt; const poFund = $("hp-po-funding-usdt"); const poTrade = $("hp-po-trading-usdt"); if (poFund) poFund.textContent = fmt(acct.funding_usdt != null ? acct.funding_usdt : acct.funding_usdc, 2); if (poTrade) poTrade.textContent = equityTxt; state.fundingEth = acct.funding_eth != null ? Number(acct.funding_eth) : null; state.tradingEth = acct.trading_eth != null ? Number(acct.trading_eth) : null; state.fundingBtc = acct.funding_btc != null ? Number(acct.funding_btc) : null; state.tradingBtc = acct.trading_btc != null ? Number(acct.trading_btc) : null; autoFillOoSheets(); } function setXferMsg(prefix, text, kind) { const el = $(prefix + "-xfer-msg"); if (!el) return; el.textContent = text || ""; el.classList.toggle("is-err", kind === "err"); el.classList.toggle("is-ok", kind === "ok"); el.classList.toggle("muted", !kind); } function setOoXferMsg(text, kind) { setXferMsg("hp-oo", text, kind); } function setPoXferMsg(text, kind) { setXferMsg("hp-po", text, kind); } function ooXferDir() { const sel = $("hp-oo-xfer-dir"); return (sel && sel.value) || "funding_to_trading"; } function poXferDir() { const sel = $("hp-po-xfer-dir"); return (sel && sel.value) || "funding_to_trading"; } function poXferCcy() { const sel = $("hp-po-xfer-ccy"); return ((sel && sel.value) || "USDT").toUpperCase(); } function xferAvailFor(ccy, fromAcct) { const c = String(ccy || "USDT").toUpperCase(); const from = String(fromAcct || "funding").toLowerCase(); if (c === "USDT") { if (from === "trading") { return Number( state.tradingUsdt != null ? state.tradingUsdt : state.tradingUsdc != null ? state.tradingUsdc : 0 ); } return Number( state.fundingUsdt != null ? state.fundingUsdt : state.fundingUsdc != null ? state.fundingUsdc : 0 ); } if (c === "ETH") { return Number(from === "trading" ? state.tradingEth || 0 : state.fundingEth || 0); } if (c === "BTC") { return Number(from === "trading" ? state.tradingBtc || 0 : state.fundingBtc || 0); } return 0; } async function submitHedgeTransfer(opts) { const prefix = opts.prefix || "hp-oo"; const dir = opts.dir || "funding_to_trading"; const ccy = String(opts.ccy || "USDT").toUpperCase(); const amount = Number(opts.amount || 0); const setMsg = prefix === "hp-po" ? setPoXferMsg : setOoXferMsg; const from = dir === "trading_to_funding" ? "trading" : "funding"; const to = dir === "trading_to_funding" ? "funding" : "trading"; if (!(amount > 0)) { setMsg("请输入有效数量", "err"); return; } setMsg("划转中…", null); try { const d = await apiJson("/api/hedge-plan/transfer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ccy: ccy, from: from, to: to, amount: amount }), }); if (!d.ok) { setMsg(d.msg || "划转失败", "err"); return; } setMsg("划转成功", "ok"); const amtEl = $(prefix + "-xfer-amount"); if (amtEl) amtEl.value = ""; await loadChain(); } catch (e) { setMsg(e.message || "划转失败", "err"); } } async function submitOoUsdcTransfer(amount) { return submitHedgeTransfer({ prefix: "hp-oo", dir: ooXferDir(), ccy: "USDT", amount: amount }); } function setSellCoinMsg(text, kind) { const el = $("hp-sell-coin-msg"); if (!el) return; const show = !!(text || "").trim(); el.hidden = !show; el.textContent = text || ""; el.classList.toggle("is-err", kind === "err"); el.classList.toggle("is-ok", kind === "ok"); el.classList.toggle("muted", !kind); } function setSellCoinVisible(visible, meta) { const btn = $("hp-sell-coin-btn"); if (!btn) return; const on = !!visible; btn.hidden = !on; btn.disabled = !on; if (on && meta) { const uly = String(meta.underlying || state.underlying || "ETH").toUpperCase(); const avail = meta.available != null ? Number(meta.available) : null; const minSz = meta.min_sz != null ? Number(meta.min_sz) : null; let tip = "市价卖出交易账户全部可用 " + uly + " 换回 USDT"; if (avail != null && !Number.isNaN(avail)) tip += " · 可用 " + avail; if (minSz != null && !Number.isNaN(minSz)) tip += " · 最小 " + minSz; btn.title = tip; } if (!on) setSellCoinMsg("", null); } async function refreshSellCoinVisibility() { const uly = String(state.underlying || "ETH").toUpperCase(); try { const d = await apiJson( "/api/hedge-plan/sell-coin-status?underlying=" + encodeURIComponent(uly) ); if (!d || !d.ok) { setSellCoinVisible(false); return; } setSellCoinVisible(!!d.sellable, d); } catch (_) { setSellCoinVisible(false); } } async function submitSellCoin() { const uly = String(state.underlying || "ETH").toUpperCase(); const btn = $("hp-sell-coin-btn"); if (!btn || btn.hidden) return; if (!window.confirm("确认市价卖出交易账户全部可用 " + uly + " 换回 USDT?")) return; setSellCoinMsg("卖回中…", null); btn.disabled = true; try { const d = await apiJson("/api/hedge-plan/sell-coin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ underlying: uly }), }); if (!d.ok) { setSellCoinMsg(d.msg || "卖回失败", "err"); await refreshSellCoinVisibility(); return; } const sold = d.sell && d.sell.coin_sold != null ? "已卖 " + d.sell.coin_sold + " " + uly : d.msg || "卖回成功"; setSellCoinMsg(sold, "ok"); await loadChain(); void loadLivePositions(); await refreshSellCoinVisibility(); } catch (e) { setSellCoinMsg(e.message || "卖回失败", "err"); await refreshSellCoinVisibility(); } } 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 normalizeOptCP(ot) { const u = String(ot || "").toUpperCase(); if (u.indexOf("C") === 0) return "C"; if (u.indexOf("P") === 0) return "P"; return ""; } function ooSizeModeLabel(mode) { if (mode === "long_bias") return "做多"; if (mode === "short_bias") return "做空"; if (mode === "split_budget") return "均分"; return "同张数"; } 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 ratio = Number(state.ooBiasRatio); if (!(ratio > 0) || !(ratio < 1)) ratio = 0.7; const splitBy = state.ooBiasSplitBy === "sheets" ? "sheets" : "budget"; const pair = costA + costB; const nPair = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0; const nSame = Math.min( capByDepth(nPair, state.legA && state.legA.ask_sz), capByDepth(nPair, state.legB && state.legB.ask_sz) ); let nA = 0; let nB = 0; if (mode === "long_bias" || mode === "short_bias") { const aCP = normalizeOptCP(state.legA && state.legA.opt_type); const bCP = normalizeOptCP(state.legB && state.legB.opt_type); if (!((aCP === "C" && bCP === "P") || (aCP === "P" && bCP === "C"))) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "做多/做空需一腿 Call、一腿 Put" }; } const callIsA = aCP === "C"; const majorIsCall = mode === "long_bias"; let nCall = 0; let nPut = 0; if (splitBy === "sheets") { // 总张数 = 同张数两侧合计(每腿 n → 共 2n),再按比例拆 const total = nSame * 2; if (total < 2) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "同张数总规模不足 2,无法按比例拆分" }; } let majorN = Math.round(total * ratio); majorN = Math.max(1, Math.min(majorN, total - 1)); const minorN = total - majorN; nCall = majorIsCall ? majorN : minorN; nPut = majorIsCall ? minorN : majorN; } else { const majBudget = budget * ratio; const minBudget = budget * (1 - ratio); const costCall = callIsA ? costA : costB; const costPut = callIsA ? costB : costA; if (majorIsCall) { nCall = Math.floor(majBudget / costCall + 1e-12); nPut = Math.floor(minBudget / costPut + 1e-12); } else { nPut = Math.floor(majBudget / costPut + 1e-12); nCall = Math.floor(minBudget / costCall + 1e-12); } } nA = callIsA ? nCall : nPut; nB = callIsA ? nPut : nCall; nA = capByDepth(nA, state.legA && state.legA.ask_sz); nB = capByDepth(nB, state.legB && state.legB.ask_sz); } else if (mode === "split_budget") { const half = budget / 2; nA = Math.floor(half / costA + 1e-12); nB = Math.floor(half / costB + 1e-12); nA = capByDepth(nA, state.legA && state.legA.ask_sz); nB = capByDepth(nB, state.legB && state.legB.ask_sz); } else { nA = nSame; nB = nSame; } 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); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function syncOoCloseModeUI() { const enabled = !!state.ooCloseModeEnabled; const row = $("hp-oo-close-mode-row"); if (row) row.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); b.classList.toggle("is-selected", on); b.setAttribute("aria-pressed", on ? "true" : "false"); }); } function updateOoBudgetLine(extra) { const line = $("hp-oo-budget-line"); if (!line) return; const b = resolveOoBudget(); const sizeLabel = ooSizeModeLabel(state.ooSheetsMode); const closeLabel = !state.ooCloseModeEnabled ? "" : state.ooCloseMode === "hold_expiry" ? "到期平" : "全平"; const parts = ["可用 " + fmt(b.budget, 2) + "U", sizeLabel]; if (closeLabel) parts.push(closeLabel); if (extra && extra.msg) parts.push(extra.msg); else if (b.msg) parts.push(b.msg); line.textContent = parts.join(" · "); line.title = "对冲预算=min(交易×" + fmt(b.buf, 2) + ", 单笔) · 缓冲 HEDGE_PLAN_BUDGET_BUFFER" + (b.tradingCap != null ? " · 交易×缓冲 " + fmt(b.tradingCap, 2) : "") + (b.tradeCap != null ? " · 单笔 " + fmt(b.tradeCap, 2) : ""); 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"; } if (d.oo_bias_split_by != null) { state.ooBiasSplitBy = d.oo_bias_split_by === "sheets" ? "sheets" : "budget"; } if (d.oo_bias_ratio != null && Number(d.oo_bias_ratio) > 0 && Number(d.oo_bias_ratio) < 1) { state.ooBiasRatio = Number(d.oo_bias_ratio); } if (d.budget_buffer != null) applyBudgetBuffer(d.budget_buffer); syncOoCloseModeUI(); setGateLine(d); if (state.mode === "options_options") autoFillOoSheets(); } catch (e) { setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false }); } } async function loadMarket() { const dir = getDirection(); // 永续行情按实例模式永续标的(跨币=ETH);期权链仍用 optionUnderly const mktBase = state.mode === "perp_options" ? state.perpUnderly || state.underlying : state.underlying; const d = await apiJson( "/api/hedge-plan/market?base=" + encodeURIComponent(mktBase) + "&direction=" + encodeURIComponent(dir) + "&option_primary=" + (isOptionPrimary() ? "1" : "0") ); 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 markEl = $("hp-po-mark"); if (markEl) { markEl.textContent = "标记 " + fmt(d.mark, 2) + " · 永续" + (d.perp_direction === "short" ? "空" : "多") + " · 面值 " + fmt(d.contract_size, 4); } const mIdx = d.index_px != null ? Number(d.index_px) : NaN; if (Number.isFinite(mIdx) && mIdx > 0) { const idxLine = $("hp-index-line"); if (idxLine && (!state.chain || !state.chain.index_px)) { idxLine.textContent = "指数 " + fmt(mIdx, 2); } const ooIdx = $("hp-oo-index"); if (ooIdx && (!state.chain || !state.chain.index_px)) { ooIdx.textContent = "指数 " + fmt(mIdx, 2); } } const quoteHtml = "卖一 " + fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + " · 面值 " + fmt(d.contract_size, 4) + " · 精度 " + amtPrec + " 位" + (d.perp_direction ? " · 永续方向 " + (d.perp_direction === "short" ? "空" : "多") : ""); const q = $("hp-perp-quote"); if (q) { q.innerHTML = quoteHtml; // 弹窗内行情已压缩到 mark 行,避免重复挤占 if (q.closest && q.closest(".hp-po-order-dialog")) q.setAttribute("hidden", ""); else q.removeAttribute("hidden"); } 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 (isOptionPrimary()) { const sized = currentOpSizing(); if (sized) { sz.innerHTML = formatOpSizingHtml(sized, amtPrec); } else { sz.textContent = "填写权利金或张数并选择期权后显示定仓(默认比例2:期权÷2=永续)"; } } else if (d.full_margin_sizing) { const s = d.full_margin_sizing; sz.innerHTML = "全仓建议 " + fmt(d.suggest_contracts, amtPrec) + " 张 · 保证金 " + fmt(s.margin_capital, 2) + " × " + s.leverage + "x · 名义 " + fmt(s.notional_value, 2) + " USDT"; } else { sz.textContent = "非全仓时请手动填张数;永期开仓需全仓"; } } const entry = $("hp-entry"); if (!isOptionPrimary() && entry && d.entry_ref && !entry.value) entry.value = d.entry_ref; if (!isOptionPrimary() && 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(); updatePoOpenSummary(); } function updatePerpPnlHint() { const el = $("hp-perp-pnl-line"); if (!el) return; if (isOptionPrimary()) { if (state.exitMode === "profit") { const p = numInput("hp-profit-target", NaN); el.innerHTML = p > 0 ? '目标净利 ' + fmt(p, 2) + ' USDT (扣开平双边手续费后)' : '填写目标盈利金额(扣买入+卖出手续费后的组合净利)'; return; } const n = numInput("hp-opt-target-pts", NaN); const m = numInput("hp-perp-target-pts", NaN); const k = state.selected && Number(state.selected.strike); if (!(k > 0) || (!(n >= 0) && !(m >= 0))) { el.innerHTML = '选用期权并填目标点数后显示 K±N 出场参考'; return; } const dir = getDirection(); const optT = n >= 0 ? (dir === "short" ? k - n : k + n) : null; const perpT = m >= 0 ? (dir === "short" ? k - m : k + m) : null; el.innerHTML = "期权目标指数 " + (optT != null ? fmt(optT, 2) : "—") + " · 永续目标指数 " + (perpT != null ? fmt(perpT, 2) : "—") + ' (相对K;须买一流动性且扣费净利>0)'; 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 = getDirection(); if (!(entry > 0) || !(contracts > 0) || !(cs > 0)) { el.innerHTML = '填开仓价与张数后,输入止盈/止损可看盈亏'; return; } function pnlAt(exitPx) { const coins = contracts * cs; if (dir === "short") return (entry - exitPx) * coins; return (exitPx - entry) * coins; } function chip(lab, px) { if (!(px > 0)) { return '' + lab + " —"; } const p = pnlAt(px); const cls = p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg"; const sign = p >= 0 ? "+" : ""; return ( '' + lab + ' ' + sign + fmt(p, 2) + "" ); } el.innerHTML = chip("止盈", tp) + chip("止损", sl); } function fillExpSelect(sel, chain) { if (!sel) return; const prev = sel.value; // 手动选约:到期列表不过滤(与期权页一致,默认最近一期);条件单 hidden 字段 min-hours=0 const minH = sel.id === "hp-exp-select" ? numInput("hp-min-hours", 0) : 0; sel.innerHTML = ''; let firstOk = null; let skippedNear = 0; (chain.expiries || []).forEach(function (e) { const h = hoursFromExpMs(e.exp_time); if (minH > 0 && h != null && h < minH) { skippedNear += 1; return; } const opt = document.createElement("option"); opt.value = String(e.exp_time); const dt = new Date(Number(e.exp_time) < 1e12 ? Number(e.exp_time) * 1000 : Number(e.exp_time)); opt.textContent = dt.toLocaleString() + (h != null ? " · " + fmt(h, 1) + "h" : ""); sel.appendChild(opt); if (!firstOk) firstOk = e; }); if (prev) sel.value = prev; if (!sel.value && firstOk) { sel.value = String(firstOk.exp_time); } if (skippedNear > 0 && minH > 0) { const tip = document.createElement("option"); tip.disabled = true; tip.textContent = "(已隐藏 " + skippedNear + " 个不足 " + minH + "h 的到期)"; sel.appendChild(tip); } } async function loadChain() { // 拉完整链(按 CHAIN_MAX_DTE);永期「最低剩余小时」只在左侧到期下拉里过滤,不影响期期看到明天到期 const url = "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying); const d = await apiJson(url); state.chain = d; const idx = $("hp-index-line"); if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2); const ooIdx = $("hp-oo-index"); if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2); setOptionsBalance(d); fillExpSelect($("hp-exp-select"), d); fillExpSelect($("hp-oo-exp-select"), d); renderListStrikes(); renderTStrikes(); if (d.index_px) { // 盈亏比默认2,不随指数自动改写 if ($("hp-profit-rr") && !$("hp-profit-rr").value) { $("hp-profit-rr").value = "2"; } } void refreshSellCoinVisibility(); } 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; const m = (c.moneyness || "").toLowerCase(); if (!isOptionPrimary() && m === "otm") { alert("永期保险腿须为实值或平值,不可选虚值"); return; } if (isOptionPrimary() && !matchesMoneyFilter(c)) { alert("不符合当前筛选(全部/实值/虚值),请切换筛选后再选"); return; } state.selected = c; const el = $("hp-sel-inst"); if (el) el.textContent = c.inst_id; updatePremiumLine(); if (isOptionPrimary()) { // 手动下单以张数为主:选约后若未填张数则默认 1 if (!(orderSheets() > 0)) { if ($("hp-sheets")) $("hp-sheets").value = "1"; if ($("hp-order-sheets")) $("hp-order-sheets").value = "1"; } else { syncSheetsInputs("hp-order-sheets"); if (!(numInput("hp-order-sheets", 0) > 0)) syncSheetsInputs("hp-sheets"); } updatePerpPnlHint(); updatePoOpenSummary(); } openOrderDialog(c.inst_id); void loadMarket(); } function renderListStrikes() { const tbody = $("hp-strike-tbody"); if (!tbody) return; const want = optTypeForDirection(getDirection()); const exp = currentExp("hp-exp-select"); const prevInst = state.selected && state.selected.inst_id; const cols = isOptionPrimary() ? 7 : 6; tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '" +
(c.inst_id || "") +
'" +
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();
syncOoPickHighlight();
}
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) +
" 币";
}
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) {
let next = (uly || "ETH").toUpperCase();
// 永期:标的由 HEDGE_PO_MODE 锁定,页内不可切换
if (state.mode === "perp_options") {
next = state.optionUnderly || next;
if (!forceReload && next === state.underlying) return;
}
const changed = next !== state.underlying;
state.underlying = next;
syncUnderlyingUI();
if (!changed && !forceReload) return;
if (state.orderDialogOpen) closeOrderDialog();
else state.selected = null;
state.legA = null;
state.legB = null;
state.ooRecommend = null;
syncOoRecommendUI();
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-profit-rr")) $("hp-profit-rr").value = "2";
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 = $("hp-result-tbody");
const summary = $("hp-preview-summary");
const midTh = $("hp-preview-mid-th");
const title = $("hp-preview-title");
state.previewOk = false;
state.previewPlanType = isOo ? "options_options" : "perp_options";
syncPreviewStartBtn();
if (midTh) midTh.textContent = isOo ? "腿盈亏" : "永续/腿盈亏";
if (title) title.textContent = isOo ? "情景测算 · 期期" : "情景测算 · 永期";
if (summary) summary.textContent = "";
if (tbody) tbody.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 += "" + legStatusLabel(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.posTab === "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