(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: "已取消", timeout: "时间取消", 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 ? "已取消" : "失败")); requestCondRefresh(); }); }); 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 ? "已更新" : "失败")); requestCondRefresh(); }); }); } 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 : ""; } var candidatesById = {}; function fmtTradingU(v) { if (v == null || v === "") return "—"; const n = Number(v); if (Number.isNaN(n)) return "—"; return n.toFixed(2) + "U"; } function openInstanceTab(instanceId, tab) { if (!instanceId) return; const q = tab ? "?tab=" + encodeURIComponent(tab) : ""; fetch("/api/instances/" + encodeURIComponent(instanceId) + "/open_url" + q) .then(function (r) { return r.json(); }) .then(function (d) { const url = d && d.url ? d.url : "/desk?id=" + encodeURIComponent(instanceId); window.open(url, "_blank", "noopener,noreferrer"); }) .catch(function () { window.open( "/desk?id=" + encodeURIComponent(instanceId), "_blank", "noopener,noreferrer" ); }); } function patchCandidateFunds(instanceId, tradingUsdt, fundingUsdt) { const id = String(instanceId); const it = candidatesById[id]; if (it) { if (tradingUsdt != null) it.trading_usdt = tradingUsdt; if (fundingUsdt != null) it.funding_usdt = fundingUsdt; } const row = document.querySelector( '.cond-inst-row[data-inst-id="' + id.replace(/"/g, "") + '"]' ); if (!row) return; const tradeEl = row.querySelector(".cond-inst-funds--trading"); const fundEl = row.querySelector(".cond-inst-funds--funding"); if (tradeEl) tradeEl.textContent = "交易 " + fmtTradingU(tradingUsdt); if (fundEl) fundEl.textContent = "资金 " + fmtTradingU(fundingUsdt); } function transferInternal(instanceId, direction) { const id = String(instanceId || ""); const it = candidatesById[id]; if (!it) return; if (!it.online) { window.alert("实例离线,无法划转"); return; } if (it.transfer_blocked) { window.alert(it.transfer_block_msg || "当前不可划转"); return; } const toTrading = direction !== "to_funding"; const fromAcct = toTrading ? "funding" : "trading"; const toAcct = toTrading ? "trading" : "funding"; const srcBal = Number(toTrading ? it.funding_usdt : it.trading_usdt); const defAmt = !Number.isNaN(srcBal) && srcBal > 0 ? String(Math.round(srcBal * 10000) / 10000) : ""; const label = toTrading ? "资金户 → 交易户" : "交易户 → 资金户"; const raw = window.prompt(label + "(USDT)。本页划转,条件单表单不会丢失。", defAmt); if (raw == null) return; const amount = Number(String(raw).trim()); if (!(amount > 0)) { window.alert("请输入大于 0 的数量"); return; } const btns = document.querySelectorAll( '.cond-inst-xfer[data-inst-id="' + id.replace(/"/g, "") + '"]' ); const labels = []; btns.forEach(function (btn) { labels.push(btn.textContent); btn.disabled = true; btn.textContent = "划转中…"; }); fetch("/api/instances/" + encodeURIComponent(id) + "/transfer", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ amount: amount, from: fromAcct, to: toAcct, ccy: "USDT", }), }) .then(function (r) { return r.json().then(function (d) { return { http: r.status, d: d }; }); }) .then(function (pack) { const d = pack.d || {}; if (!d.ok) { window.alert(d.msg || "划转失败"); return; } patchCandidateFunds(id, d.trading_usdt, d.funding_usdt); const st = document.getElementById("cf-status"); if (st) { st.textContent = (it.name || id) + " " + label + " " + fmtTradingU(amount) + "(交易 " + fmtTradingU(d.trading_usdt) + " / 资金 " + fmtTradingU(d.funding_usdt) + ")"; } }) .catch(function () { window.alert("划转请求失败"); }) .finally(function () { const blocked = !!(it.transfer_blocked || !it.online); btns.forEach(function (btn, i) { btn.disabled = blocked; btn.textContent = labels[i] || btn.getAttribute("data-label") || "划转"; }); }); } 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; const xferBlocked = !!(it.transfer_blocked || !it.online); const xferTitle = xferBlocked ? it.transfer_block_msg || (!it.online ? "离线" : "不可划转") : "本页双向划转,表单不丢"; const dis = xferBlocked ? " disabled" : ""; return ( '
' + '
' + '" + '
' + '交易 ' + escapeHtml(fmtTradingU(it.trading_usdt)) + "" + '资金 ' + escapeHtml(fmtTradingU(it.funding_usdt)) + "" + '' + '" + '" + '' + "" + "
" + "
" + "
" ); } 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 = []; candidatesById = {}; list.forEach(function (it) { const m = instanceMode(it); candidatesById[String(it.id)] = 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); box.querySelectorAll(".cond-inst-xfer").forEach(function (btn) { btn.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); transferInternal( btn.getAttribute("data-inst-id") || "", btn.getAttribute("data-dir") || "to_trading" ); }); }); box.querySelectorAll(".cond-inst-open").forEach(function (btn) { btn.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openInstanceTab(btn.getAttribute("data-inst-id") || "", "options"); }); }); } 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(""); } var PAGE_SIZE = 10; var listPages = { active: 1, done: 1 }; var lastActive = []; var lastDone = []; function pageInfo(list, page) { const total = list.length; const pages = Math.max(1, Math.ceil(total / PAGE_SIZE) || 1); const p = Math.min(Math.max(1, page || 1), pages); const start = (p - 1) * PAGE_SIZE; return { page: p, pages: pages, total: total, slice: list.slice(start, start + PAGE_SIZE) }; } function renderPager(key, info) { if (info.total <= PAGE_SIZE) return ""; return ( '
' + '" + '第 ' + info.page + " / " + info.pages + " 页 · 共 " + info.total + " 条" + '
" ); } 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, key) { if (!box) return; const info = pageInfo(list, listPages[key] || 1); listPages[key] = info.page; const body = isHubPhone() ? renderPhoneList(info.slice, emptyText) : renderDesktopTable(info.slice, emptyText); box.innerHTML = body + renderPager(key, info); bindOrderActions(box); box.querySelectorAll("[data-cond-page]").forEach(function (btn) { btn.addEventListener("click", function () { if (btn.disabled) return; const k = btn.getAttribute("data-cond-page"); const dir = Number(btn.getAttribute("data-dir") || 0); listPages[k] = (listPages[k] || 1) + dir; paintCachedOrders(); }); }); } function paintCachedOrders() { fillOrderBox(document.getElementById("cond-list-active"), lastActive, "暂无进行中的条件单", "active"); fillOrderBox(document.getElementById("cond-list-done"), lastDone, "暂无已结束的条件单", "done"); } let condVersion = 0; let condLoading = false; let condEventSource = null; let condReconnectTimer = null; function setCondStatus(msg) { const el = document.getElementById("cond-status"); if (el) el.textContent = msg || ""; } async function loadOrders(opts) { opts = opts || {}; 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", { credentials: "same-origin" }); const d = await r.json(); const list = d.orders || []; condVersion = Number(d.cond_version) || condVersion; lastActive = []; lastDone = []; list.forEach(function (o) { if (isActiveOrder(o)) lastActive.push(o); else lastDone.push(o); }); paintCachedOrders(); if (!opts.silent) { const live = condEventSource && condEventSource.readyState === 1; const sec = Number(d.poll_interval_sec) || 10; setCondStatus( (live ? "SSE 已连接 · " : "") + "进行中 " + lastActive.length + " · 已结束 " + lastDone.length + (d.updated_at ? " · " + d.updated_at : "") + (d.refreshing ? " · 后台拉取中" : " · 后台每 " + sec + "s 并行拉") ); } } 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; } requestCondRefresh(); } async function refresh(opts) { opts = opts || {}; if (condLoading) return; condLoading = true; if (!opts.silent) setCondStatus("读取缓存…"); try { await Promise.all([loadCandidates(), loadOrders({ silent: !!opts.silent })]); } finally { condLoading = false; } } async function requestCondRefresh() { try { await fetch("/api/cond_orders/refresh", { method: "POST", credentials: "same-origin", }); } catch (_) {} refresh({ silent: true }); } function closeCondStream() { if (condEventSource) { condEventSource.close(); condEventSource = null; } if (condReconnectTimer) { clearTimeout(condReconnectTimer); condReconnectTimer = null; } } function connectCondStream() { if (typeof EventSource === "undefined") return; closeCondStream(); condEventSource = new EventSource("/api/cond_orders/stream"); condEventSource.addEventListener("cond", function (ev) { try { const st = JSON.parse(ev.data || "{}"); const ver = Number(st.cond_version) || 0; if (ver && ver !== condVersion) { refresh({ silent: true }); } else if (st.refreshing) { setCondStatus("后台并行拉取条件单…"); } } catch (_) {} }); condEventSource.onerror = function () { closeCondStream(); setCondStatus("SSE 断开,8s 后重连…"); condReconnectTimer = setTimeout(function () { connectCondStream(); refresh({ silent: true }); }, 8000); }; } function boot() { const btn = document.getElementById("btn-cond-refresh"); if (btn) btn.addEventListener("click", function () { requestCondRefresh(); }); 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(); // 读缓存秒开 + SSE;不再前端定时现场拉实例 refresh(); connectCondStream(); } global.HubCondOrders = { boot: boot, refresh: refresh, requestRefresh: requestCondRefresh, }; })(window);