Add options index target monitors that auto limit-close on hit.

Position and order forms can arm a target; right-side and hub panels show active monitors; expiry remains the stop with no separate SL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-15 15:27:46 +08:00
parent 8d2da921ee
commit d6823a2903
13 changed files with 1011 additions and 18 deletions
+73
View File
@@ -4091,6 +4091,79 @@ html[data-theme="light"] .options-estimate-row {
.options-page-wrap .opt-pos-cell--depth {
grid-column: span 2;
}
.options-page-wrap .opt-target-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(67, 82, 118, 0.45);
}
.options-page-wrap .opt-target-row-label {
font-size: 0.72rem;
color: #9aa8c7;
min-width: 2.5em;
}
.options-page-wrap .opt-pos-target-input {
width: 110px;
max-width: 36vw;
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #3a4660;
background: #0f1420;
color: #e8eefc;
font-size: 0.82rem;
}
.options-page-wrap .opt-target-row .btn-secondary {
padding: 4px 10px;
font-size: 0.75rem;
}
.options-page-wrap .opt-target-row-hint {
font-size: 0.7rem;
}
.opt-target-monitors {
margin: 0 0 10px;
padding: 10px 12px;
border: 1px solid rgba(99, 118, 168, 0.45);
border-radius: 10px;
background: rgba(18, 28, 48, 0.75);
}
.opt-target-monitors-head {
font-size: 0.78rem;
font-weight: 600;
color: #c9d6f5;
margin-bottom: 8px;
}
.opt-target-mon-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 6px 0;
border-top: 1px solid rgba(67, 82, 118, 0.35);
}
.opt-target-mon-item:first-child {
border-top: 0;
padding-top: 0;
}
.opt-target-mon-inst {
font-size: 0.72rem;
color: #dbe6ff;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.opt-target-mon-rule {
font-size: 0.78rem;
color: #9ad0ff;
font-variant-numeric: tabular-nums;
}
.opt-target-mon-item .btn-secondary {
margin-left: auto;
padding: 2px 8px;
font-size: 0.7rem;
}
.options-page-wrap .opt-bid-plain {
color: #dbe6ff;
font-variant-numeric: tabular-nums;
+141 -1
View File
@@ -788,6 +788,15 @@
} else if (mode === "sheets") {
body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10);
}
const tgtRaw = (document.getElementById("opt-target-idx").value || "").trim();
if (tgtRaw !== "") {
const tgt = parseFloat(tgtRaw);
if (!Number.isFinite(tgt) || tgt <= 0) {
alert("目标位无效");
return;
}
body.target_index = tgt;
}
const d = await apiJson("/api/options/open", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -845,6 +854,22 @@
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span></div>" +
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid) + "</span></div>" +
"</div>" +
renderTargetDelegateRow(p)
);
}
function renderTargetDelegateRow(p) {
const inst = p.inst_id || "";
const tgt = p.target_index != null && p.target_index !== "" ? String(p.target_index) : "";
const armed = tgt !== "";
return (
'<div class="opt-target-row">' +
'<span class="opt-target-row-label">委托</span>' +
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="目标指数价" value="' + tgt + '">' +
'<button type="button" class="btn-secondary opt-target-set-btn" data-inst="' + inst + '">设定</button>' +
'<button type="button" class="btn-secondary opt-target-cancel-btn" data-inst="' + inst + '"' + (armed ? "" : " disabled") + ">取消</button>" +
'<span class="muted opt-target-row-hint">' + (armed ? "监控中 · 达价限价平 · 无止损" : "达价限价平 · 无止损 · 到期即止损") + "</span>" +
"</div>"
);
}
@@ -912,6 +937,28 @@
closePosition(btn.getAttribute("data-inst"), btn);
});
});
container.querySelectorAll(".opt-target-set-btn").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
setPositionTarget(btn.getAttribute("data-inst"), btn);
});
});
container.querySelectorAll(".opt-target-cancel-btn").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
cancelPositionTarget(btn.getAttribute("data-inst"), btn);
});
});
container.querySelectorAll(".opt-pos-target-input").forEach(function (inp) {
inp.addEventListener("click", function (e) { e.stopPropagation(); });
inp.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
setPositionTarget(inp.getAttribute("data-inst"), null);
}
});
});
container.querySelectorAll(".opt-pos-bar").forEach(function (bar) {
bar.addEventListener("click", function () {
const item = bar.closest(".opt-pos-accordion-item");
@@ -926,6 +973,81 @@
});
}
async function setPositionTarget(inst, btn) {
if (!inst) return;
const card = document.querySelector('.opt-pos-card[data-inst="' + inst + '"]') ||
document.querySelector('.opt-pos-accordion-item[data-inst="' + inst + '"]');
const inp = card ? card.querySelector(".opt-pos-target-input") : null;
const raw = inp ? String(inp.value || "").trim() : "";
const tgt = parseFloat(raw);
if (!Number.isFinite(tgt) || tgt <= 0) {
alert("请输入有效目标指数价");
return;
}
if (btn) btn.disabled = true;
try {
const d = await apiJson("/api/options/target", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inst_id: inst, target_index: tgt }),
});
if (!d.ok) {
alert(d.msg || "设定失败");
return;
}
await refreshAllPositions();
} finally {
if (btn) btn.disabled = false;
}
}
async function cancelPositionTarget(inst, btn) {
if (!inst) return;
if (btn) btn.disabled = true;
try {
const d = await apiJson("/api/options/target/cancel", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inst_id: inst }),
});
if (!d.ok) {
alert(d.msg || "取消失败");
return;
}
await refreshAllPositions();
} finally {
if (btn) btn.disabled = false;
}
}
function paintTargetMonitors(list) {
const box = document.getElementById("opt-target-monitors");
const host = document.getElementById("opt-target-monitors-list");
if (!box || !host) return;
const rows = Array.isArray(list) ? list.filter(function (t) { return t && t.inst_id; }) : [];
if (!rows.length) {
box.hidden = true;
host.innerHTML = "";
return;
}
box.hidden = false;
host.innerHTML = rows.map(function (t) {
const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
return (
'<div class="opt-target-mon-item">' +
'<code class="opt-target-mon-inst" title="' + (t.inst_id || "") + '">' + (t.inst_id || "") + "</code>" +
'<span class="opt-target-mon-rule">' + side + " " + fmt(t.target_index, 1) + "</span>" +
'<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>' +
"</div>"
);
}).join("");
host.querySelectorAll(".opt-target-mon-cancel").forEach(function (btn) {
btn.addEventListener("click", function () {
cancelPositionTarget(btn.getAttribute("data-inst"), btn);
});
});
}
async function closePosition(inst, btn) {
const sheets = btn && btn.getAttribute("data-sheets") ? parseInt(btn.getAttribute("data-sheets"), 10) : null;
let url = "/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=close_preview";
@@ -1063,7 +1185,25 @@
const seq = ++positionsRefreshSeq;
const d = await apiJson("/api/options/positions");
if (seq !== positionsRefreshSeq) return;
paintPositions(resolvePositionsList(d));
const list = resolvePositionsList(d);
paintPositions(list);
const fromPos = list
.filter(function (p) { return p && p.target_index != null; })
.map(function (p) {
return {
id: p.target_monitor_id,
inst_id: p.inst_id,
opt_type: p.opt_type,
target_index: p.target_index,
};
});
if (fromPos.length) {
paintTargetMonitors(fromPos);
} else {
const t = await apiJson("/api/options/targets");
if (seq !== positionsRefreshSeq) return;
paintTargetMonitors((t && t.ok && t.targets) ? t.targets : []);
}
}
async function refreshStats() {
+4 -1
View File
@@ -113,7 +113,10 @@
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span></div>" +
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid, hub) + "</span></div>" +
"</div>"
"</div>" +
(p.target_index != null
? '<div class="opt-target-row opt-target-row--ro"><span class="opt-target-row-label">委托</span><span class="pos-value">目标指数 ' + fmt(p.target_index, 1) + "</span><span class="muted opt-target-row-hint">监控中 · 达价限价平</span></div>"
: "")
);
}