ab47fb6142
Co-authored-by: Cursor <cursoragent@cursor.com>
742 lines
25 KiB
JavaScript
742 lines
25 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: "已取消",
|
||
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 +=
|
||
'<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 ? "已取消" : "失败"));
|
||
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 (
|
||
'<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>" : "") +
|
||
'<div class="hub-phone-cond-meta muted">开仓 ' +
|
||
escapeHtml(formatOpenTime(o)) +
|
||
"</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 : "";
|
||
}
|
||
|
||
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 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 (
|
||
'<div class="cond-inst-row' +
|
||
(disabled ? " is-off" : "") +
|
||
'" data-inst-id="' +
|
||
escapeHtml(id) +
|
||
'">' +
|
||
'<div class="cond-inst' +
|
||
(disabled ? " is-off" : "") +
|
||
'">' +
|
||
'<label class="cond-inst-head" 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>" +
|
||
'<div class="cond-inst-body">' +
|
||
'<span class="cond-inst-funds cond-inst-funds--trading" title="交易账户可用">交易 ' +
|
||
escapeHtml(fmtTradingU(it.trading_usdt)) +
|
||
"</span>" +
|
||
'<span class="cond-inst-funds cond-inst-funds--funding muted" title="资金账户可用">资金 ' +
|
||
escapeHtml(fmtTradingU(it.funding_usdt)) +
|
||
"</span>" +
|
||
'<span class="cond-inst-actions">' +
|
||
'<button type="button" class="btn btn-secondary btn-sm cond-inst-xfer" data-inst-id="' +
|
||
escapeHtml(id) +
|
||
'" data-dir="to_trading" data-label="划入交易" title="' +
|
||
escapeHtml(xferTitle) +
|
||
'"' +
|
||
dis +
|
||
">划入交易</button>" +
|
||
'<button type="button" class="btn btn-secondary btn-sm cond-inst-xfer" data-inst-id="' +
|
||
escapeHtml(id) +
|
||
'" data-dir="to_funding" data-label="划入资金" title="' +
|
||
escapeHtml(xferTitle) +
|
||
'"' +
|
||
dis +
|
||
">划入资金</button>" +
|
||
"</span>" +
|
||
"</div>" +
|
||
"</div>" +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
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 = [];
|
||
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"
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderDesktopRows(list) {
|
||
return 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="cond-td-time">' +
|
||
escapeHtml(formatOpenTime(o)) +
|
||
"</td>" +
|
||
'<td class="muted">' +
|
||
escapeHtml(o.message || "") +
|
||
"</td>" +
|
||
'<td><div class="overview-actions">' +
|
||
orderActionHtml(o) +
|
||
"</div></td>" +
|
||
"</tr>"
|
||
);
|
||
})
|
||
.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 (
|
||
'<div class="cond-pager">' +
|
||
'<button type="button" class="btn btn-sm" data-cond-page="' +
|
||
key +
|
||
'" data-dir="-1"' +
|
||
(info.page <= 1 ? " disabled" : "") +
|
||
">上一页</button>" +
|
||
'<span class="muted">第 ' +
|
||
info.page +
|
||
" / " +
|
||
info.pages +
|
||
" 页 · 共 " +
|
||
info.total +
|
||
" 条</span>" +
|
||
'<button type="button" class="btn btn-sm" data-cond-page="' +
|
||
key +
|
||
'" data-dir="1"' +
|
||
(info.page >= info.pages ? " disabled" : "") +
|
||
">下一页</button></div>"
|
||
);
|
||
}
|
||
|
||
function renderDesktopTable(list, emptyText) {
|
||
if (!list.length) {
|
||
return '<div class="cond-empty">' + escapeHtml(emptyText) + "</div>";
|
||
}
|
||
return (
|
||
'<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><th>操作</th>" +
|
||
"</tr></thead><tbody>" +
|
||
renderDesktopRows(list) +
|
||
"</tbody></table></div>"
|
||
);
|
||
}
|
||
|
||
function renderPhoneList(list, emptyText) {
|
||
if (!list.length) {
|
||
return '<div class="cond-empty">' + escapeHtml(emptyText) + "</div>";
|
||
}
|
||
return '<div class="hub-phone-cond-list">' + list.map(renderPhoneOrderCard).join("") + "</div>";
|
||
}
|
||
|
||
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);
|