feat(hedge): option-primary watch entry with leverage gate

Start strategy arms a watching plan instead of opening immediately; list filters by leverage; type is a dropdown defaulting to OTM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-09 09:27:42 +08:00
parent 6ab27cebcd
commit eca6d091e9
10 changed files with 608 additions and 39 deletions
+125 -6
View File
@@ -22,7 +22,7 @@
tab: pickDefaultTab(),
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
underlying: root.getAttribute("data-default-underly") || "ETH",
moneyFilter: "itm", // 永期锁定:实值+平值
moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "otm" : "itm",
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
ooRecommend: null, // atm_straddle | double_otm | null
ooStrikeExpandAll: false, // 默认 Call/Put 各 3 档
@@ -149,17 +149,35 @@
return fmt(v, 2) + ":1";
}
/** 列表/盯盘候选杠杆门槛(与后端 effective_min_opt_leverage 对齐). */
function optionPrimaryMinLev() {
const minLev = numInput("hp-opt-leverage", opMoneyKind() === "otm" ? 200 : 100);
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 || "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;
}
if (!optionPrimaryLevOk(c)) return false;
}
if (f === "itm") return m === "itm" || m === "atm";
if (f === "atm") return m === "atm";
@@ -315,6 +333,7 @@
const dirShort = document.querySelector('.hp-po-dir[data-dir="short"]');
if (dirLong) dirLong.title = on ? "做多=买Call+永续空" : "做多永续";
if (dirShort) dirShort.title = on ? "做空=买Put+永续多" : "做空永续";
syncPoActionBtn();
}
function hoursFromExpMs(expMs) {
@@ -372,6 +391,10 @@
}
function syncMoneyUI() {
const moneySel = $("hp-money-select");
if (moneySel && isOptionPrimary()) {
moneySel.value = state.moneyFilter === "otm" ? "otm" : state.moneyFilter === "atm" ? "atm" : "itm";
}
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
const on = b.getAttribute("data-money") === state.moneyFilter;
b.classList.toggle("active", on);
@@ -386,6 +409,18 @@
});
}
function syncPoActionBtn() {
const btn = $("hp-preview-btn");
if (!btn) return;
if (isOptionPrimary()) {
btn.textContent = "策略启动";
btn.title = "按参数启动盯盘;杠杆/间隔达标后自动开仓(非现场开)";
} else {
btn.textContent = "计算";
btn.title = "情景测算后再启动";
}
}
function syncOoRecommendUI() {
const cur = state.ooRecommend || "";
document.querySelectorAll(".hp-oo-recommend-btn").forEach(function (b) {
@@ -1097,7 +1132,7 @@
return;
}
if (isOptionPrimary() && !matchesMoneyFilter(c)) {
alert("不符合当前间隔/虚实值过滤");
alert("不符合当前间隔/虚实值/杠杆门槛");
return;
}
state.selected = c;
@@ -1149,7 +1184,8 @@
sameType.length +
" 档)·检查间隔" +
(interval != null ? "≤" + interval : "") +
"点/虚实值";
"点/虚实值/杠杆≥" +
optionPrimaryMinLev();
} else if (!sameType.length) {
hint =
"该到期无 " +
@@ -1741,9 +1777,21 @@
renderListStrikes();
});
});
if ($("hp-money-select")) {
$("hp-money-select").addEventListener("change", function () {
const m = $("hp-money-select").value || "otm";
state.moneyFilter = m === "otm" ? "otm" : m === "atm" ? "atm" : "itm";
state.opLevTouched = false;
state.opRatioTouched = false;
applyOpDefaultsFromMoney(true);
syncMoneyUI();
renderListStrikes();
});
}
if ($("hp-opt-leverage")) {
$("hp-opt-leverage").addEventListener("input", function () {
state.opLevTouched = true;
if (isOptionPrimary()) renderListStrikes();
});
}
if ($("hp-opt-perp-ratio")) {
@@ -1940,7 +1988,8 @@
if ($("hp-preview-btn"))
$("hp-preview-btn").addEventListener("click", function () {
state.mode = "perp_options";
void runPreview();
if (isOptionPrimary()) void startOptionPrimaryWatch();
else void runPreview();
});
if ($("hp-preview-btn-oo"))
$("hp-preview-btn-oo").addEventListener("click", function () {
@@ -2036,6 +2085,14 @@
}
function activeTargetLabel(p) {
if (p.plan_type === "perp_options" && (p.option_primary == 1 || p.option_primary === true || Number(p.option_primary) === 1)) {
return (
"期权K±" +
fmt(p.option_target_points, 0) +
" · 永续K±" +
fmt(p.perp_target_points, 0)
);
}
if (p.plan_type === "perp_options") {
return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl);
}
@@ -2043,6 +2100,9 @@
}
function activeStatusLabel(p) {
if ((p.status || "") === "watching") {
return '<span class="hp-plan-watching">盯盘中</span>';
}
if ((p.status || "") === "partial") {
return '<span class="hp-plan-partial">半腿待补</span>';
}
@@ -2422,6 +2482,65 @@
}
}
async function startOptionPrimaryWatch() {
try {
const optPts = numInput("hp-opt-target-pts", NaN);
const perpPts = numInput("hp-perp-target-pts", NaN);
const prem = numInput("hp-premium-budget", 0);
const optLev = numInput("hp-opt-leverage", 200);
if (!(prem > 0)) throw new Error("请填写权利金预算");
if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)");
if (!(optLev > 0)) throw new Error("请填写期权杠杆门槛");
if (!(state.market && state.market.exchange_symbol)) throw new Error("永续行情未就绪,请先刷新");
if (!state.canStart) {
throw new Error("当前不可启动(门禁未满足),请查看上方提示");
}
const msg =
"确认启动盯盘?\n" +
"类型 " +
(opMoneyKind() === "otm" ? "虚值" : opMoneyKind() === "atm" ? "平值" : "实/平") +
" · 间隔 " +
numInput("hp-strike-interval", 15) +
" · 杠杆≥" +
optionPrimaryMinLev() +
"\n达标后自动开仓(非现场立即开)";
if (!window.confirm(msg)) return;
const body = {
plan_type: "perp_options",
option_primary: true,
watch_entry: 1,
underlying: state.underlying,
direction: getDirection(),
exchange_symbol: state.market.exchange_symbol,
contract_size: state.market.contract_size || 0.01,
index_px: indexPx() || Number(state.market.mark || 0),
entry: indexPx() || Number(state.market.mark || 0),
premium_budget: prem,
option_perp_ratio: numInput("hp-opt-perp-ratio", 4),
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: optLev,
leverage: numInput("hp-perp-leverage", 100),
moneyness: opMoneyKind(),
};
const d = await apiJson("/api/hedge-plan/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
setGateLine(d.gates);
alert((d.msg || "已启动盯盘") + (d.plan_id ? "\n计划 #" + d.plan_id : ""));
state.tab = "active";
syncTabUI();
void loadActivePlans();
void loadGates();
} catch (e) {
alert(e.message || String(e));
}
}
async function startPlan(planType, fromPreviewModal) {
const isOo = planType === "options_options";
const startBtn = $("hp-preview-start");
@@ -2567,7 +2686,7 @@
await loadChain();
} catch (e) {
const tbody = $("hp-strike-tbody");
if (tbody) tbody.innerHTML = '<tr><td colspan="5" class="err">' + (e.message || e) + "</td></tr>";
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="err">' + (e.message || e) + "</td></tr>";
}
}
+8 -10
View File
@@ -3515,22 +3515,20 @@ html[data-theme="light"] .opt-be-dist-down {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.hedge-plan-page-wrap .hp-po-fields--select {
grid-template-columns: minmax(72px, 0.9fr) minmax(72px, 0.9fr) minmax(0, 1.6fr) minmax(72px, 0.9fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: end;
}
.hedge-plan-page-wrap .hp-po-field--type {
min-width: 0;
}
.hedge-plan-page-wrap .hp-po-type-seg {
display: flex;
flex-wrap: nowrap;
gap: 4px;
.hedge-plan-page-wrap .hp-po-field--type select {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.hedge-plan-page-wrap .hp-po-type-seg .hp-money-btn {
flex: 1 1 0;
padding: 4px 6px;
font-size: 0.72rem;
white-space: nowrap;
.hedge-plan-page-wrap .hp-plan-watching {
color: #fbbf24;
font-weight: 650;
}
@media (max-width: 720px) {
.hedge-plan-page-wrap .hp-po-fields--capital,