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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user