永期手动下单:修币本位杠杆显示,张数定仓与双出场模式.

右侧以张数+比例算永续,杠杆取 env(BTC10/ETH5);开仓买币+5%滑点;出场可选盈利金额(扣双边费)或目标位;平仓后卖光币回 U.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 15:43:44 +08:00
parent e474c89c35
commit d68e9c3946
8 changed files with 392 additions and 63 deletions
+198 -24
View File
@@ -39,6 +39,7 @@
ooCloseMode: "close_all",
direction: "long",
optionPrimary: root.getAttribute("data-option-primary") !== "0",
exitMode: "profit", // profit=盈利金额出场 | target=目标位出场
opLevTouched: false,
opRatioTouched: false,
perpLevTouched: false,
@@ -449,9 +450,24 @@
};
}
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 = numInput("hp-sheets", 0);
const sheets = orderSheets();
return computeOpSizing(
state.selected.ask,
state.selected.ct_mult || 0.01,
@@ -489,6 +505,112 @@
return Number.isFinite(eth) && eth > 0 ? eth : 5;
}
/** 按交易账户 USDT(买币+5%滑点)与合约账户永续杠杆算最大期权张数. */
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 cs = Number((state.market && state.market.contract_size) || 0.01);
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);
const perpAvail = Number((state.market && state.market.available_usdt) || 0);
if (!(ask > 0) || !(ct > 0) || !(ratio > 0) || !(cs > 0) || !(mark > 0) || !(lev > 0)) {
return null;
}
// 买币:权利金币×1.05×指数 ≈ 所需 USDT
const coinPerSheet = ask * ct;
const usdtPerSheet = coinPerSheet * 1.05 * mark;
let maxByOpt = Infinity;
if (tradingUsdt > 0 && usdtPerSheet > 0) {
maxByOpt = Math.floor(tradingUsdt / usdtPerSheet + 1e-12);
}
// 永续:名义 = 保证金×杠杆; 永续币 = 期权币/比例; 合约张 = 永续币/cs
// 期权张 n → 永续名义 = (n*ct/ratio)*mark ≤ avail*lev
let maxByPerp = Infinity;
if (perpAvail > 0) {
const maxPerpNotional = perpAvail * lev;
const notionalPerSheet = (ct / ratio) * mark;
if (notionalPerSheet > 0) {
maxByPerp = Math.floor(maxPerpNotional / notionalPerSheet + 1e-12);
}
}
const maxSheets = Math.max(0, Math.min(maxByOpt, maxByPerp));
if (!(maxSheets > 0) && !(tradingUsdt > 0) && !(perpAvail > 0)) return null;
return {
max_sheets: maxSheets,
max_by_opt: Number.isFinite(maxByOpt) ? maxByOpt : null,
max_by_perp: Number.isFinite(maxByPerp) ? maxByPerp : null,
lev: lev,
trading_usdt: tradingUsdt,
perp_avail: perpAvail,
usdt_per_sheet: usdtPerSheet,
};
}
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);
if (maxEl) {
if (maxInfo) {
maxEl.innerHTML =
"最大约 <strong>" +
maxInfo.max_sheets +
"</strong> 张 · 永续杠杆 " +
lev +
"x(env) · 交易 USDT " +
fmt(maxInfo.trading_usdt, 2) +
" · 合约可用 " +
fmt(maxInfo.perp_avail, 2);
} else {
maxEl.textContent = "选择期权并刷新行情后显示最大开仓张数(交易账户买币+永续杠杆)";
}
}
if (perpEl) {
if (sized) {
perpEl.innerHTML =
"永续自动开 <strong>" +
fmt(sized.perp_qty, 4) +
"</strong> " +
coin +
" / <strong>" +
fmt(sized.contracts, coin === "BTC" ? 4 : 2) +
"</strong> 张 · 买币约 " +
fmt(sized.premium_est * 1.05, 4) +
" " +
coin +
"(含5%滑点)";
} 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) {
profitBox.classList.toggle("hidden", mode !== "profit");
if (mode === "profit") profitBox.removeAttribute("hidden");
else profitBox.setAttribute("hidden", "hidden");
}
if (targetBox) {
targetBox.classList.toggle("hidden", mode !== "target");
if (mode === "target") targetBox.removeAttribute("hidden");
else targetBox.setAttribute("hidden", "hidden");
}
}
function syncUnderlyingUI() {
const uly = state.underlying || "ETH";
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
@@ -622,12 +744,11 @@
return (Math.round((k / a) * 10) / 10).toFixed(1) + "×";
}
/** 永期期权行情杠杆:指数÷卖一(与选约杠杆门一致). */
/** CoinOX 币本位:卖一为币报价 → 杠杆≈1÷卖一(与期权页一致). */
function calcIndexAskLeverage(ask) {
const idx = indexPx();
const a = Number(ask);
if (!(idx > 0) || !(a > 0)) return "—";
return (Math.round((idx / a) * 10) / 10).toFixed(1) + "×";
if (!(a > 0)) return "—";
return (Math.round((1 / a) * 10) / 10).toFixed(1) + "×";
}
function clearPoSelection() {
@@ -800,6 +921,11 @@
if (acct.trading_usdc != null && acct.trading_usdc !== "") {
state.tradingUsdc = Number(acct.trading_usdc);
}
if (acct.trading_usdt != null && acct.trading_usdt !== "") {
state.tradingUsdt = Number(acct.trading_usdt);
} else if (acct.trading_usdc != null && acct.trading_usdc !== "") {
state.tradingUsdt = Number(acct.trading_usdc);
}
if (acct.funding_usdc != null && acct.funding_usdc !== "") {
state.fundingUsdc = Number(acct.funding_usdc);
}
@@ -1174,12 +1300,20 @@
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
? '目标净利 <strong>' + fmt(p, 2) + '</strong> USDT <span class="muted">(扣开平双边手续费后)</span>'
: '<span class="muted">填写目标盈利金额(扣买入+卖出手续费后的组合净利)</span>';
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);
@@ -1195,7 +1329,7 @@
(optT != null ? fmt(optT, 2) : "—") +
" · 永续目标指数 " +
(perpT != null ? fmt(perpT, 2) : "—") +
' <span class="muted">(相对K;期权目标需买一且扣费净利&gt;0)</span>';
' <span class="muted">(相对K;须买一流动性且扣费净利&gt;0)</span>';
return;
}
const entry = Number(($("hp-entry") && $("hp-entry").value) || NaN);
@@ -1324,10 +1458,17 @@
}
updatePremiumLine();
if (isOptionPrimary()) {
const sized = computeOpSizing(c.ask, c.ct_mult || 0.01);
if (sized && $("hp-sheets")) $("hp-sheets").value = String(sized.sheets);
// 手动下单以张数为主:选约后若未填张数则默认 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");
}
void loadMarket();
updatePerpPnlHint();
updatePoOpenSummary();
}
}
@@ -2028,6 +2169,7 @@
const sz = $("hp-sizing-line");
const sized = currentOpSizing();
if (sz && sized) sz.innerHTML = formatOpSizingHtml(sized);
updatePoOpenSummary();
void loadMarket();
});
}
@@ -2155,17 +2297,38 @@
}
if ($("hp-exp-select")) $("hp-exp-select").addEventListener("change", renderListStrikes);
if ($("hp-oo-exp-select")) $("hp-oo-exp-select").addEventListener("change", renderTStrikes);
function onSheetsInput(fromId) {
syncSheetsInputs(fromId);
updatePremiumLine();
if (isOptionPrimary()) {
const sz = $("hp-sizing-line");
const sized = currentOpSizing();
if (sz && sized) sz.innerHTML = formatOpSizingHtml(sized);
updatePoOpenSummary();
void loadMarket();
}
}
if ($("hp-sheets")) {
$("hp-sheets").addEventListener("input", function () {
updatePremiumLine();
if (isOptionPrimary() && state.selected) {
const sz = $("hp-sizing-line");
const sized = currentOpSizing();
if (sz && sized) sz.innerHTML = formatOpSizingHtml(sized);
void loadMarket();
}
onSheetsInput("hp-sheets");
});
}
if ($("hp-order-sheets")) {
$("hp-order-sheets").addEventListener("input", function () {
onSheetsInput("hp-order-sheets");
});
}
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
b.addEventListener("click", function () {
state.exitMode = b.getAttribute("data-exit") === "target" ? "target" : "profit";
syncExitModeUI();
updatePerpPnlHint();
});
});
if ($("hp-profit-target")) {
$("hp-profit-target").addEventListener("input", updatePerpPnlHint);
}
syncExitModeUI();
["hp-oo-sheets-a", "hp-oo-sheets-b"].forEach(function (id) {
const el = $(id);
if (el) el.addEventListener("input", updateOoPremiumLine);
@@ -2820,21 +2983,30 @@
if (!state.selected) throw new Error("请选用期权腿");
if (isOptionPrimary()) {
const sized = currentOpSizing();
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)");
if (!sized || !(sized.sheets > 0)) throw new Error("请选择期权并填写张数");
const exitMode = state.exitMode === "target" ? "target" : "profit";
let optPts = 0;
let perpPts = 0;
let profitTarget = 0;
if (exitMode === "profit") {
profitTarget = numInput("hp-profit-target", NaN);
if (!(profitTarget > 0)) throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
} else {
optPts = numInput("hp-opt-target-pts", NaN);
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);
const premEst =
Number(state.selected.ask || 0) *
sized.sheets *
Number(state.selected.ct_mult || 0.01);
const premBudget = numInput("hp-premium-budget", 0);
body = {
plan_type: "perp_options",
option_primary: true,
market_order: 1,
exit_mode: exitMode,
underlying: state.underlying,
direction: getDirection(),
entry: entry,
@@ -2848,16 +3020,18 @@
ask: state.selected.ask,
index_px: entry,
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
leverage: numInput("hp-perp-leverage", defaultPerpLeverage(state.underlying)),
leverage: defaultPerpLeverage(state.underlying),
option_leverage: 0,
premium_budget: premBudget > 0 ? premBudget : Math.max(premEst / 0.95, premEst),
premium_budget: Math.max(premEst / 0.95, premEst),
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
option_target_points: optPts,
perp_target_points: perpPts,
option_target_points: optPts || 0,
perp_target_points: perpPts || 0,
profit_target_usdt: profitTarget || 0,
strike_interval: 0,
min_option_hours: 0,
moneyness: (state.selected.moneyness || opMoneyKind() || "all").toLowerCase(),
hours_to_expiry: exp ? hoursFromExpMs(exp.exp_time) : null,
spot_buy_buffer: 1.05,
};
} else {
const m = (state.selected.moneyness || "").toLowerCase();