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