diff --git a/hub_bridge.py b/hub_bridge.py
index 8ca0476..9bdaf07 100644
--- a/hub_bridge.py
+++ b/hub_bridge.py
@@ -167,17 +167,32 @@ def register_hub_routes(app):
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
).fetchall():
trends.append(_row_to_dict(row))
+ rolls = []
+ try:
+ for row in conn.execute(
+ "SELECT * FROM roll_groups WHERE status='active' ORDER BY id DESC"
+ ).fetchall():
+ rolls.append(_row_to_dict(row))
+ except Exception:
+ pass
conn.close()
enrich = c.get("enrich_monitor")
if callable(enrich):
try:
- payload = enrich(keys=keys, orders=orders, trends=trends)
+ payload = enrich(keys=keys, orders=orders, trends=trends, rolls=rolls)
if isinstance(payload, dict):
return jsonify({"ok": True, **payload})
except Exception as e:
return jsonify({"ok": False, "msg": str(e)}), 500
return jsonify(
- {"ok": True, "keys": keys, "orders": orders, "trends": trends, "key_prices": []}
+ {
+ "ok": True,
+ "keys": keys,
+ "orders": orders,
+ "trends": trends,
+ "rolls": rolls,
+ "key_prices": [],
+ }
)
@app.route("/api/hub/add_order", methods=["POST"])
diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py
index 449034a..43af05d 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-expand-fix"
+HUB_BUILD = "20260525-fullscreen"
def _is_local(host: str | None) -> bool:
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index 44eda7d..966c016 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -493,18 +493,62 @@ button:disabled {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
-.grid-monitor.grid-monitor-expanded {
- grid-template-columns: minmax(0, 1fr);
- max-width: 720px;
+.card-expand-zone {
+ cursor: pointer;
+}
+
+.card-expand-zone:hover .card-title {
+ color: var(--accent);
+}
+
+body.hub-fullscreen-open {
+ overflow: hidden;
+}
+
+.exchange-fullscreen {
+ position: fixed;
+ inset: 0;
+ z-index: 150;
+ background: rgba(2, 6, 12, 0.92);
+ backdrop-filter: blur(6px);
+ overflow: auto;
+ padding: 16px 20px 24px;
+}
+
+.exchange-fullscreen-panel {
+ max-width: 820px;
margin: 0 auto;
}
-.card.card-expanded {
- grid-column: 1 / -1;
+.fs-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border-soft);
}
-.card-expand-hit {
- cursor: pointer;
+.fs-title {
+ margin: 0;
+ font-family: var(--display);
+ font-size: 18px;
+ letter-spacing: 0.04em;
+}
+
+.fs-sub {
+ font-size: 11px;
+ color: var(--muted);
+ margin-top: 4px;
+ word-break: break-all;
+}
+
+.fs-head-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: flex-end;
}
.card-expand-hint {
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index c111df9..4904acd 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -191,21 +191,59 @@
}
}
+ function closeExchangeFullscreen() {
+ expandedExchangeId = "";
+ sessionStorage.removeItem("hub_expanded_ex");
+ const fs = document.getElementById("exchange-fullscreen");
+ if (fs) {
+ fs.classList.add("hidden");
+ fs.setAttribute("aria-hidden", "true");
+ }
+ document.body.classList.remove("hub-fullscreen-open");
+ }
+
+ function openExchangeFullscreen(exId) {
+ expandedExchangeId = String(exId);
+ sessionStorage.setItem("hub_expanded_ex", expandedExchangeId);
+ renderMonitorGrid(lastMonitorRows);
+ }
+
function renderMonitorGrid(rows) {
const box = document.getElementById("monitor-grid");
+ const fs = document.getElementById("exchange-fullscreen");
+ const fsInner = document.getElementById("exchange-fullscreen-inner");
if (!box) return;
if (expandedExchangeId && !rows.some((r) => String(r.id) === String(expandedExchangeId))) {
- expandedExchangeId = "";
- sessionStorage.removeItem("hub_expanded_ex");
+ closeExchangeFullscreen();
}
- 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);
+ box.innerHTML =
+ rows.map((r) => renderMonitorCard(r)).join("") || '无已启用账户
';
+ syncMonitorGridColumns(box, rows.length);
bindMonitorInteractions(box);
+
+ if (expandedExchangeId && fs && fsInner) {
+ const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
+ if (row) {
+ fsInner.innerHTML = renderFullscreenExchange(row);
+ fs.classList.remove("hidden");
+ fs.setAttribute("aria-hidden", "false");
+ document.body.classList.add("hub-fullscreen-open");
+ bindMonitorInteractions(fsInner);
+ fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
+ btn.onclick = (ev) => {
+ ev.stopPropagation();
+ closeExchangeFullscreen();
+ renderMonitorGrid(lastMonitorRows);
+ };
+ });
+ } else {
+ closeExchangeFullscreen();
+ }
+ } else if (fs) {
+ fs.classList.add("hidden");
+ fs.setAttribute("aria-hidden", "true");
+ document.body.classList.remove("hub-fullscreen-open");
+ }
}
function bindMonitorInteractions(box) {
@@ -249,22 +287,11 @@
);
};
});
- 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(".card-expand-zone").forEach((zone) => {
+ zone.onclick = (ev) => {
+ if (ev.target.closest("a, button, input, summary, details, .card-actions")) return;
+ const id = zone.closest(".card")?.dataset.exId;
+ if (id) openExchangeFullscreen(id);
};
});
box.querySelectorAll("details.pos-orders-collapse[data-collapse-key]").forEach((el) => {
@@ -461,77 +488,180 @@
.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 renderOrderMonitorSection(orders) {
+ if (!orders || !orders.length) return "";
+ return orders
+ .map(
+ (o) => `
+
#${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)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}
+
`
+ )
+ .join("");
}
- function renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
+ function renderTrendSection(trends) {
+ if (!trends || !trends.length) return "";
+ return trends
+ .map(
+ (t) => `
+
#${esc(t.id)} · ${esc(t.symbol)} · ${esc(t.direction)}
+
SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)} · 状态 ${esc(t.status || "active")}
+
`
+ )
+ .join("");
+ }
+
+ function renderRollSection(rolls) {
+ if (!rolls || !rolls.length) return "";
+ return rolls
+ .map(
+ (g) => `
+
组 #${esc(g.id)} · 监控单 #${esc(g.order_monitor_id || "—")}
+
腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmt(g.current_stop_loss, 4)} · ${esc(g.status || "active")}
+
`
+ )
+ .join("");
+ }
+
+ 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, """);
+ return `
+
| 合约 | 方向 | 张数 | 浮盈 | 操作 |
+
+ | ${esc(x.symbol)} |
+ ${esc(x.side)} |
+ ${fmt(x.contracts, 4)} |
+ ${fmt(x.unrealized_pnl, 4)} |
+
+
+
+
+
+ |
+
+
+ ${renderOrdersCollapse(exchangeId, x.symbol, cond, reg)}
+
`;
+ }
+
+ function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
let inner = `
余额
${fmt(ag.balance_usdt, 2)} U
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
`;
- inner += `持仓 · ${pos.length}
`;
+ inner += `交易所持仓
`;
if (pos.length) {
- inner += '';
- pos.forEach((p) => {
- inner += `
${esc(p.symbol)} · ${esc(p.side)}${fmt(p.unrealized_pnl, 4)}
`;
- });
- inner += "
";
+ inner += pos.map((p) => renderPositionBlock(row.id, p)).join("");
} 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}
`;
+ if (orders.length) {
+ inner += `下单监控 · ${orders.length}
`;
+ orders.forEach((o) => {
+ inner += `${esc(o.symbol || o.exchange_symbol)} · ${esc(o.direction)} · 触发 ${fmt(o.trigger_price, 4)}
`;
+ });
+ }
+ if ((row.capabilities || []).includes("key")) {
+ inner += `关键位
`;
+ if (!flaskOk) {
+ const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通";
+ inner += `${esc(fe)}
`;
+ } else if (!keys.length) {
+ inner += '当前无记录
';
+ } else {
+ keys.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}
`;
+ });
+ }
+ if (rolls.length) {
+ inner += `顺势加仓 · ${rolls.length}
`;
+ rolls.forEach((g) => {
+ inner += `组 #${g.id} · 监控 #${g.order_monitor_id || "—"} · ${g.leg_count != null ? g.leg_count : "—"} 腿
`;
+ });
+ }
+ inner += `点击标题栏放大全屏 · 查看持仓卡片 / 关键位 / 策略详情
`;
return inner;
}
- function renderExpandedBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap) {
- let inner = `
-
余额
${fmt(ag.balance_usdt, 2)} U
+ function renderFullscreenExchange(row) {
+ const ag = row.agent || {};
+ const pos = Array.isArray(ag.positions) ? ag.positions : [];
+ const hm = row.hub_monitor || {};
+ const flaskOk = row.flask_ok !== false && hm.ok !== false;
+ const keys = flaskOk ? hm.keys || [] : [];
+ const orders = flaskOk ? hm.orders || [] : [];
+ const trends = flaskOk ? hm.trends || [] : [];
+ const rolls = flaskOk ? hm.rolls || [] : [];
+ const kmap = {};
+ (row.key_prices || []).forEach((k) => {
+ kmap[k.id] = k;
+ });
+ const flaskOpen = row.flask_url_browser || row.flask_url;
+ const strategyUrl = flaskOpen ? esc(flaskOpen.replace(/\/$/, "") + "/strategy") : "";
+ let html = `
+
+
${esc(row.name)}
+
${esc(flaskOpen || "")}
+
+
+
+ ${flaskOpen ? `
打开实例` : ""}
+ ${strategyUrl ? `
策略交易` : ""}
+
+
+
`;
+ if (!row.http_ok || ag.ok === false) {
+ html += `
${esc(row.error || ag.error || "子代理不可用")}
`;
+ return html;
+ }
+ html += `
+
余额
${fmt(ag.balance_usdt, 2)} U
浮盈合计
${fmt(ag.total_unrealized_pnl, 4)}
`;
- inner += '
';
+ html += '
持仓(每币种一卡)
';
if (pos.length) {
pos.forEach((p) => {
- const mo = findMonitorOrder(orders, p.symbol, p.side);
- inner += renderLivePositionCard(row.id, p, mo);
+ html += renderLivePositionCard(row.id, p, findMonitorOrder(orders, p.symbol, p.side));
});
} else {
- inner += '
暂无持仓
';
+ html += '
暂无持仓
';
}
- inner += "
";
+ html += "
";
if ((row.capabilities || []).includes("key")) {
if (!flaskOk) {
- const fe = row.flask_error || hm.msg || hm.error || "策略 Flask 未连通";
- inner += renderHubSectionCard("关键位", `
${esc(fe)}
`, "");
+ html += renderHubSectionCard("关键位", `
${esc(row.flask_error || hm.error || "Flask 未连通")}
`, "");
} else {
- inner += renderHubSectionCard("关键位", renderKeySection(keys, kmap), "当前无关键位记录");
+ html += 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 : []),
- "当前无策略记录"
- );
+ html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders), "暂无运行中的下单监控");
+ if ((row.capabilities || []).includes("trend")) {
+ html += renderHubSectionCard("趋势回调", renderTrendSection(trends), "暂无运行中的趋势回调计划");
}
- return inner;
+ html += renderHubSectionCard("顺势加仓", renderRollSection(rolls), "暂无运行中的顺势加仓组");
+ return html;
}
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
@@ -627,7 +757,13 @@
if (cancel) cancel.onclick = closeTpslModal;
if (submit) submit.onclick = () => submitTpslModal();
document.addEventListener("keydown", (ev) => {
- if (ev.key === "Escape") closeTpslModal();
+ if (ev.key === "Escape") {
+ closeTpslModal();
+ if (expandedExchangeId) {
+ closeExchangeFullscreen();
+ renderMonitorGrid(lastMonitorRows);
+ }
+ }
});
}
@@ -672,7 +808,7 @@
}
}
- function renderMonitorCard(row, expanded) {
+ function renderMonitorCard(row) {
const ag = row.agent || {};
const pos = Array.isArray(ag.positions) ? ag.positions : [];
const hm = row.hub_monitor || {};
@@ -680,6 +816,7 @@
const keys = flaskOk ? hm.keys || [] : [];
const orders = flaskOk ? hm.orders || [] : [];
const trends = flaskOk ? hm.trends || [] : [];
+ const rolls = flaskOk ? hm.rolls || [] : [];
const kmap = {};
(row.key_prices || []).forEach((k) => {
kmap[k.id] = k;
@@ -692,10 +829,8 @@
} 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 = renderCompactBody(row, ag, pos, hm, flaskOk, keys, orders, trends, kmap);
+ inner = renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap);
}
const online = row.http_ok && agOk;
const cardCls = online ? "card-online" : "card-offline";
@@ -707,12 +842,8 @@
const openFlask = flaskOpen
? `
实例`
: "";
- const backBtn = expanded
- ? `
`
- : "";
- const expandHit = !expanded && online ? " card-expand-hit" : "";
- return `
-
+ return `
+
@@ -721,13 +852,12 @@
${esc(flaskOpen || "")}
- ${backBtn}
${openFlask}
${review}
-
${inner}
+
${inner}
`;
}
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index 21444f4..6e00352 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -7,7 +7,7 @@
-
+
@@ -56,6 +56,12 @@
+
+
CFG 系统设置
@@ -101,6 +107,6 @@
-
+