747ec62e34
Co-authored-by: Cursor <cursoragent@cursor.com>
396 lines
14 KiB
JavaScript
396 lines
14 KiB
JavaScript
(function (global) {
|
|
function escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function leftTriggerSide(optType) {
|
|
return String(optType || "P").toUpperCase() === "C" ? "lte" : "gte";
|
|
}
|
|
|
|
function triggerHintText(optType, px) {
|
|
const put = String(optType || "P").toUpperCase() !== "C";
|
|
const price = px && String(px).trim() !== "" ? String(px) : "触发价";
|
|
if (put) {
|
|
return "看跌左侧单:指数上穿 " + price + " 后,在偏离区内开最近实值 Put。";
|
|
}
|
|
return "看涨左侧单:指数下穿 " + price + " 后,在偏离区内开最近实值 Call。";
|
|
}
|
|
|
|
function syncTriggerHint() {
|
|
const hint = document.getElementById("cf-trigger-hint");
|
|
const typeEl = document.getElementById("cf-opt-type");
|
|
const pxEl = document.getElementById("cf-trigger-px");
|
|
if (!hint || !typeEl) return;
|
|
hint.textContent = triggerHintText(typeEl.value, pxEl && pxEl.value);
|
|
hint.className = "cond-trigger-hint " + (typeEl.value === "C" ? "is-call" : "is-put");
|
|
}
|
|
|
|
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 || "—";
|
|
}
|
|
|
|
function statusClass(o) {
|
|
const st = o.status || "";
|
|
if (st === "open") return "st-open";
|
|
if (st === "triggering") return "st-trig";
|
|
if (st === "armed") return "st-armed";
|
|
return "st-done";
|
|
}
|
|
|
|
function isHubPhone() {
|
|
return (
|
|
document.documentElement.classList.contains("hub-phone-html") ||
|
|
!!(document.body && document.body.classList.contains("hub-phone"))
|
|
);
|
|
}
|
|
|
|
function orderTriggerText(o) {
|
|
const side = o.trigger_side === "lte" ? "下穿≤" : "上穿≥";
|
|
const px = o.trigger_px != null ? String(o.trigger_px) : "";
|
|
return side + px;
|
|
}
|
|
|
|
function orderExitText(o) {
|
|
const bits = [];
|
|
if (o.profit_exit_enabled) bits.push("翻倍×" + (o.profit_exit_mult || ""));
|
|
if (o.target_index != null) bits.push("目标" + o.target_index);
|
|
return bits.join(" · ");
|
|
}
|
|
|
|
function orderActionHtml(o) {
|
|
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 actions;
|
|
}
|
|
|
|
function bindOrderActions(box) {
|
|
if (!box) return;
|
|
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();
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderPhoneOrderCard(o) {
|
|
const exit = orderExitText(o);
|
|
const meta = [];
|
|
if (o.deviation_pct != null) meta.push("偏离 " + o.deviation_pct + "%");
|
|
if (o.min_expiry_hours != null) meta.push("到期 " + o.min_expiry_hours + "h");
|
|
if (exit) meta.push(exit);
|
|
const actions = orderActionHtml(o);
|
|
return (
|
|
'<article class="hub-phone-cond">' +
|
|
'<div class="hub-phone-cond-head">' +
|
|
"<strong>" +
|
|
escapeHtml(o.instance_name || "") +
|
|
"</strong>" +
|
|
'<span class="cond-st ' +
|
|
statusClass(o) +
|
|
'">' +
|
|
escapeHtml(statusLabel(o)) +
|
|
"</span></div>" +
|
|
'<div class="hub-phone-cond-main">' +
|
|
escapeHtml((o.underlying || "") + " " + (o.opt_type === "C" ? "Call" : "Put")) +
|
|
" · " +
|
|
escapeHtml(orderTriggerText(o)) +
|
|
"</div>" +
|
|
(meta.length ? '<div class="hub-phone-cond-meta muted">' + escapeHtml(meta.join(" · ")) + "</div>" : "") +
|
|
(o.message ? '<div class="hub-phone-cond-msg muted">' + escapeHtml(o.message) + "</div>" : "") +
|
|
(actions ? '<div class="hub-phone-cond-actions">' + actions + "</div>" : "") +
|
|
"</article>"
|
|
);
|
|
}
|
|
|
|
function instanceMode(it) {
|
|
const m = String((it && it.mode) || "").toLowerCase();
|
|
return m === "sim" || m === "live" ? m : "";
|
|
}
|
|
|
|
function renderCandidateCard(it, checkedIds) {
|
|
const disabled = !it.selectable;
|
|
const tip = disabled
|
|
? it.has_position
|
|
? "已有持仓"
|
|
: it.cond_order_locked
|
|
? "条件单占用"
|
|
: it.online
|
|
? "不可选"
|
|
: "离线"
|
|
: "可选";
|
|
const id = String(it.id);
|
|
const checked = !disabled && checkedIds.indexOf(id) >= 0;
|
|
return (
|
|
'<label class="cond-inst' +
|
|
(disabled ? " is-off" : "") +
|
|
'" title="' +
|
|
escapeHtml(tip) +
|
|
'">' +
|
|
'<input type="checkbox" name="cf-inst" value="' +
|
|
encodeURIComponent(id) +
|
|
'"' +
|
|
(disabled ? " disabled" : "") +
|
|
(checked ? " checked" : "") +
|
|
">" +
|
|
'<span class="cond-inst-name">' +
|
|
escapeHtml(it.name || "") +
|
|
"</span>" +
|
|
'<span class="cond-inst-tag">' +
|
|
escapeHtml(tip) +
|
|
"</span></label>"
|
|
);
|
|
}
|
|
|
|
function renderCandidateGroup(mode, items, checkedIds) {
|
|
if (!items.length) return "";
|
|
const label =
|
|
mode === "sim" || mode === "live"
|
|
? '<div class="cond-inst-group-label"><span class="overview-mode-tag overview-mode-tag--' +
|
|
mode +
|
|
'">' +
|
|
mode +
|
|
"</span></div>"
|
|
: "";
|
|
return (
|
|
'<div class="cond-inst-group">' +
|
|
label +
|
|
'<div class="cond-inst-grid">' +
|
|
items
|
|
.map(function (it) {
|
|
return renderCandidateCard(it, checkedIds);
|
|
})
|
|
.join("") +
|
|
"</div></div>"
|
|
);
|
|
}
|
|
|
|
async function loadCandidates() {
|
|
const box = document.getElementById("cf-instances");
|
|
if (!box) return;
|
|
const checkedIds = Array.prototype.slice
|
|
.call(box.querySelectorAll('input[name="cf-inst"]:checked'))
|
|
.map(function (el) {
|
|
return decodeURIComponent(el.value || "");
|
|
});
|
|
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;
|
|
}
|
|
const sim = [];
|
|
const live = [];
|
|
const other = [];
|
|
list.forEach(function (it) {
|
|
const m = instanceMode(it);
|
|
if (m === "sim") sim.push(it);
|
|
else if (m === "live") live.push(it);
|
|
else other.push(it);
|
|
});
|
|
box.innerHTML =
|
|
renderCandidateGroup("sim", sim, checkedIds) +
|
|
renderCandidateGroup("live", live, checkedIds) +
|
|
renderCandidateGroup("", other, checkedIds);
|
|
}
|
|
|
|
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 = '<div class="cond-empty">暂无条件单</div>';
|
|
return;
|
|
}
|
|
if (isHubPhone()) {
|
|
box.innerHTML =
|
|
'<div class="hub-phone-cond-list">' +
|
|
list.map(renderPhoneOrderCard).join("") +
|
|
"</div>";
|
|
bindOrderActions(box);
|
|
return;
|
|
}
|
|
box.innerHTML =
|
|
'<div class="overview-table-wrap cond-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) {
|
|
return (
|
|
"<tr>" +
|
|
"<td>" +
|
|
escapeHtml(o.instance_name || "") +
|
|
"</td>" +
|
|
'<td><span class="cond-st ' +
|
|
statusClass(o) +
|
|
'">' +
|
|
escapeHtml(statusLabel(o)) +
|
|
"</span></td>" +
|
|
"<td>" +
|
|
escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) +
|
|
"</td>" +
|
|
"<td>" +
|
|
escapeHtml(orderTriggerText(o).replace("≤", " ≤ ").replace("≥", " ≥ ")) +
|
|
"</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(orderExitText(o) || "—") +
|
|
"</td>" +
|
|
'<td class="muted">' +
|
|
escapeHtml(o.message || "") +
|
|
"</td>" +
|
|
'<td><div class="overview-actions">' +
|
|
orderActionHtml(o) +
|
|
"</div></td>" +
|
|
"</tr>"
|
|
);
|
|
})
|
|
.join("") +
|
|
"</tbody></table></div>";
|
|
bindOrderActions(box);
|
|
}
|
|
|
|
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 optType = document.getElementById("cf-opt-type").value;
|
|
const body = {
|
|
instance_ids: ids,
|
|
underlying: document.getElementById("cf-underlying").value,
|
|
opt_type: optType,
|
|
trigger_side: leftTriggerSide(optType),
|
|
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);
|
|
const typeEl = document.getElementById("cf-opt-type");
|
|
const pxEl = document.getElementById("cf-trigger-px");
|
|
if (typeEl) typeEl.addEventListener("change", syncTriggerHint);
|
|
if (pxEl) pxEl.addEventListener("input", syncTriggerHint);
|
|
syncTriggerHint();
|
|
refresh();
|
|
setInterval(refresh, 10000);
|
|
}
|
|
|
|
global.HubCondOrders = { boot: boot, refresh: refresh };
|
|
})(window);
|