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:
@@ -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>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user