diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py
index f72a1b4..98b5af6 100644
--- a/manual_trading_hub/hub.py
+++ b/manual_trading_hub/hub.py
@@ -43,7 +43,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN")
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
DIR = Path(__file__).resolve().parent
-HUB_BUILD = "20260525-orders-ui"
+HUB_BUILD = "20260525-expand-ui"
def _is_local(host: str | None) -> bool:
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index 5f76f6d..44eda7d 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -493,6 +493,290 @@ button:disabled {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
+.grid-monitor.grid-monitor-expanded {
+ grid-template-columns: minmax(0, 1fr);
+ max-width: 720px;
+ margin: 0 auto;
+}
+
+.card.card-expanded {
+ grid-column: 1 / -1;
+}
+
+.card-expand-hit {
+ cursor: pointer;
+}
+
+.card-expand-hint {
+ margin-top: 12px;
+ padding: 8px 10px;
+ font-size: 11px;
+ color: var(--muted);
+ text-align: center;
+ border: 1px dashed var(--border-soft);
+ border-radius: 8px;
+ background: rgba(0, 212, 255, 0.03);
+}
+
+.compact-pos-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.compact-pos-line {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ padding: 6px 8px;
+ background: rgba(0, 0, 0, 0.25);
+ border-radius: 6px;
+ border: 1px solid var(--border-soft);
+}
+
+.hub-pos-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+/* 对齐实盘「实时持仓」pos-card */
+.hub-pos-card.pos-card {
+ background: rgba(10, 16, 28, 0.95);
+ border: 1px solid var(--border-soft);
+ border-radius: 10px;
+ padding: 12px 14px;
+}
+
+.hub-pos-card .pos-card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.hub-pos-card .pos-card-symbol {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+
+.hub-pos-card .pos-card-symbol strong {
+ font-size: 14px;
+ color: var(--text);
+ font-weight: 600;
+}
+
+.hub-pos-card .pos-side-badge {
+ padding: 3px 8px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.hub-pos-card .pos-side-long {
+ background: rgba(37, 58, 110, 0.9);
+ color: #6eb5ff;
+}
+
+.hub-pos-card .pos-side-short {
+ background: rgba(74, 34, 48, 0.9);
+ color: #ff8a8a;
+}
+
+.hub-pos-card .pos-head-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.hub-pos-card .pos-entrust-btn {
+ padding: 6px 12px;
+ background: rgba(42, 74, 122, 0.9);
+ color: #8fc8ff;
+ border: 1px solid rgba(0, 212, 255, 0.25);
+ border-radius: 8px;
+ font-size: 12px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.hub-pos-card .pos-close-btn {
+ padding: 6px 14px;
+ background: rgba(196, 84, 84, 0.95);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 12px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.hub-pos-card .pos-meta {
+ font-size: 11px;
+ color: var(--muted);
+ line-height: 1.45;
+ margin-bottom: 12px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 0;
+}
+
+.hub-pos-card .pos-meta-item:not(:last-child)::after {
+ content: "|";
+ margin: 0 8px;
+ color: var(--border-soft);
+}
+
+.hub-pos-card .pos-meta-on {
+ color: #6eb5ff;
+}
+
+.hub-pos-card .pos-meta-off {
+ color: var(--muted);
+}
+
+.hub-pos-card .pos-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px 14px;
+ margin-bottom: 12px;
+}
+
+.hub-pos-card .pos-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.hub-pos-card .pos-label {
+ font-size: 10px;
+ color: var(--muted);
+ letter-spacing: 0.04em;
+}
+
+.hub-pos-card .pos-value {
+ font-size: 13px;
+ color: var(--text);
+ font-weight: 500;
+}
+
+.hub-pos-card .pos-footer {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px 16px;
+ font-size: 11px;
+ color: var(--muted);
+ margin-bottom: 4px;
+}
+
+.hub-pos-card .pos-ex-orders {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px dashed var(--border-soft);
+}
+
+.hub-pos-card .pos-ex-orders-title {
+ font-size: 11px;
+ color: var(--muted);
+ margin-bottom: 6px;
+}
+
+.hub-pos-card .pos-ex-order-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.hub-pos-card .pos-ex-order-main {
+ flex: 1;
+ min-width: 0;
+}
+
+.hub-pos-card .pos-ex-cancel-btn {
+ padding: 3px 10px;
+ background: rgba(58, 48, 72, 0.9);
+ color: #d4b8ff;
+ border: 1px solid rgba(123, 97, 255, 0.35);
+ border-radius: 6px;
+ font-size: 11px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.hub-pos-card .pos-orders-collapse {
+ margin-top: 10px;
+}
+
+.hub-section-card {
+ margin-top: 14px;
+ padding: 12px 14px;
+ background: rgba(0, 0, 0, 0.22);
+ border: 1px solid var(--border-soft);
+ border-radius: 10px;
+}
+
+.hub-section-head {
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 10px;
+}
+
+.hub-section-body {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.hub-mini-card {
+ padding: 10px 12px;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid var(--border-soft);
+ border-radius: 8px;
+}
+
+.hub-mini-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 4px;
+}
+
+.hub-mini-line {
+ font-size: 11px;
+ color: var(--muted);
+ line-height: 1.45;
+}
+
+.pos-empty {
+ padding: 18px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 12px;
+ border: 1px dashed var(--border-soft);
+ border-radius: 10px;
+}
+
+@media (max-width: 520px) {
+ .hub-pos-card .pos-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
.settings-grid-wrap {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 564d924..0486eee 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -4,6 +4,8 @@
let monitorTimer = null;
let authState = { required: false, logged_in: true };
let tpslPending = null;
+ let lastMonitorRows = [];
+ let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
async function apiFetch(url, opts) {
const r = await fetch(url, opts);
@@ -113,62 +115,166 @@
gridEl.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
}
+ function normSym(s) {
+ return String(s || "")
+ .toUpperCase()
+ .replace(/:USDT$/i, "")
+ .replace(/\/USDT:USDT$/i, "")
+ .replace(/\/USDT$/i, "");
+ }
+
+ function symbolsMatchHub(a, b) {
+ const x = normSym(a);
+ const y = normSym(b);
+ if (!x || !y) return false;
+ return x === y;
+ }
+
+ function ordersCollapseKey(exchangeId, symbol) {
+ const sym = normSym(symbol) || "unknown";
+ return `hub_orders_${exchangeId}_${sym}`;
+ }
+
+ function isOrdersCollapseOpen(exchangeId, symbol) {
+ return localStorage.getItem(ordersCollapseKey(exchangeId, symbol)) === "1";
+ }
+
+ function findMonitorOrder(orders, symbol, side) {
+ const want = (side || "").toLowerCase();
+ for (const o of orders || []) {
+ const sym = o.exchange_symbol || o.symbol || "";
+ if (!symbolsMatchHub(sym, symbol)) continue;
+ const d = (o.direction || "").toLowerCase();
+ if (!d || d === want) return o;
+ }
+ return null;
+ }
+
+ function calcRrRatio(side, entry, sl, tp) {
+ const e = Number(entry);
+ const s = Number(sl);
+ const t = Number(tp);
+ if (![e, s, t].every((n) => Number.isFinite(n) && n > 0)) return null;
+ if ((side || "long").toLowerCase() === "short") {
+ const risk = s - e;
+ const reward = e - t;
+ if (risk <= 0 || reward <= 0) return null;
+ return reward / risk;
+ }
+ const risk = e - s;
+ const reward = t - e;
+ if (risk <= 0 || reward <= 0) return null;
+ return reward / risk;
+ }
+
async function loadMonitorBoard() {
const box = document.getElementById("monitor-grid");
try {
const r = await apiFetch("/api/monitor/board");
const data = await r.json();
- const rows = data.rows || [];
- const online = rows.filter((x) => x.http_ok && (x.agent || {}).ok !== false).length;
+ lastMonitorRows = data.rows || [];
+ const online = lastMonitorRows.filter(
+ (x) => x.http_ok && (x.agent || {}).ok !== false
+ ).length;
const pill = document.getElementById("sys-status");
if (pill) {
- pill.textContent = rows.length ? `LINK ${online}/${rows.length}` : "NO DATA";
- pill.classList.toggle("warn", rows.length && online < rows.length);
+ pill.textContent = lastMonitorRows.length
+ ? `LINK ${online}/${lastMonitorRows.length}`
+ : "NO DATA";
+ pill.classList.toggle("warn", lastMonitorRows.length && online < lastMonitorRows.length);
}
document.getElementById("monitor-updated").textContent =
"UPD " + (data.updated_at || "").replace("T", " ");
- const parts = rows.map(renderMonitorCard);
- box.innerHTML = parts.join("") || '
无已启用账户
';
- syncMonitorGridColumns(box, rows.length);
- box.querySelectorAll(".btn-close-ex").forEach((btn) => {
- btn.onclick = () => closeOne(btn.dataset.id);
- });
- box.querySelectorAll(".btn-close-pos").forEach((btn) => {
- btn.onclick = () =>
- closeOnePosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.side);
- });
- box.querySelectorAll(".btn-cancel-order").forEach((btn) => {
- btn.onclick = () =>
- cancelOneOrder(
- btn.dataset.exId,
- btn.dataset.symbol,
- btn.dataset.orderId,
- btn.dataset.channel
- );
- });
- box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
- btn.onclick = (ev) => {
- ev.preventDefault();
- ev.stopPropagation();
- cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
- };
- });
- box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
- btn.onclick = () =>
- openTpslModal(
- btn.dataset.exId,
- btn.dataset.symbol,
- btn.dataset.side,
- btn.dataset.contracts,
- btn.dataset.sl || "",
- btn.dataset.tp || ""
- );
- });
+ renderMonitorGrid(lastMonitorRows);
} catch (e) {
box.innerHTML = `${esc(e)}
`;
}
}
+ function renderMonitorGrid(rows) {
+ const box = document.getElementById("monitor-grid");
+ if (!box) return;
+ if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
+ expandedExchangeId = "";
+ sessionStorage.removeItem("hub_expanded_ex");
+ }
+ const visible = expandedExchangeId
+ ? rows.filter((r) => String(r.id) === String(expandedExchangeId))
+ : rows;
+ box.classList.toggle("grid-monitor-expanded", !!expandedExchangeId);
+ const parts = visible.map((r) => renderMonitorCard(r, !!expandedExchangeId));
+ box.innerHTML = parts.join("") || '无已启用账户
';
+ if (!expandedExchangeId) syncMonitorGridColumns(box, rows.length);
+ bindMonitorInteractions(box);
+ }
+
+ function bindMonitorInteractions(box) {
+ box.querySelectorAll(".btn-close-ex").forEach((btn) => {
+ btn.onclick = () => closeOne(btn.dataset.id);
+ });
+ box.querySelectorAll(".btn-close-pos").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.stopPropagation();
+ closeOnePosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.side);
+ };
+ });
+ box.querySelectorAll(".btn-cancel-order").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.stopPropagation();
+ cancelOneOrder(
+ btn.dataset.exId,
+ btn.dataset.symbol,
+ btn.dataset.orderId,
+ btn.dataset.channel
+ );
+ };
+ });
+ box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
+ };
+ });
+ box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.stopPropagation();
+ openTpslModal(
+ btn.dataset.exId,
+ btn.dataset.symbol,
+ btn.dataset.side,
+ btn.dataset.contracts,
+ btn.dataset.sl || "",
+ btn.dataset.tp || ""
+ );
+ };
+ });
+ box.querySelectorAll(".btn-expand-back").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.stopPropagation();
+ expandedExchangeId = "";
+ sessionStorage.removeItem("hub_expanded_ex");
+ renderMonitorGrid(lastMonitorRows);
+ };
+ });
+ box.querySelectorAll(".card-expand-hit").forEach((hit) => {
+ hit.onclick = (ev) => {
+ if (ev.target.closest("a, button, input, summary, .pos-orders-collapse")) return;
+ const id = hit.closest(".card")?.dataset.exId;
+ if (!id || expandedExchangeId) return;
+ expandedExchangeId = id;
+ sessionStorage.setItem("hub_expanded_ex", id);
+ renderMonitorGrid(lastMonitorRows);
+ };
+ });
+ box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => {
+ el.addEventListener("toggle", () => {
+ const k = el.dataset.collapseKey;
+ if (k) localStorage.setItem(k, el.open ? "1" : "0");
+ });
+ });
+ }
+
function renderOrderRows(exchangeId, symbol, orders, kind) {
if (!orders || !orders.length) {
const hint =
@@ -209,38 +315,18 @@
return { sl: triggers[0], tp: triggers[triggers.length - 1] };
}
- function renderPositionBlock(exchangeId, x) {
- const symAttr = esc(x.symbol || "").replace(/"/g, """);
- const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
- const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
- const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
- const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
- const guess = guessTpslFromCondOrders(x.side, cond);
- const slAttr = esc(String(guess.sl)).replace(/"/g, """);
- const tpAttr = esc(String(guess.tp)).replace(/"/g, """);
+ function renderOrdersCollapse(exchangeId, symbol, cond, reg) {
+ const symAttr = esc(symbol || "").replace(/"/g, """);
const orderTotal = cond.length + reg.length;
+ const collapseKey = ordersCollapseKey(exchangeId, symbol);
+ const openAttr = isOrdersCollapseOpen(exchangeId, symbol) ? " open" : "";
const condAllBtn =
cond.length > 0
? ``
: "";
- const condBody = renderOrderRows(exchangeId, x.symbol, cond, "conditional");
- const regBody = renderOrderRows(exchangeId, x.symbol, reg, "limit");
- return `
-
| 合约 | 方向 | 张数 | 浮盈 | 操作 |
-
- | ${esc(x.symbol)} |
- ${esc(x.side)} |
- ${fmt(x.contracts, 4)} |
- ${fmt(x.unrealized_pnl, 4)} |
-
-
-
-
-
- |
-
-
-
+ const condBody = renderOrderRows(exchangeId, symbol, cond, "conditional");
+ const regBody = renderOrderRows(exchangeId, symbol, reg, "limit");
+ return `
委托单 ${orderTotal}
条件 ${cond.length} · 普通 ${reg.length}
@@ -256,10 +342,198 @@
${regBody}
-
+ `;
+ }
+
+ function renderExTpslRows(exchangeId, symbol, cond) {
+ const symAttr = esc(symbol || "").replace(/"/g, """);
+ const sl = cond.find((o) => (o.label || "").includes("止损"));
+ const tp = cond.find((o) => (o.label || "").includes("止盈"));
+ function row(label, o) {
+ if (!o) {
+ return `${label}:—
`;
+ }
+ const oid = esc(o.id || "").replace(/"/g, """);
+ const ch = esc(o.channel || "regular").replace(/"/g, """);
+ const trig = o.trigger_price != null ? fmt(o.trigger_price, 4) : "—";
+ return `
+ ${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}
+
+
`;
+ }
+ return row("止损", sl) + row("止盈", tp);
+ }
+
+ function renderLivePositionCard(exchangeId, pos, monitorOrder) {
+ const symbol = pos.symbol || "";
+ const side = (pos.side || "long").toLowerCase();
+ const sideCn = side === "long" ? "做多" : "做空";
+ const sideCls = side === "long" ? "pos-side-long" : "pos-side-short";
+ const mo = monitorOrder || {};
+ const cond = Array.isArray(pos.conditional_orders) ? pos.conditional_orders : [];
+ const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
+ const guess = guessTpslFromCondOrders(side, cond);
+ const symAttr = esc(symbol).replace(/"/g, """);
+ const sideAttr = esc(side).replace(/"/g, """);
+ const contractsAttr = esc(String(pos.contracts != null ? pos.contracts : "")).replace(/"/g, """);
+ const slAttr = esc(String(mo.stop_loss != null ? mo.stop_loss : guess.sl)).replace(/"/g, """);
+ const tpAttr = esc(String(mo.take_profit != null ? mo.take_profit : guess.tp)).replace(/"/g, """);
+ const entry = pos.entry_price != null ? pos.entry_price : mo.trigger_price;
+ const sl = mo.stop_loss != null ? mo.stop_loss : guess.sl;
+ const tp = mo.take_profit != null ? mo.take_profit : guess.tp;
+ const rr = calcRrRatio(side, entry, sl, tp);
+ const upnl = pos.unrealized_pnl;
+ let pnlText = fmt(upnl, 2) + "U";
+ if (pos.notional_usdt && upnl != null && Math.abs(Number(pos.notional_usdt)) > 1e-8) {
+ const pct = (Number(upnl) / Math.abs(Number(pos.notional_usdt))) * 100;
+ pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
+ }
+ const meta = [];
+ if (mo.monitor_type || mo.key_signal_type) {
+ meta.push(
+ `来源: ${esc(mo.monitor_type || "下单监控")}${mo.key_signal_type ? " · " + esc(mo.key_signal_type) : ""}`
+ );
+ } else {
+ meta.push("来源: 交易所持仓");
+ }
+ if (mo.trade_style) meta.push(`风格: ${esc(mo.trade_style)}`);
+ else meta.push("风格: —");
+ if (mo.risk_percent != null) {
+ meta.push(`风险: ${esc(mo.risk_percent)}%`);
+ }
+ const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true;
+ meta.push(
+ `移动保本:${beOn ? "开" : "关"}`
+ );
+ return `
+
+
+ ${esc(symbol)}
+ ${sideCn}
+
+
+
+
+
+
+
${meta.map((m) => `${m}`).join("")}
+
+
成交价${entry != null ? fmt(entry, 4) : "—"}
+
止损${sl != null && sl !== "" ? fmt(sl, 4) : "—"}
+
止盈${tp != null && tp !== "" ? fmt(tp, 4) : "—"}
+
盈亏比${rr != null ? fmt(rr, 2) + ":1" : "-:1"}
+
张数${fmt(pos.contracts, 4)}
+
浮盈亏${pnlText}
+
+
+
+
交易所止盈止损
+ ${renderExTpslRows(exchangeId, symbol, cond)}
+
+ ${renderOrdersCollapse(exchangeId, symbol, cond, reg)}
`;
}
+ function renderHubSectionCard(title, bodyHtml, emptyHint) {
+ const inner = bodyHtml || `${esc(emptyHint || "暂无")}
`;
+ return `
+
${esc(title)}
+
${inner}
+
`;
+ }
+
+ function renderKeySection(keys, kmap) {
+ if (!keys.length) return "";
+ return keys
+ .map((k) => {
+ const kp = kmap[k.id] || kmap[String(k.id)] || {};
+ const mt = k.monitor_type || k.type || "";
+ return `
+
${esc(k.symbol)} · ${esc(mt)}
+
上沿 ${esc(k.upper)} / 下沿 ${esc(k.lower)}
+
${esc(kp.gate_summary || kp.price_display || kp.price || "—")}
+
`;
+ })
+ .join("");
+ }
+
+ function renderStrategySection(orders, trends) {
+ const parts = [];
+ (orders || []).forEach((o) => {
+ parts.push(`
+
下单监控 #${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)}
+
${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)}
+
`);
+ });
+ (trends || []).forEach((t) => {
+ parts.push(`
+
趋势计划 #${esc(t.id)} · ${esc(t.symbol)}
+
${esc(t.direction)} · SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)}
+
`;
+ });
+ return parts.join("");
+ }
+
+ function renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
+ let inner = `
+
余额
${fmt(ag.balance_usdt, 2)} U
+
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
+
`;
+ inner += `持仓 · ${pos.length}
`;
+ if (pos.length) {
+ inner += '';
+ pos.forEach((p) => {
+ inner += `
${esc(p.symbol)} · ${esc(p.side)}${fmt(p.unrealized_pnl, 4)}
`;
+ });
+ inner += "
";
+ } else {
+ inner += '无持仓
';
+ }
+ const keyN = (row.capabilities || []).includes("key") ? keys.length : 0;
+ const stratN = orders.length + ((row.capabilities || []).includes("trend") ? trends.length : 0);
+ inner += `点击卡片展开 · 持仓详情 / 关键位 ${keyN} / 策略 ${stratN}
`;
+ return inner;
+ }
+
+ function renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
+ let inner = `
+
余额
${fmt(ag.balance_usdt, 2)} U
+
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
+
`;
+ inner += '';
+ if (pos.length) {
+ pos.forEach((p) => {
+ const mo = findMonitorOrder(orders, p.symbol, p.side);
+ inner += renderLivePositionCard(row.id, p, mo);
+ });
+ } else {
+ inner += '
暂无持仓
';
+ }
+ inner += "
";
+ if ((row.capabilities || []).includes("key")) {
+ if (!flaskOk) {
+ const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通";
+ inner += renderHubSectionCard("关键位", `${esc(fe)}
`, "");
+ } else {
+ inner += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录");
+ }
+ }
+ const showStrategy =
+ orders.length || ((row.capabilities || []).includes("trend") && trends.length);
+ if (showStrategy) {
+ inner += renderHubSectionCard(
+ "策略",
+ renderStrategySection(orders, (row.capabilities || []).includes("trend") ? trends : []),
+ "当前无策略记录"
+ );
+ }
+ return inner;
+ }
+
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
tpslPending = {
exchangeId,
@@ -398,7 +672,7 @@
}
}
- function renderMonitorCard(row) {
+ function renderMonitorCard(row, expanded) {
const ag = row.agent || {};
const pos = Array.isArray(ag.positions) ? ag.positions : [];
const hm = row.hub_monitor || {};
@@ -418,54 +692,10 @@
} else if (!agOk) {
inner = `${esc(agErr || "子代理返回失败")}
`;
inner += `请检查 PM2 子代理与 ${esc(row.agent_url || "")}/status
`;
+ } else if (expanded) {
+ inner = renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap);
} else {
- inner = `
-
余额
${fmt(ag.balance_usdt, 2)} U
-
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
-
`;
- inner += `交易所持仓
`;
- if (pos.length) {
- inner += pos.map((x) => renderPositionBlock(row.id, x)).join("");
- } else {
- inner += `无持仓
`;
- }
- if (orders.length) {
- inner += `机器人单 · ${orders.length}
`;
- orders.forEach((o) => {
- inner += `${esc(o.symbol)} · ${esc(o.direction)} · 触发 ${o.trigger_price}
`;
- });
- }
- if ((row.capabilities || []).includes("key")) {
- inner += `关键位
`;
- if (!flaskOk) {
- const fe = row.flask_error || hm.msg || hm.error || "";
- const short =
- fe ||
- (hm.status === 404
- ? "HTTP 404:请重启各 crypto_* Flask"
- : "策略 Flask 未连通");
- inner += `${esc(short)}
`;
- } else if (!keys.length) {
- inner += `当前无记录
`;
- } else {
- keys.slice(0, 8).forEach((k) => {
- const kp = kmap[k.id] || kmap[String(k.id)] || {};
- const mt = k.monitor_type || k.type || "";
- let line = `${esc(k.symbol)} · ${esc(mt)} · ${k.upper} / ${k.lower}`;
- if (kp.price_display != null || kp.price != null) {
- line += ` · ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
- }
- line += ` · ${esc(kp.gate_summary || "-")}`;
- inner += `${line}
`;
- });
- }
- }
- if ((row.capabilities || []).includes("trend") && trends.length) {
- inner += `趋势计划 · ${trends.length}
`;
- trends.forEach((t) => {
- inner += `#${t.id} ${esc(t.symbol)} ${t.direction} · SL ${t.stop_loss} · TP ${t.take_profit}
`;
- });
- }
+ inner = renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap);
}
const online = row.http_ok && agOk;
const cardCls = online ? "card-online" : "card-offline";
@@ -477,7 +707,11 @@
const openFlask = flaskOpen
? `实例`
: "";
- return `
+ const backBtn = expanded
+ ? `
`
+ : "";
+ const expandHit = !expanded && online ? " card-expand-hit" : "";
+ return `
@@ -487,12 +721,13 @@
${esc(flaskOpen || "")}
+ ${backBtn}
${openFlask}
${review}
-
${inner}
+
${inner}
`;
}
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index c9f6b09..c0eb826 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -7,7 +7,7 @@
-
+
@@ -101,6 +101,6 @@
-
+