(function (global) { function escapeHtml(s) { return String(s) .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 += ''; } if (canExit) { actions += ''; } 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 isActiveOrder(o) { const st = String((o && o.status) || ""); return st === "armed" || st === "triggering" || st === "open"; } function formatOpenTime(o) { const raw = String((o && o.opened_at) || "").trim(); if (!raw) return "—"; return raw.replace("T", " ").slice(0, 19); } 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 ( '
' + '
' + "" + escapeHtml(o.instance_name || "") + "" + '' + escapeHtml(statusLabel(o)) + "
" + '
' + escapeHtml((o.underlying || "") + " " + (o.opt_type === "C" ? "Call" : "Put")) + " · " + escapeHtml(orderTriggerText(o)) + "
" + (meta.length ? '
' + escapeHtml(meta.join(" · ")) + "
" : "") + '
开仓 ' + escapeHtml(formatOpenTime(o)) + "
" + (o.message ? '
' + escapeHtml(o.message) + "
" : "") + (actions ? '
' + actions + "
" : "") + "
" ); } 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 ( '" ); } function renderCandidateGroup(mode, items, checkedIds) { if (!items.length) return ""; const label = mode === "sim" || mode === "live" ? '
' + mode + "
" : ""; return ( '
' + label + '
' + items .map(function (it) { return renderCandidateCard(it, checkedIds); }) .join("") + "
" ); } 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 = '暂无启用实例'; 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); } function renderDesktopRows(list) { return list .map(function (o) { return ( "" + "" + escapeHtml(o.instance_name || "") + "" + '' + escapeHtml(statusLabel(o)) + "" + "" + escapeHtml((o.underlying || "") + " " + (o.opt_type || "")) + "" + "" + escapeHtml(orderTriggerText(o).replace("≤", " ≤ ").replace("≥", " ≥ ")) + "" + "" + escapeHtml(o.deviation_pct != null ? o.deviation_pct + "%" : "—") + "" + "" + escapeHtml(o.min_expiry_hours != null ? String(o.min_expiry_hours) : "—") + "" + "" + escapeHtml(orderExitText(o) || "—") + "" + '' + escapeHtml(formatOpenTime(o)) + "" + '' + escapeHtml(o.message || "") + "" + '
' + orderActionHtml(o) + "
" + "" ); }) .join(""); } function renderDesktopTable(list, emptyText) { if (!list.length) { return '
' + escapeHtml(emptyText) + "
"; } return ( '
' + "" + "" + renderDesktopRows(list) + "
客户端状态标的触发偏离到期h出场开仓时间说明操作
" ); } function renderPhoneList(list, emptyText) { if (!list.length) { return '
' + escapeHtml(emptyText) + "
"; } return '
' + list.map(renderPhoneOrderCard).join("") + "
"; } function fillOrderBox(box, list, emptyText) { if (!box) return; box.innerHTML = isHubPhone() ? renderPhoneList(list, emptyText) : renderDesktopTable(list, emptyText); bindOrderActions(box); } async function loadOrders() { const activeBox = document.getElementById("cond-list-active"); const doneBox = document.getElementById("cond-list-done"); if (!activeBox && !doneBox) return; const r = await fetch("/api/cond_orders"); const d = await r.json(); const list = d.orders || []; const active = []; const done = []; list.forEach(function (o) { if (isActiveOrder(o)) active.push(o); else done.push(o); }); fillOrderBox(activeBox, active, "暂无进行中的条件单"); fillOrderBox(doneBox, done, "暂无已结束的条件单"); } 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 ? "已下发" : "失败")); if (d.ok) { const tog = document.getElementById("cond-add-toggle"); if (tog) tog.checked = false; } 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);