/** * 实例数据看板:拉 /api/instance/dashboard 渲染只读表格. * 各区块无数据时不展示;有数据按表格展示. */ (function (global) { const SECTION_ORDER = ["orders", "keys", "strategy", "options", "hedge_plan"]; let loading = false; let timer = null; function root() { const active = document.querySelector('.embed-tab-pane.is-active-pane [data-inst-dashboard="1"]'); if (active) return active; return document.getElementById("instance-dashboard"); } function escapeHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function fmtNum(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return escapeHtml(v); return String(n); } function fmtPnl(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; const cls = n > 0 ? "pos-pnl-profit" : n < 0 ? "pos-pnl-loss" : ""; const sign = n > 0 ? "+" : ""; return '' + sign + n.toFixed(2) + "U"; } function fmtPnlPlain(v) { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return "—"; const cls = n > 0 ? "pos-pnl-profit" : n < 0 ? "pos-pnl-loss" : ""; return '' + n.toFixed(2) + ""; } function dirCell(it) { const d = String(it.direction || "").toLowerCase(); const label = it.direction_label || (d === "short" ? "做空" : d === "long" ? "做多" : "-"); const cls = d === "short" ? "inst-dash-dir-short" : d === "long" ? "inst-dash-dir-long" : ""; return '' + escapeHtml(label) + ""; } function fmtExpiry(ms) { const n = Number(ms); if (!Number.isFinite(n) || n <= 0) return "—"; let fallback = "—"; try { const d = new Date(n); if (!Number.isNaN(d.getTime())) { const pad = function (x) { return String(x).padStart(2, "0"); }; fallback = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()); } } catch (_) {} return ( '' + escapeHtml(fallback) + "" ); } function goTab(tab) { if (!tab) return; if (global.InstanceEmbed && typeof global.InstanceEmbed.loadTab === "function") { global.InstanceEmbed.loadTab(tab); return; } const pathMap = { trade: "/trade", key_monitor: "/key_monitor", strategy: "/strategy", options: "/options", hedge_plan: "/hedge-plan", }; const path = pathMap[tab] || "/" + tab; location.href = path; } function tableWrap(headers, rowsHtml) { return ( '
' + '' + "" + headers .map(function (h) { return ""; }) .join("") + "" + "" + rowsHtml + "
" + escapeHtml(h) + "
" ); } function rowClickAttrs(tab) { return ' class="inst-dash-row" data-dash-tab="' + escapeHtml(tab || "") + '" role="link" tabindex="0"'; } function mergeOrderLive(items, orderPrices) { const map = {}; (orderPrices || []).forEach(function (p) { if (p && p.id != null) map[String(p.id)] = p; }); return (items || []).map(function (it) { const live = map[String(it.id)] || {}; const mark = live.exchange_mark_price != null ? live.exchange_mark_price : live.price != null ? live.price : it.mark_price; const contracts = live.contracts != null ? live.contracts : live.order_amount != null ? live.order_amount : it.contracts; const entry = live.avg_entry_price != null ? live.avg_entry_price : it.entry; return Object.assign({}, it, { entry: entry, mark_price: mark, mark_display: live.price_display || null, contracts: contracts, tp_profit: live.reward_at_tp_usdt != null ? live.reward_at_tp_usdt : it.tp_profit, float_pnl: live.float_pnl != null ? live.float_pnl : it.float_pnl, }); }); } function renderOrdersTable(items) { const rows = items .map(function (it) { const sym = it.symbol || "-"; const mark = it.mark_display != null && it.mark_display !== "" ? escapeHtml(it.mark_display) : fmtNum(it.mark_price); const tpProfit = it.tp_profit != null && Number.isFinite(Number(it.tp_profit)) ? '' + Number(it.tp_profit).toFixed(2) + "U" : "—"; return ( "" + '' + escapeHtml(sym) + "" + dirCell(it) + "" + fmtNum(it.entry) + "" + "" + mark + "" + "" + fmtNum(it.contracts) + "" + "" + tpProfit + "" + "" + fmtPnlPlain(it.float_pnl) + "" + "—" + "" ); }) .join(""); return tableWrap( ["合约", "方向", "开仓价", "标记价", "张数", "盈利金额", "浮盈", "操作"], rows ); } function renderKeysTable(items) { const rows = items .map(function (it) { return ( "" + "" + escapeHtml(it.symbol || "-") + "" + dirCell(it) + "" + escapeHtml(it.subtitle || "—") + "" + "" + fmtNum(it.upper) + "" + "" + fmtNum(it.lower) + "" + "" ); }) .join(""); return tableWrap(["合约", "方向", "信号", "上沿", "下沿"], rows); } function renderStrategyTable(items) { const rows = items .map(function (it) { const kindLabel = it.kind === "roll" ? "顺势加仓" : it.kind === "trend" ? "趋势回调" : "策略"; return ( "" + "" + escapeHtml(kindLabel) + "" + "" + escapeHtml(it.symbol || "-") + "" + dirCell(it) + "" + escapeHtml(it.status || "—") + "" + "" + fmtNum(it.entry) + "" + "" ); }) .join(""); return tableWrap(["类型", "合约", "方向", "状态", "入场"], rows); } function renderOptionsTable(items) { const rows = items .map(function (it) { const opt = it.opt_type_label || (String(it.opt_type || "").toUpperCase() === "C" ? "Call" : String(it.opt_type || "").toUpperCase() === "P" ? "Put" : it.opt_type || "—"); return ( "" + "" + escapeHtml(it.inst_id || it.title || "-") + "" + "" + escapeHtml(it.source_label || "纯期权") + "" + "" + escapeHtml(opt) + "" + "" + fmtNum(it.pos) + "" + "" + fmtExpiry(it.exp_time_ms) + "" + "" + escapeHtml(it.target_monitor || "—") + "" + "" + fmtPnl(it.pnl) + "" + "" ); }) .join(""); return tableWrap(["合约", "来源", "类型", "张数", "到期时间", "目标监控", "盈亏"], rows); } function renderHedgeTable(items) { const rows = items .map(function (it) { const stCls = it.status_active ? "inst-dash-status-active" : ""; const stText = it.status_label || (it.status_active ? "进行中" : it.status || "—"); return ( "" + "#" + escapeHtml(it.id != null ? it.id : "—") + "" + "" + escapeHtml(it.underlying || "-") + "" + "" + escapeHtml(it.plan_type_label || it.plan_type || "—") + "" + '' + escapeHtml(stText) + "" + "" + escapeHtml(it.contracts_summary || it.subtitle || "—") + "" + "" ); }) .join(""); return tableWrap(["ID", "标的", "计划类型", "状态", "说明"], rows); } function renderTable(key, items) { if (key === "orders") return renderOrdersTable(items); if (key === "keys") return renderKeysTable(items); if (key === "strategy") return renderStrategyTable(items); if (key === "options") return renderOptionsTable(items); if (key === "hedge_plan") return renderHedgeTable(items); return ""; } function sectionHasData(sec) { if (!sec) return false; const count = Number(sec.count); if (Number.isFinite(count) && count > 0) return true; return Array.isArray(sec.items) && sec.items.length > 0; } function renderSection(key, sec) { if (!sectionHasData(sec)) return ""; const items = sec.items || []; const count = Number(sec.count) || items.length; return ( '
' + '
' + "

" + escapeHtml(sec.title || key) + ' ' + count + "

" + '' + "
" + renderTable(key, items) + "
" ); } function bindClicks(el) { if (!el) return; el.querySelectorAll("[data-dash-tab]").forEach(function (node) { const handler = function () { goTab(node.getAttribute("data-dash-tab")); }; node.addEventListener("click", handler); node.addEventListener("keydown", function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); handler(); } }); }); } async function load(opts) { const el = root(); if (!el) return; const status = el.querySelector("#inst-dash-status") || document.getElementById("inst-dash-status"); const sections = el.querySelector("#inst-dash-sections") || document.getElementById("inst-dash-sections"); const updated = el.querySelector("#inst-dash-updated") || document.getElementById("inst-dash-updated"); if (loading) return; loading = true; if (status && !(opts && opts.silent)) status.textContent = "加载中…"; try { const [dashRes, priceRes] = await Promise.all([ fetch("/api/instance/dashboard", { credentials: "same-origin" }), fetch("/api/price_snapshot", { credentials: "same-origin" }).catch(function () { return null; }), ]); const data = await dashRes.json().catch(function () { return {}; }); if (!dashRes.ok || !data.ok) { throw new Error(data.msg || dashRes.statusText || "加载失败"); } let orderPrices = []; if (priceRes && priceRes.ok) { try { const snap = await priceRes.json(); orderPrices = snap.order_prices || []; } catch (_) {} } if (data.orders && Array.isArray(data.orders.items)) { data.orders.items = mergeOrderLive(data.orders.items, orderPrices); data.orders.count = data.orders.items.length; } if (updated) updated.textContent = "更新 " + (data.updated_at || "—"); if (sections) { const html = SECTION_ORDER.map(function (k) { return renderSection(k, data[k]); }).join(""); sections.innerHTML = html || '

当前无活跃监控与持仓

'; bindClicks(sections); if (global.OptionsExpiryCountdown) { if (typeof global.OptionsExpiryCountdown.tick === "function") { global.OptionsExpiryCountdown.tick(sections); } if (typeof global.OptionsExpiryCountdown.ensureTimer === "function") { global.OptionsExpiryCountdown.ensureTimer(); } } } if (status) status.textContent = ""; } catch (e) { if (status) status.textContent = e.message || "加载失败"; } finally { loading = false; } } function stopAuto() { if (timer) { clearInterval(timer); timer = null; } } function startAuto() { stopAuto(); timer = setInterval(function () { const el = root(); if (!el) return; const pane = el.closest(".embed-tab-pane"); if (pane && !pane.classList.contains("is-active-pane")) return; load({ silent: true }); }, 15000); } function init(force) { const el = root(); if (!el) return; if (!force && el.getAttribute("data-dash-booted") === "1") { load({ silent: true }); startAuto(); return; } el.setAttribute("data-dash-booted", "1"); const btn = el.querySelector("#inst-dash-refresh") || document.getElementById("inst-dash-refresh"); if (btn && !btn.getAttribute("data-bound")) { btn.setAttribute("data-bound", "1"); btn.addEventListener("click", function () { load({}); }); } load({}); startAuto(); } function refreshSoft(opts) { load(Object.assign({ silent: true }, opts || {})); } global.InstanceDashboard = { init: init, refreshSoft: refreshSoft, load: load, stopAuto: stopAuto, }; })(window);