(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 (
'"
);
}
function renderDesktopTable(list, emptyText) {
if (!list.length) {
return '' + escapeHtml(emptyText) + "
";
}
return (
'' +
"| 客户端 | 状态 | 标的 | 触发 | 偏离 | 到期h | 出场 | 开仓时间 | 说明 | 操作 | " +
"
" +
renderDesktopRows(list) +
"
"
);
}
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);