中控条件单页:下发/取消/改出场,有仓客户端不可选。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
(function (global) {
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function statusLabel(o) {
|
||||
const st = o.status || "";
|
||||
const reason = o.end_reason || "";
|
||||
if (st === "done") {
|
||||
const map = {
|
||||
leave_up: "结束·上破",
|
||||
leave_down: "结束·下破",
|
||||
cancel: "已取消",
|
||||
closed: "已完成",
|
||||
};
|
||||
return map[reason] || "已结束";
|
||||
}
|
||||
const map2 = { armed: "待命", triggering: "触发中", open: "持仓中" };
|
||||
return map2[st] || st || "—";
|
||||
}
|
||||
|
||||
async function loadCandidates() {
|
||||
const box = document.getElementById("cf-instances");
|
||||
if (!box) return;
|
||||
const r = await fetch("/api/cond_orders/candidates");
|
||||
const d = await r.json();
|
||||
const list = d.instances || [];
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<span class="muted">暂无启用实例</span>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = list
|
||||
.map(function (it) {
|
||||
const disabled = !it.selectable;
|
||||
const tip = disabled
|
||||
? it.has_position
|
||||
? "已有持仓"
|
||||
: it.cond_order_locked
|
||||
? "条件单占用"
|
||||
: it.online
|
||||
? "不可选"
|
||||
: "离线"
|
||||
: "可选";
|
||||
return (
|
||||
'<label class="check" title="' +
|
||||
escapeHtml(tip) +
|
||||
'">' +
|
||||
'<input type="checkbox" name="cf-inst" value="' +
|
||||
encodeURIComponent(String(it.id)) +
|
||||
'"' +
|
||||
(disabled ? " disabled" : "") +
|
||||
"> " +
|
||||
escapeHtml(it.name || "") +
|
||||
' <span class="muted">(' +
|
||||
escapeHtml(tip) +
|
||||
")</span></label>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadOrders() {
|
||||
const box = document.getElementById("cond-list");
|
||||
if (!box) return;
|
||||
const r = await fetch("/api/cond_orders");
|
||||
const d = await r.json();
|
||||
const list = d.orders || [];
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<span class="muted">暂无条件单</span>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML =
|
||||
'<div class="overview-table-wrap"><table class="overview-table"><thead><tr>' +
|
||||
"<th>客户端</th><th>状态</th><th>标的</th><th>触发</th><th>偏离</th><th>到期h</th><th>出场</th><th>说明</th><th>操作</th>" +
|
||||
"</tr></thead><tbody>" +
|
||||
list
|
||||
.map(function (o) {
|
||||
const side = o.trigger_side === "lte" ? "≤" : "≥";
|
||||
const exit =
|
||||
(o.profit_exit_enabled ? "翻倍×" + (o.profit_exit_mult || "") : "") +
|
||||
(o.target_index != null ? " 目标" + o.target_index : "");
|
||||
const canCancel = o.status === "armed" || o.status === "triggering";
|
||||
const canExit = o.status === "open";
|
||||
let actions = "";
|
||||
if (canCancel) {
|
||||
actions +=
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-cancel="' +
|
||||
encodeURIComponent(o.hub_order_id || "") +
|
||||
'" data-iid="' +
|
||||
encodeURIComponent(String(o.instance_id || "")) +
|
||||
'">取消</button>';
|
||||
}
|
||||
if (canExit) {
|
||||
actions +=
|
||||
'<button type="button" class="btn btn-sm" data-exit="' +
|
||||
encodeURIComponent(o.hub_order_id || "") +
|
||||
'" data-iid="' +
|
||||
encodeURIComponent(String(o.instance_id || "")) +
|
||||
'" data-target="' +
|
||||
encodeURIComponent(o.target_index != null ? String(o.target_index) : "") +
|
||||
'" data-pe="' +
|
||||
(o.profit_exit_enabled ? "1" : "0") +
|
||||
'" data-mult="' +
|
||||
encodeURIComponent(String(o.profit_exit_mult || "1")) +
|
||||
'">改出场</button>';
|
||||
}
|
||||
return (
|
||||
"<tr>" +
|
||||
"<td>" +
|
||||
escapeHtml(o.instance_name || "") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(statusLabel(o)) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(side + " " + (o.trigger_px != null ? o.trigger_px : "")) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(o.deviation_pct != null ? o.deviation_pct + "%" : "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(o.min_expiry_hours != null ? String(o.min_expiry_hours) : "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(exit || "—") +
|
||||
"</td>" +
|
||||
"<td class=\"muted\">" +
|
||||
escapeHtml(o.message || "") +
|
||||
"</td>" +
|
||||
'<td><div class="overview-actions">' +
|
||||
actions +
|
||||
"</div></td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("") +
|
||||
"</tbody></table></div>";
|
||||
|
||||
box.querySelectorAll("[data-cancel]").forEach(function (btn) {
|
||||
btn.addEventListener("click", async function () {
|
||||
if (!confirm("确认取消该条件单?")) return;
|
||||
const oid = decodeURIComponent(btn.getAttribute("data-cancel") || "");
|
||||
const iid = decodeURIComponent(btn.getAttribute("data-iid") || "");
|
||||
const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/cancel", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ instance_id: Number(iid) }),
|
||||
});
|
||||
const dd = await rr.json();
|
||||
alert(dd.msg || (dd.ok ? "已取消" : "失败"));
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
box.querySelectorAll("[data-exit]").forEach(function (btn) {
|
||||
btn.addEventListener("click", async function () {
|
||||
const oid = decodeURIComponent(btn.getAttribute("data-exit") || "");
|
||||
const iid = decodeURIComponent(btn.getAttribute("data-iid") || "");
|
||||
const curT = decodeURIComponent(btn.getAttribute("data-target") || "");
|
||||
const curM = decodeURIComponent(btn.getAttribute("data-mult") || "1");
|
||||
const pe = btn.getAttribute("data-pe") === "1";
|
||||
const t = prompt("目标指数(留空清除)", curT);
|
||||
if (t === null) return;
|
||||
const peOn = confirm("是否启用翻倍出场?\n当前:" + (pe ? "开" : "关"));
|
||||
let mult = curM;
|
||||
if (peOn) {
|
||||
const m = prompt("翻倍倍数", curM);
|
||||
if (m === null) return;
|
||||
mult = m;
|
||||
}
|
||||
const rr = await fetch("/api/cond_orders/" + encodeURIComponent(oid) + "/exit", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
instance_id: Number(iid),
|
||||
target_index: t === "" ? "" : t,
|
||||
profit_exit_enabled: peOn,
|
||||
profit_exit_mult: mult,
|
||||
}),
|
||||
});
|
||||
const dd = await rr.json();
|
||||
alert(dd.msg || (dd.ok ? "已更新" : "失败"));
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createOrder() {
|
||||
const status = document.getElementById("cf-status");
|
||||
const ids = Array.prototype.slice
|
||||
.call(document.querySelectorAll('input[name="cf-inst"]:checked'))
|
||||
.map(function (el) {
|
||||
return Number(decodeURIComponent(el.value));
|
||||
});
|
||||
if (!ids.length) {
|
||||
alert("请选择至少一台可选客户端");
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
instance_ids: ids,
|
||||
underlying: document.getElementById("cf-underlying").value,
|
||||
opt_type: document.getElementById("cf-opt-type").value,
|
||||
trigger_side: document.getElementById("cf-trigger-side").value,
|
||||
trigger_px: document.getElementById("cf-trigger-px").value,
|
||||
deviation_pct: document.getElementById("cf-deviation").value,
|
||||
min_expiry_hours: document.getElementById("cf-expiry-h").value,
|
||||
target_index: document.getElementById("cf-target").value,
|
||||
profit_exit_enabled: document.getElementById("cf-pe").checked,
|
||||
profit_exit_mult: document.getElementById("cf-pe-mult").value,
|
||||
};
|
||||
if (status) status.textContent = "下发中…";
|
||||
const r = await fetch("/api/cond_orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (status) status.textContent = d.msg || "";
|
||||
alert(d.msg || (d.ok ? "已下发" : "失败"));
|
||||
refresh();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([loadCandidates(), loadOrders()]);
|
||||
}
|
||||
|
||||
function boot() {
|
||||
const btn = document.getElementById("btn-cond-refresh");
|
||||
if (btn) btn.addEventListener("click", refresh);
|
||||
const create = document.getElementById("btn-cond-create");
|
||||
if (create) create.addEventListener("click", createOrder);
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
}
|
||||
|
||||
global.HubCondOrders = { boot: boot, refresh: refresh };
|
||||
})(window);
|
||||
@@ -202,6 +202,18 @@ button, .btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.inst-pick {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
}
|
||||
.inst-pick .check { margin: 0; }
|
||||
.btn-sm {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn[disabled] {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
|
||||
Reference in New Issue
Block a user