feat(hedge): add option-primary mode for perp+options plans
Add UI switch for Call+short/Put+long, premium x0.95 sizing, option-first open, and K+/-points exits with fee-aware net PnL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+399
-77
@@ -37,6 +37,9 @@
|
||||
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
|
||||
ooCloseMode: "close_all",
|
||||
direction: "long",
|
||||
optionPrimary: false,
|
||||
opLevTouched: false,
|
||||
opRatioTouched: false,
|
||||
tradingUsdc: null,
|
||||
fundingUsdc: null,
|
||||
tradeBudgetUsdc: null,
|
||||
@@ -147,9 +150,20 @@
|
||||
}
|
||||
|
||||
function matchesMoneyFilter(c) {
|
||||
// 永期:仅实值/平值(禁虚值)
|
||||
const f = state.moneyFilter || "itm";
|
||||
const m = (c.moneyness || "").toLowerCase();
|
||||
if (!isOptionPrimary() && f === "otm") return false;
|
||||
if (isOptionPrimary()) {
|
||||
const idx = indexPx();
|
||||
const interval = numInput("hp-strike-interval", 15);
|
||||
if (idx && interval > 0 && Math.abs(Number(c.strike) - idx) > interval + 1e-9) {
|
||||
return false;
|
||||
}
|
||||
const ask = Number(c.ask || 0);
|
||||
const minLev = numInput("hp-opt-leverage", f === "otm" ? 200 : 100);
|
||||
const levFloor = f === "otm" ? Math.max(minLev, 180) : minLev;
|
||||
if (idx && ask > 0 && levFloor > 0 && idx / ask < levFloor) return false;
|
||||
}
|
||||
if (f === "itm") return m === "itm" || m === "atm";
|
||||
if (f === "atm") return m === "atm";
|
||||
if (f === "otm") return m === "otm";
|
||||
@@ -226,10 +240,138 @@
|
||||
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 || "itm";
|
||||
if (f === "otm") return "otm";
|
||||
if (f === "atm") return "atm";
|
||||
return "itm";
|
||||
}
|
||||
|
||||
function applyOpDefaultsFromMoney(force) {
|
||||
const kind = opMoneyKind();
|
||||
const levEl = $("hp-opt-leverage");
|
||||
const ratioEl = $("hp-opt-perp-ratio");
|
||||
if (levEl && (force || !state.opLevTouched)) {
|
||||
levEl.value = kind === "otm" ? "200" : "100";
|
||||
}
|
||||
if (ratioEl && (force || !state.opRatioTouched)) {
|
||||
ratioEl.value = kind === "otm" ? "4" : "2";
|
||||
}
|
||||
}
|
||||
|
||||
function syncOptionPrimaryUI() {
|
||||
const on = isOptionPrimary();
|
||||
document.querySelectorAll(".hp-po-mode").forEach(function (b) {
|
||||
const v = b.getAttribute("data-option-primary") === "1";
|
||||
b.classList.toggle("is-selected", v === on);
|
||||
b.classList.toggle("active", v === on);
|
||||
});
|
||||
const ins = $("hp-po-fields-insurance");
|
||||
const op = $("hp-po-fields-option-primary");
|
||||
if (ins) {
|
||||
ins.classList.toggle("hidden", on);
|
||||
if (on) ins.setAttribute("hidden", "hidden");
|
||||
else ins.removeAttribute("hidden");
|
||||
}
|
||||
if (op) {
|
||||
op.classList.toggle("hidden", !on);
|
||||
if (!on) op.setAttribute("hidden", "hidden");
|
||||
else op.removeAttribute("hidden");
|
||||
}
|
||||
const otmBtn = document.querySelector(".hp-money-otm");
|
||||
if (otmBtn) {
|
||||
otmBtn.classList.toggle("hidden", !on);
|
||||
if (!on) otmBtn.setAttribute("hidden", "hidden");
|
||||
else otmBtn.removeAttribute("hidden");
|
||||
}
|
||||
if (!on && state.moneyFilter === "otm") {
|
||||
state.moneyFilter = "itm";
|
||||
syncMoneyUI();
|
||||
}
|
||||
if (on) applyOpDefaultsFromMoney(false);
|
||||
const title = $("hp-po-card-title");
|
||||
if (title) title.textContent = on ? "执行参数" : "永续";
|
||||
const rightQ = $("hp-po-perp-quote-right");
|
||||
if (rightQ) {
|
||||
rightQ.classList.toggle("hidden", !on);
|
||||
if (!on) rightQ.setAttribute("hidden", "hidden");
|
||||
else rightQ.removeAttribute("hidden");
|
||||
}
|
||||
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 (dirShort) dirShort.title = on ? "做空=买Put+永续多" : "做空永续";
|
||||
}
|
||||
|
||||
function setOptionPrimary(on, forceReload) {
|
||||
const next = !!on;
|
||||
const changed = next !== isOptionPrimary();
|
||||
state.optionPrimary = next;
|
||||
if (changed) {
|
||||
state.opLevTouched = false;
|
||||
state.opRatioTouched = false;
|
||||
state.selected = null;
|
||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||
}
|
||||
syncOptionPrimaryUI();
|
||||
if (!changed && !forceReload) return;
|
||||
void loadMarket().then(function () {
|
||||
return loadChain();
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
const budget = numInput("hp-premium-budget", 0);
|
||||
const ratio = numInput("hp-opt-perp-ratio", 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 (!(budget > 0) || !(a > 0) || !(ct > 0) || !(ratio > 0) || !(cs > 0)) {
|
||||
return null;
|
||||
}
|
||||
let eth = Math.floor((usable / a) * 100 + 1e-12) / 100;
|
||||
if (!(eth > 0)) return null;
|
||||
let sheets = Math.floor(eth / ct + 1e-12);
|
||||
if (!(sheets > 0)) return null;
|
||||
eth = Math.round(sheets * ct * 100) / 100;
|
||||
const perpEth = eth / ratio;
|
||||
const contracts = perpEth / cs;
|
||||
return {
|
||||
usable: usable,
|
||||
eth_qty: eth,
|
||||
sheets: sheets,
|
||||
contracts: contracts,
|
||||
premium_est: a * sheets * ct,
|
||||
ratio: ratio,
|
||||
};
|
||||
}
|
||||
|
||||
function syncUnderlyingUI() {
|
||||
const uly = state.underlying || "ETH";
|
||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||||
@@ -728,7 +870,9 @@
|
||||
"/api/hedge-plan/market?base=" +
|
||||
encodeURIComponent(state.underlying) +
|
||||
"&direction=" +
|
||||
encodeURIComponent(dir)
|
||||
encodeURIComponent(dir) +
|
||||
"&option_primary=" +
|
||||
(isOptionPrimary() ? "1" : "0")
|
||||
);
|
||||
state.market = d;
|
||||
setGateLine(d.gates);
|
||||
@@ -738,20 +882,26 @@
|
||||
const amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4;
|
||||
const markEl = $("hp-po-mark");
|
||||
if (markEl) markEl.textContent = "标记 " + fmt(d.mark, 2);
|
||||
const quoteHtml =
|
||||
"可用 <strong>" +
|
||||
fmt(d.available_usdt, 2) +
|
||||
"</strong> USDT · 卖一 " +
|
||||
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 =
|
||||
"可用 <strong>" +
|
||||
fmt(d.available_usdt, 2) +
|
||||
"</strong> USDT · 卖一 " +
|
||||
fmt(d.ask, 2) +
|
||||
" · 买一 " +
|
||||
fmt(d.bid, 2) +
|
||||
" · 面值 " +
|
||||
fmt(d.contract_size, 4) +
|
||||
" · 精度 " +
|
||||
amtPrec +
|
||||
" 位";
|
||||
if (q) q.innerHTML = quoteHtml;
|
||||
const rightQ = $("hp-po-perp-quote-right");
|
||||
if (rightQ && isOptionPrimary()) {
|
||||
rightQ.innerHTML = "永续行情 · " + quoteHtml;
|
||||
}
|
||||
const contractsInput = $("hp-contracts");
|
||||
if (contractsInput) {
|
||||
@@ -760,7 +910,25 @@
|
||||
}
|
||||
const sz = $("hp-sizing-line");
|
||||
if (sz) {
|
||||
if (d.full_margin_sizing) {
|
||||
if (isOptionPrimary()) {
|
||||
const sized =
|
||||
state.selected &&
|
||||
computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01);
|
||||
if (sized) {
|
||||
sz.innerHTML =
|
||||
"执行预算 <strong>" +
|
||||
fmt(sized.usable, 2) +
|
||||
"</strong> · 期权 ETH <strong>" +
|
||||
fmt(sized.eth_qty, 2) +
|
||||
"</strong> / " +
|
||||
sized.sheets +
|
||||
" 张 · 永续 <strong>" +
|
||||
fmt(sized.contracts, amtPrec) +
|
||||
"</strong> 张";
|
||||
} else {
|
||||
sz.textContent = "填写权利金并选用期权后显示定仓(权利金×0.95,ETH两位小数)";
|
||||
}
|
||||
} else if (d.full_margin_sizing) {
|
||||
const s = d.full_margin_sizing;
|
||||
sz.innerHTML =
|
||||
"全仓建议 <strong>" +
|
||||
@@ -777,8 +945,8 @@
|
||||
}
|
||||
}
|
||||
const entry = $("hp-entry");
|
||||
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
|
||||
if (contractsInput && d.suggest_contracts != null && !contractsInput.value) {
|
||||
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");
|
||||
@@ -789,6 +957,25 @@
|
||||
function updatePerpPnlHint() {
|
||||
const el = $("hp-perp-pnl-line");
|
||||
if (!el) return;
|
||||
if (isOptionPrimary()) {
|
||||
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 = '<span class="muted">选用期权并填目标点数后显示 K±N 出场参考</span>';
|
||||
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) : "—") +
|
||||
' <span class="muted">(相对K;期权目标需买一且扣费净利>0)</span>';
|
||||
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);
|
||||
@@ -828,17 +1015,23 @@
|
||||
function fillExpSelect(sel, chain) {
|
||||
if (!sel) return;
|
||||
const prev = sel.value;
|
||||
const isPoSel = sel.id === "hp-exp-select";
|
||||
const minH = isPoSel && isOptionPrimary() ? numInput("hp-min-hours", 36) : 0;
|
||||
sel.innerHTML = '<option value="">选择到期日</option>';
|
||||
let firstOk = null;
|
||||
(chain.expiries || []).forEach(function (e) {
|
||||
const h = hoursFromExpMs(e.exp_time);
|
||||
if (minH > 0 && h != null && h < minH) return;
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(e.exp_time);
|
||||
const dt = new Date(Number(e.exp_time));
|
||||
opt.textContent = dt.toLocaleString();
|
||||
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 && chain.expiries && chain.expiries[0]) {
|
||||
sel.value = String(chain.expiries[0].exp_time);
|
||||
if (!sel.value && firstOk) {
|
||||
sel.value = String(firstOk.exp_time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,10 +1072,14 @@
|
||||
function pickContract(c) {
|
||||
if (!c) return;
|
||||
const m = (c.moneyness || "").toLowerCase();
|
||||
if (m === "otm") {
|
||||
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;
|
||||
@@ -896,6 +1093,12 @@
|
||||
});
|
||||
}
|
||||
updatePremiumLine();
|
||||
if (isOptionPrimary()) {
|
||||
const sized = computeOpSizing(c.ask, c.ct_mult || 0.01);
|
||||
if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets);
|
||||
void loadMarket();
|
||||
updatePerpPnlHint();
|
||||
}
|
||||
}
|
||||
|
||||
function renderListStrikes() {
|
||||
@@ -1280,29 +1483,62 @@
|
||||
};
|
||||
} else {
|
||||
if (!state.selected) throw new Error("请选用期权腿");
|
||||
const mSel = (state.selected.moneyness || "").toLowerCase();
|
||||
if (mSel === "otm") 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: getDirection(),
|
||||
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: indexPx() || entry,
|
||||
};
|
||||
if (isOptionPrimary()) {
|
||||
const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01);
|
||||
if (!sized) throw new Error("请填写权利金并确认卖一有效");
|
||||
const optPts = numInput("hp-opt-target-pts", NaN);
|
||||
const perpPts = numInput("hp-perp-target-pts", NaN);
|
||||
if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)");
|
||||
const exp = currentExp("hp-exp-select");
|
||||
body = {
|
||||
plan_type: "perp_options",
|
||||
option_primary: true,
|
||||
direction: getDirection(),
|
||||
entry: indexPx() || Number((state.market && state.market.mark) || 0),
|
||||
contracts: sized.contracts,
|
||||
sheets: sized.sheets,
|
||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||
opt_type: state.selected.opt_type,
|
||||
strike: state.selected.strike,
|
||||
ct_mult: state.selected.ct_mult || 0.01,
|
||||
ask: state.selected.ask,
|
||||
index_px: indexPx() || 0,
|
||||
premium_budget: numInput("hp-premium-budget", 0),
|
||||
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
||||
option_target_points: optPts,
|
||||
perp_target_points: perpPts,
|
||||
strike_interval: numInput("hp-strike-interval", 15),
|
||||
min_option_hours: numInput("hp-min-hours", 36),
|
||||
option_leverage: numInput("hp-opt-leverage", 100),
|
||||
leverage: numInput("hp-perp-leverage", 100),
|
||||
moneyness: opMoneyKind(),
|
||||
hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null,
|
||||
};
|
||||
} else {
|
||||
const mSel = (state.selected.moneyness || "").toLowerCase();
|
||||
if (mSel === "otm") 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: getDirection(),
|
||||
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: indexPx() || entry,
|
||||
};
|
||||
}
|
||||
}
|
||||
const d = await apiJson("/api/hedge-plan/preview", {
|
||||
method: "POST",
|
||||
@@ -1312,7 +1548,18 @@
|
||||
setGateLine(d.gates);
|
||||
const s = d.summary || {};
|
||||
if (summary) {
|
||||
if (d.plan_type === "perp_options") {
|
||||
if (d.plan_type === "perp_options" && (d.option_primary || s.opt_target_total != null)) {
|
||||
const sz = d.sizing || {};
|
||||
summary.innerHTML =
|
||||
"期权目标净利 " +
|
||||
fmtPnlHtml(s.opt_target_total) +
|
||||
" · 永续目标净利 " +
|
||||
fmtPnlHtml(s.perp_target_total) +
|
||||
" · 保费 " +
|
||||
fmt(s.premium_paid) +
|
||||
(sz.eth_qty != null ? " · ETH " + fmt(sz.eth_qty, 2) : "") +
|
||||
(s.perp_direction ? " · 永续" + (s.perp_direction === "short" ? "空" : "多") : "");
|
||||
} else if (d.plan_type === "perp_options") {
|
||||
summary.innerHTML =
|
||||
"止盈合计 " +
|
||||
fmtPnlHtml(s.tp_total) +
|
||||
@@ -1439,16 +1686,53 @@
|
||||
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
const m = b.getAttribute("data-money") || "itm";
|
||||
// 永期禁止选虚值筛选
|
||||
if (m === "otm") {
|
||||
alert("永期保险腿仅允许实值或平值");
|
||||
if (m === "otm" && !isOptionPrimary()) {
|
||||
alert("永期保险腿仅允许实值或平值;请先打开「以期权为主」");
|
||||
return;
|
||||
}
|
||||
state.moneyFilter = m === "atm" ? "atm" : "itm";
|
||||
state.moneyFilter = m === "otm" ? "otm" : m === "atm" ? "atm" : "itm";
|
||||
if (isOptionPrimary()) applyOpDefaultsFromMoney(false);
|
||||
syncMoneyUI();
|
||||
renderListStrikes();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".hp-po-mode").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
setOptionPrimary(b.getAttribute("data-option-primary") === "1", true);
|
||||
});
|
||||
});
|
||||
if ($("hp-opt-leverage")) {
|
||||
$("hp-opt-leverage").addEventListener("input", function () {
|
||||
state.opLevTouched = true;
|
||||
renderListStrikes();
|
||||
});
|
||||
}
|
||||
if ($("hp-opt-perp-ratio")) {
|
||||
$("hp-opt-perp-ratio").addEventListener("input", function () {
|
||||
state.opRatioTouched = true;
|
||||
if (state.selected) {
|
||||
const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01);
|
||||
if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets);
|
||||
}
|
||||
void loadMarket();
|
||||
});
|
||||
}
|
||||
["hp-premium-budget", "hp-strike-interval", "hp-min-hours", "hp-opt-target-pts", "hp-perp-target-pts"].forEach(
|
||||
function (id) {
|
||||
const el = $(id);
|
||||
if (!el) return;
|
||||
el.addEventListener("input", function () {
|
||||
if (id === "hp-min-hours" && state.chain) fillExpSelect($("hp-exp-select"), state.chain);
|
||||
if (id === "hp-strike-interval" || id === "hp-premium-budget") renderListStrikes();
|
||||
if (state.selected && (id === "hp-premium-budget" || id === "hp-strike-interval")) {
|
||||
const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01);
|
||||
if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets);
|
||||
void loadMarket();
|
||||
}
|
||||
updatePerpPnlHint();
|
||||
});
|
||||
}
|
||||
);
|
||||
document.querySelectorAll(".hp-oo-money-btn").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
const m = b.getAttribute("data-oo-money") || "atm_otm";
|
||||
@@ -1513,6 +1797,7 @@
|
||||
const el = $(id);
|
||||
if (el) el.addEventListener("input", updatePerpPnlHint);
|
||||
});
|
||||
syncOptionPrimaryUI();
|
||||
if ($("hp-refresh"))
|
||||
$("hp-refresh").addEventListener("click", function () {
|
||||
void refreshAll();
|
||||
@@ -2109,32 +2394,69 @@
|
||||
};
|
||||
} else {
|
||||
if (!state.selected) throw new Error("请选用期权腿");
|
||||
const m = (state.selected.moneyness || "").toLowerCase();
|
||||
if (m === "otm") 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: getDirection(),
|
||||
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,
|
||||
ask: state.selected.ask,
|
||||
index_px: indexPx() || entry,
|
||||
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 (isOptionPrimary()) {
|
||||
const sized = computeOpSizing(state.selected.ask, state.selected.ct_mult || 0.01);
|
||||
if (!sized) throw new Error("请填写权利金并确认卖一有效");
|
||||
const optPts = numInput("hp-opt-target-pts", NaN);
|
||||
const perpPts = numInput("hp-perp-target-pts", NaN);
|
||||
if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)");
|
||||
const exp = currentExp("hp-exp-select");
|
||||
const entry = indexPx() || Number((state.market && state.market.mark) || 0);
|
||||
body = {
|
||||
plan_type: "perp_options",
|
||||
option_primary: true,
|
||||
underlying: state.underlying,
|
||||
direction: getDirection(),
|
||||
entry: entry,
|
||||
contracts: sized.contracts,
|
||||
sheets: sized.sheets,
|
||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||
ct_mult: state.selected.ct_mult || 0.01,
|
||||
opt_inst_id: state.selected.inst_id,
|
||||
opt_type: state.selected.opt_type,
|
||||
strike: state.selected.strike,
|
||||
ask: state.selected.ask,
|
||||
index_px: entry,
|
||||
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
||||
leverage: numInput("hp-perp-leverage", 100),
|
||||
option_leverage: numInput("hp-opt-leverage", 100),
|
||||
premium_budget: numInput("hp-premium-budget", 0),
|
||||
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
||||
option_target_points: optPts,
|
||||
perp_target_points: perpPts,
|
||||
strike_interval: numInput("hp-strike-interval", 15),
|
||||
min_option_hours: numInput("hp-min-hours", 36),
|
||||
moneyness: opMoneyKind(),
|
||||
hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null,
|
||||
};
|
||||
} else {
|
||||
const m = (state.selected.moneyness || "").toLowerCase();
|
||||
if (m === "otm") 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: getDirection(),
|
||||
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,
|
||||
ask: state.selected.ask,
|
||||
index_px: indexPx() || entry,
|
||||
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 (!fromPreviewModal) {
|
||||
if (!window.confirm("确认启动对冲计划并真实下单?\n(将按期权账户/合约账户分别下单)")) return;
|
||||
|
||||
Reference in New Issue
Block a user