(function (global) { function fmtPnl(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; return (n > 0 ? "+" : "") + n.toFixed(2) + " U"; } function pnlClass(v) { const n = Number(v); if (!Number.isFinite(n) || n === 0) return "pnl-flat"; return n > 0 ? "pnl-up" : "pnl-down"; } function fmtPnlHtml(v) { return '' + escapeHtml(fmtPnl(v)) + ""; } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function positionSummary(brief) { const list = brief || []; if (!list.length) return '暂无持仓'; const items = list.slice(0, 8).map(function (p) { const id = p.inst_id || ""; const sheets = escapeHtml(String(p.sheets != null ? p.sheets : "—")); return ( "
  • " + escapeHtml(id) + " · " + sheets + "张 · " + fmtPnlHtml(p.pnl_usdt) + "
  • " ); }); const more = list.length > 8 ? '
  • …共 ' + list.length + " 笔
  • " : ""; return ""; } function closeActionButtons(brief, instanceId, online) { if (!online) return ""; const list = (brief || []).filter(function (p) { return !!(p && p.inst_id); }); if (!list.length) return ""; return list .slice(0, 8) .map(function (p) { const id = p.inst_id || ""; const invalid = !!( p.bid_invalid || (p.bid_px == null && (p.bid_sz == null || p.bid_sz === "")) ); const reason = (p.bid_invalid_reason || "买一无效,禁止平仓").toString(); const tip = list.length > 1 ? id + " · 买一限价全平" : "买一限价全平"; if (invalid) { return ( '' ); } return ( '' ); }) .join(""); } function fmtBidPx(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; let s = n.toFixed(4).replace(/\.?0+$/, ""); return s || "0"; } function isLiquidityWeak(p) { if (!p) return false; if (p.liquidity_ok === false || p.bid_invalid) return true; const uncovered = Number(p.uncovered_sheets); return Number.isFinite(uncovered) && uncovered > 0; } function bidLiquidityLine(p) { if (p.bid_invalid || (p.bid_px == null && (p.bid_sz == null || p.bid_sz === ""))) { const reason = (p.bid_invalid_reason || "无买一/残档").toString(); const short = reason.length > 18 ? reason.slice(0, 18) + "…" : reason; return { text: short, weak: true }; } const px = fmtBidPx(p.bid_px); const sz = p.bid_sz != null ? String(p.bid_sz) : "—"; const uncovered = Number(p.uncovered_sheets); let text = px + " / " + sz + "张"; const weak = isLiquidityWeak(p); if (Number.isFinite(uncovered) && uncovered > 0) { text += "(差" + uncovered + ")"; } return { text: text, weak: weak }; } function bidLiquiditySummary(brief) { const list = brief || []; if (!list.length) return '—'; const items = list.slice(0, 8).map(function (p) { const line = bidLiquidityLine(p); const cls = line.weak ? "liq-warn" : "liq-ok"; return '
  • ' + escapeHtml(line.text) + "
  • "; }); const more = list.length > 8 ? '
  • …共 ' + list.length + " 笔
  • " : ""; const anyWeak = list.some(isLiquidityWeak); const wrapCls = anyWeak ? "pos-brief liq-brief liq-brief--warn" : "pos-brief liq-brief"; return '"; } function fmtMoneyU(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; return n.toFixed(2) + " U"; } function fmtRatioPct(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; return n.toFixed(2) + "%"; } function premiumCell(snap) { const prem = snap.premium || {}; const paid = prem.paid_usdt; const equity = prem.equity_usdt; const cash = prem.cash_usdt; const tipParts = []; if (cash != null) tipParts.push("余额 " + fmtMoneyU(cash)); if (paid != null) tipParts.push("开仓占用 " + fmtMoneyU(paid)); if (equity != null) tipParts.push("合计(余额+占用) " + fmtMoneyU(equity)); const tip = tipParts.join(" · ") || "账户资金暂无"; return ( '' + escapeHtml(fmtMoneyU(paid)) + "" ); } function premiumRatioCell(snap) { const prem = snap.premium || {}; const ratio = prem.ratio_pct; const tip = prem.paid_usdt != null && prem.equity_usdt != null ? "开仓占用 " + fmtMoneyU(prem.paid_usdt) + " / (余额+占用) " + fmtMoneyU(prem.equity_usdt) : ""; return ( '' + escapeHtml(fmtRatioPct(ratio)) + "" ); } function renderRow(row) { const snap = row.snapshot || {}; const inst = snap.instance || {}; const pnl = snap.pnl && snap.pnl.unrealized_usdt != null ? snap.pnl.unrealized_usdt : snap.upl_total_usdc; const brief = snap.positions_brief || snap.positions || []; const online = !!row.online; const status = online ? '在线' : '离线'; const err = row.error ? '
    错误: ' + escapeHtml(row.error) + "
    " : ""; return ( "" + "" + escapeHtml(row.name || "") + "" + err + "" + "" + status + "" + "" + escapeHtml(inst.mode || "—") + "" + "" + fmtPnlHtml(pnl) + "" + "" + premiumCell(snap) + "" + "" + premiumRatioCell(snap) + "" + "" + positionSummary(brief) + "" + "" + bidLiquiditySummary(brief) + "" + '' + escapeHtml(row.pulled_at || "—") + "" + '
    ' + '内嵌打开' + '' + closeActionButtons(brief, row.id, online) + "
    " + "" ); } async function requestClose(btn) { const instanceId = decodeURIComponent(btn.getAttribute("data-close-inst") || ""); const optId = decodeURIComponent(btn.getAttribute("data-close-opt") || ""); if (!instanceId || !optId) return; if ( !confirm( "确认向该客户端下发买一限价全平?\n\n合约: " + optId + "\n\n无效买一将被拒绝;下单在客户端本地执行。" ) ) { return; } const oldText = btn.textContent; btn.disabled = true; btn.textContent = "平仓中…"; try { const rr = await fetch("/api/instances/" + encodeURIComponent(instanceId) + "/options/close", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ inst_id: optId }), }); let dd = {}; try { dd = await rr.json(); } catch (e) { dd = {}; } if (dd && dd.ok) { let msg = dd.msg || "已提交买一全平"; if (dd.submitted_sheets != null) msg += " · " + dd.submitted_sheets + " 张"; if (dd.fully_closed) msg += " · 已全平"; alert(msg); await refresh(); } else { alert((dd && dd.msg) || "平仓失败"); btn.disabled = false; btn.textContent = oldText; } } catch (e) { alert((e && e.message) || "请求失败"); btn.disabled = false; btn.textContent = oldText; } } function numOrNull(v) { if (v == null || v === "") return null; const n = Number(v); return Number.isFinite(n) ? n : null; } function emptyAgg() { return { machines: 0, online: 0, offline: 0, positions: 0, pnl: 0, premium: 0, cash: 0, hasPnl: false, hasPremium: false, hasCash: false, }; } function addOnlineMetrics(bucket, snap) { const brief = snap.positions_brief || snap.positions || []; bucket.positions += brief.length; const pnl = snap.pnl && snap.pnl.unrealized_usdt != null ? numOrNull(snap.pnl.unrealized_usdt) : numOrNull(snap.upl_total_usdc); if (pnl != null) { bucket.pnl += pnl; bucket.hasPnl = true; } const prem = snap.premium || {}; const paid = numOrNull(prem.paid_usdt); const cash = numOrNull(prem.cash_usdt); if (paid != null) { bucket.premium += paid; bucket.hasPremium = true; } if (cash != null) { bucket.cash += cash; bucket.hasCash = true; } } function summarizeOverview(list) { const all = emptyAgg(); const sim = emptyAgg(); const live = emptyAgg(); (list || []).forEach(function (row) { const snap = row.snapshot || {}; const mode = String((snap.instance || {}).mode || "").toLowerCase(); const online = !!row.online; const targets = [all]; if (mode === "sim") targets.push(sim); else if (mode === "live") targets.push(live); targets.forEach(function (b) { b.machines += 1; if (online) b.online += 1; else b.offline += 1; }); if (!online) return; addOnlineMetrics(all, snap); if (mode === "sim") addOnlineMetrics(sim, snap); else if (mode === "live") addOnlineMetrics(live, snap); }); return { all: all, sim: sim, live: live }; } function fmtMoneyPlain(v, known) { if (!known || v == null || !Number.isFinite(Number(v))) return "—"; return Number(v).toFixed(2) + " U"; } function renderOverviewSummary(list) { const box = document.getElementById("overview-summary"); if (!box) return; if (!list || !list.length) { box.hidden = true; box.innerHTML = ""; return; } const s = summarizeOverview(list); function modeRow(label, tag, bucket) { return ( "" + '' + escapeHtml(label) + "" + '' + bucket.online + "" + '' + bucket.offline + "" + "" + bucket.positions + "" + '' + escapeHtml(bucket.hasPnl ? fmtPnl(bucket.pnl) : "—") + "" + "" + escapeHtml(fmtMoneyPlain(bucket.premium, bucket.hasPremium)) + "" + "" + escapeHtml(fmtMoneyPlain(bucket.cash, bucket.hasCash)) + "" + "" ); } box.hidden = false; box.innerHTML = '
    ' + '
    实时汇总仅统计在线机资金与仓位
    ' + '' + "" + "" + "" + modeRow("sim", "sim", s.sim) + modeRow("live", "live", s.live) + "
    模式在线离线仓位浮盈权利金余额
    "; } async function refresh() { const status = document.getElementById("overview-status"); const grid = document.getElementById("overview-grid"); if (!grid) return; if (status) status.textContent = "刷新中…"; try { const r = await fetch("/api/overview"); const d = await r.json(); if (!d.ok) throw new Error(d.msg || "失败"); const list = d.instances || []; renderOverviewSummary(list); if (!list.length) { grid.innerHTML = '

    暂无启用实例,请先到「实例」添加.

    '; } else { grid.innerHTML = '
    ' + "" + "" + "" + list.map(renderRow).join("") + "
    名称状态模式浮盈权利金资金占比持仓买一/流动性拉取时间操作
    "; grid.querySelectorAll("[data-open-tab]").forEach(function (btn) { btn.addEventListener("click", async function () { const id = btn.getAttribute("data-open-tab"); const rr = await fetch("/api/instances/" + id + "/open_url"); const dd = await rr.json(); if (dd.ok && dd.url) window.open(dd.url, "_blank", "noopener"); else alert(dd.msg || "无法打开"); }); }); grid.querySelectorAll("[data-close-opt]").forEach(function (btn) { btn.addEventListener("click", function () { requestClose(btn); }); }); } if (status) { const s = summarizeOverview(list); status.textContent = "已更新 · 共 " + list.length + " 台 · 在线 " + s.all.online + " · 离线 " + s.all.offline; } } catch (e) { if (status) status.textContent = e.message || "加载失败"; const box = document.getElementById("overview-summary"); if (box) { box.hidden = true; box.innerHTML = ""; } } } function formatBeijingNow() { try { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).formatToParts(new Date()); const get = function (type) { const hit = parts.find(function (p) { return p.type === type; }); return hit ? hit.value : ""; }; return ( get("year") + "-" + get("month") + "-" + get("day") + " " + get("hour") + ":" + get("minute") + ":" + get("second") ); } catch (e) { const d = new Date(); const utc = d.getTime() + d.getTimezoneOffset() * 60000; const bj = new Date(utc + 8 * 3600000); const p = function (n) { return String(n).padStart(2, "0"); }; return ( bj.getFullYear() + "-" + p(bj.getMonth() + 1) + "-" + p(bj.getDate()) + " " + p(bj.getHours()) + ":" + p(bj.getMinutes()) + ":" + p(bj.getSeconds()) ); } } function tickBeijingClock() { const el = document.getElementById("overview-bj-time"); if (!el) return; const text = formatBeijingNow(); el.textContent = text; el.setAttribute("datetime", text.replace(" ", "T") + "+08:00"); } function boot() { const btn = document.getElementById("btn-refresh"); if (btn) btn.addEventListener("click", refresh); tickBeijingClock(); setInterval(tickBeijingClock, 1000); refresh(); setInterval(refresh, 10000); } global.HubOverview = { boot: boot, refresh: refresh }; })(window);