Add HEDGE_PO_MODE instance modes with shared sim/live path.

Lock one mode per instance/subaccount, cross-asset BTC option/ETH perp sizing, combo-U exit, and ship docs/tests/audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 20:05:25 +08:00
parent ab7a2309ad
commit 5d161b4220
18 changed files with 1215 additions and 115 deletions
+106 -35
View File
@@ -18,11 +18,25 @@
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: root.getAttribute("data-default-underly") || "ETH",
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
@@ -41,9 +55,9 @@
ooCloseMode: "close_all",
direction: "long",
optionPrimary: root.getAttribute("data-option-primary") !== "0",
exitMode: "profit", // profit=盈利金额出场 | target=目标位出场
exitMode: "profit", // 实例模式强制组合盈利金额(U)
opLevTouched: false,
opRatioTouched: false,
opRatioTouched: crossAsset, // 跨币锁比例,视为已设定
perpLevTouched: false,
tradingUsdc: null,
fundingUsdc: null,
@@ -58,6 +72,32 @@
previewPlanType: null,
};
function lockPoUnderlyingUI() {
const opt = state.optionUnderly || "ETH";
document.querySelectorAll(".hp-uly-btn").forEach(function (b) {
const uly = (b.getAttribute("data-uly") || "").toUpperCase();
const on = uly === opt;
b.classList.toggle("active", on);
b.disabled = true;
b.title = "由 HEDGE_PO_MODE 锁定(" + (root.getAttribute("data-po-mode-label") || poMode) + ")";
});
const badge = $("hp-po-mode-badge");
if (badge) {
const perp = state.perpUnderly || opt;
badge.textContent =
(root.getAttribute("data-po-mode-label") || poMode) +
(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";
}
@@ -421,7 +461,7 @@
function computeOpSizing(ask, ctMult, sheetsOverride) {
const budget = numInput("hp-premium-budget", 0);
const ratio = numInput("hp-opt-perp-ratio", 2);
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);
@@ -443,16 +483,32 @@
if (!(sheets > 0)) return null;
eth = Math.round(sheets * ct * 100) / 100;
}
const perpEth = eth / ratio;
const contracts = perpEth / cs;
// 跨币:期权币×期权指数×(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: perpEth,
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,
};
}
@@ -483,24 +539,32 @@
function formatOpSizingHtml(sized, amtPrec) {
if (!sized) return "";
const coin = (state.underlying || "ETH").toUpperCase();
const prec = amtPrec != null ? amtPrec : coin === "BTC" ? 4 : 2;
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 =
"期权 <strong>" +
fmt(sized.eth_qty, 2) +
"</strong> " +
coin +
optCoin +
" / " +
sized.sheets +
" 张 → 永续 <strong>" +
fmt(sized.perp_qty, 4) +
"</strong> " +
coin +
perpCoin +
" (" +
fmt(sized.contracts, prec) +
" 张) · 比例 " +
fmt(sized.ratio, 2)
);
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) {
@@ -1881,9 +1945,12 @@
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(state.underlying) +
encodeURIComponent(mktBase) +
"&direction=" +
encodeURIComponent(dir) +
"&option_primary=" +
@@ -2523,7 +2590,12 @@
}
function setUnderlying(uly, forceReload) {
const next = (uly || "ETH").toUpperCase();
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();
@@ -2813,6 +2885,9 @@
});
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
b.addEventListener("click", function () {
if (b.classList.contains("hp-uly-btn") && state.mode === "perp_options") {
return; // 永期标的由实例模式锁定
}
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
});
});
@@ -3011,7 +3086,8 @@
}
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
b.addEventListener("click", function () {
state.exitMode = b.getAttribute("data-exit") === "target" ? "target" : "profit";
// 实例模式强制组合盈利金额(U)出场
state.exitMode = "profit";
syncExitModeUI();
updatePerpPnlHint();
});
@@ -3763,23 +3839,14 @@
setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets");
throw new Error("请选择期权并填写张数(≥1)");
}
const exitMode = state.exitMode === "target" ? "target" : "profit";
const exitMode = "profit";
let optPts = 0;
let perpPts = 0;
let profitTarget = 0;
if (exitMode === "profit") {
profitTarget = numInput("hp-profit-target", NaN);
if (!(profitTarget > 0)) {
setPoOrderErr("请填写目标盈利金额(USDT,扣双边手续费后净利)", "hp-profit-target");
throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
}
} else {
optPts = numInput("hp-opt-target-pts", NaN);
perpPts = numInput("hp-perp-target-pts", NaN);
if (!(optPts > 0) || !(perpPts > 0)) {
setPoOrderErr("请填写期权/永续目标位点数(须大于0)", !(optPts > 0) ? "hp-opt-target-pts" : "hp-perp-target-pts");
throw new Error("请填写期权/永续目标位点数(须大于0)");
}
profitTarget = numInput("hp-profit-target", NaN);
if (!(profitTarget > 0)) {
setPoOrderErr("请填写目标盈利金额(USDT,组合净利折U)", "hp-profit-target");
throw new Error("请填写目标盈利金额(USDT,组合净利折U)");
}
if (!(state.market && state.market.exchange_symbol)) {
setPoOrderErr("永续行情未就绪,请先点「刷新行情」");
@@ -3805,9 +3872,12 @@
market_order: 1,
watch_entry: 0,
exit_mode: exitMode,
underlying: state.underlying,
po_mode: state.poMode,
underlying: state.optionUnderly || state.underlying,
option_underlying: state.optionUnderly || state.underlying,
perp_underlying: state.perpUnderly || state.underlying,
direction: getDirection(),
entry: entry,
entry: Number((state.market && state.market.mark) || entry),
contracts: sized.contracts,
sheets: sized.sheets,
contract_size: (state.market && state.market.contract_size) || 0.01,
@@ -3818,10 +3888,10 @@
ask: state.selected.ask,
index_px: entry,
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
leverage: defaultPerpLeverage(state.underlying),
leverage: defaultPerpLeverage(state.perpUnderly || state.underlying),
option_leverage: 0,
premium_budget: Math.max(premEst / 0.95, premEst),
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
option_perp_ratio: numInput("hp-opt-perp-ratio", lockedRatio),
option_target_points: optPts || 0,
perp_target_points: perpPts || 0,
profit_target_usdt: profitTarget || 0,
@@ -3929,6 +3999,7 @@
if (state.tab === "perp_options") void refreshPoStrategyStatus();
}
lockPoUnderlyingUI();
syncTabUI();
syncUnderlyingUI();
syncMoneyUI();