From 695a785832da20c7855ef59335e3790e3eda4f11 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 24 May 2026 08:27:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AFui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manual_trading_hub/hub.py | 2 +- manual_trading_hub/static/app.css | 284 ++++++++++++++++ manual_trading_hub/static/app.js | 473 ++++++++++++++++++++------- manual_trading_hub/static/index.html | 4 +- manual_trading_hub/使用说明.md | 4 +- 5 files changed, 644 insertions(+), 123 deletions(-) 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 @@
- + diff --git a/manual_trading_hub/使用说明.md b/manual_trading_hub/使用说明.md index 590e435..04fb6fc 100644 --- a/manual_trading_hub/使用说明.md +++ b/manual_trading_hub/使用说明.md @@ -176,7 +176,9 @@ curl -s http://127.0.0.1:5100/api/ping | 功能 | 说明 | |------|------| -| **2×2 卡片** | 仅显示「已启用」账户;每卡含子代理持仓、浮盈、余额 | +| **2×2 卡片** | 仅显示「已启用」账户;**点击卡片**可放大,放大后每仓一张「实盘」风格持仓卡 | +| **放大视图** | 持仓详情(与实例「实时持仓」布局一致)、**关键位**与**策略**各为独立卡片;顶栏「返回」回到网格 | +| **委托单折叠** | 展开/收起状态保存在浏览器本地,**5 秒自动刷新不会重置**(便于填写委托) | | **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) | | **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` | | **挂止盈止损** | 持仓行 **「委托」**:弹窗填止损/止盈价 → **先撤该合约全部条件单,再挂新 TP/SL**(币安 / OKX / Gate / Gate趋势 四所统一,逻辑与各实例 `.env` 参数一致) |