diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index ed57e72..0a2adf7 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -7414,6 +7414,12 @@ def stats_page():
return render_main_page("stats")
+@app.route("/dashboard")
+@login_required
+def dashboard_page():
+ return render_main_page("dashboard")
+
+
@app.route("/risk_policy")
@login_required
def risk_policy_page():
@@ -9447,6 +9453,15 @@ register_trade_records_api(
app_tz=APP_TZ,
)
+from lib.instance.instance_dashboard_register import register_instance_dashboard_routes
+
+register_instance_dashboard_routes(
+ app,
+ login_required=login_required,
+ get_db=get_db,
+ hedge_enabled=False,
+)
+
@app.route("/api/journals")
@login_required
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index e9ae24d..42b4c6b 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -7230,6 +7230,12 @@ def stats_page():
return render_main_page("stats")
+@app.route("/dashboard")
+@login_required
+def dashboard_page():
+ return render_main_page("dashboard")
+
+
@app.route("/risk_policy")
@login_required
def risk_policy_page():
@@ -9314,6 +9320,15 @@ register_trade_records_api(
app_tz=APP_TZ,
)
+from lib.instance.instance_dashboard_register import register_instance_dashboard_routes
+
+register_instance_dashboard_routes(
+ app,
+ login_required=login_required,
+ get_db=get_db,
+ hedge_enabled=False,
+)
+
@app.route("/api/journals")
@login_required
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index cdba7fb..197c15f 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -6858,6 +6858,15 @@ def stats_page():
return render_main_page("stats")
+@app.route("/dashboard")
+@login_required
+def dashboard_page():
+ redir = redirect_to_embed_shell_if_enabled("dashboard")
+ if redir is not None:
+ return redir
+ return render_main_page("dashboard")
+
+
@app.route("/risk_policy")
@login_required
def risk_policy_page():
@@ -9003,6 +9012,34 @@ register_trade_records_api(
)
+def _dashboard_fetch_options_positions():
+ if not OKX_OPTIONS_ENABLED:
+ return []
+ cfg = app.extensions.get("options_cfg")
+ if not isinstance(cfg, dict) or not cfg.get("enabled"):
+ return []
+ try:
+ from lib.options.options_hub_lib import build_options_hub_snapshot
+
+ snap = build_options_hub_snapshot(cfg)
+ except Exception:
+ return []
+ if not snap.get("ok"):
+ return []
+ return list(snap.get("positions") or [])
+
+
+from lib.instance.instance_dashboard_register import register_instance_dashboard_routes
+
+register_instance_dashboard_routes(
+ app,
+ login_required=login_required,
+ get_db=get_db,
+ fetch_options_positions=_dashboard_fetch_options_positions,
+ hedge_enabled=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
+)
+
+
@app.route("/api/journals")
@login_required
def api_journals():
diff --git a/docs/系统设置说明.md b/docs/系统设置说明.md
index 3c4164a..6ef8f2b 100644
--- a/docs/系统设置说明.md
+++ b/docs/系统设置说明.md
@@ -26,15 +26,16 @@
### 2.1 顶栏导航开关
-| 开关 | 对应 Tab |
-|------|----------|
-| 策略交易 | 策略交易 |
-| 策略交易记录 | 策略交易记录 |
-| 交易记录与复盘 | 交易记录与复盘 |
-| 统计分析 | 统计分析 |
-| 风控说明 | 风控说明 |
-| env 配置 | env 配置 |
-| 期权 | 期权(仅 OKX 等有期权模块时有效) |
+| 开关 | 对应 Tab | 默认 |
+|------|----------|------|
+| 数据看板 | 数据看板(本户活跃监控总览) | **关闭** |
+| 策略交易 | 策略交易 | 开 |
+| 策略交易记录 | 策略交易记录 | 开 |
+| 交易记录与复盘 | 交易记录与复盘 | 开 |
+| 统计分析 | 统计分析 | 开 |
+| 风控说明 | 风控说明 | 开 |
+| env 配置 | env 配置 | 开 |
+| 期权 | 期权(仅 OKX 等有期权模块时有效) | 开 |
保存后 **立即生效**,无需重启.中控 iframe 内嵌导航同步生效.
diff --git a/lib/common/static/instance_dashboard.js b/lib/common/static/instance_dashboard.js
new file mode 100644
index 0000000..5f905e4
--- /dev/null
+++ b/lib/common/static/instance_dashboard.js
@@ -0,0 +1,204 @@
+/**
+ * 实例数据看板:拉 /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 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 renderItems(items) {
+ if (!items || !items.length) {
+ return '
暂无
';
+ }
+ return (
+ '' +
+ items
+ .map(function (it) {
+ const meta = [];
+ if (it.entry != null) meta.push("入场 " + fmtNum(it.entry));
+ if (it.stop_loss != null) meta.push("止损 " + fmtNum(it.stop_loss));
+ if (it.take_profit != null) meta.push("止盈 " + fmtNum(it.take_profit));
+ if (it.upper != null || it.lower != null) {
+ meta.push("上 " + fmtNum(it.upper) + " / 下 " + fmtNum(it.lower));
+ }
+ const pnlHtml = fmtPnl(it.pnl);
+ if (pnlHtml) meta.push(pnlHtml);
+ return (
+ '
"
+ );
+ })
+ .join("") +
+ "
"
+ );
+ }
+
+ function renderSection(key, sec) {
+ if (!sec) return "";
+ if ((key === "options" || key === "hedge_plan") && !sec.visible) return "";
+ const count = Number(sec.count) || 0;
+ return (
+ '' +
+ '' +
+ "
" +
+ escapeHtml(sec.title || key) +
+ ' ' +
+ count +
+ "
" +
+ '' +
+ "" +
+ renderItems(sec.items || []) +
+ ""
+ );
+ }
+
+ function bindClicks(el) {
+ if (!el) return;
+ el.querySelectorAll("[data-dash-tab]").forEach(function (btn) {
+ btn.addEventListener("click", function () {
+ goTab(btn.getAttribute("data-dash-tab"));
+ });
+ });
+ }
+
+ 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 res = await fetch("/api/instance/dashboard", { credentials: "same-origin" });
+ const data = await res.json().catch(function () {
+ return {};
+ });
+ if (!res.ok || !data.ok) {
+ throw new Error(data.msg || res.statusText || "加载失败");
+ }
+ if (updated) updated.textContent = "更新 " + (data.updated_at || "—");
+ if (sections) {
+ sections.innerHTML = SECTION_ORDER.map(function (k) {
+ return renderSection(k, data[k]);
+ }).join("");
+ bindClicks(sections);
+ }
+ 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);
diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js
index 13f378a..cc3bef0 100644
--- a/lib/common/static/instance_embed.js
+++ b/lib/common/static/instance_embed.js
@@ -4,6 +4,7 @@
*/
(function (global) {
const TAB_PATH = {
+ dashboard: "/dashboard",
key_monitor: "/key_monitor",
trade: "/trade",
strategy: "/strategy",
@@ -99,6 +100,9 @@
if (!revisit && tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
global.KeyMonitorForm.init();
}
+ if (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.init === "function") {
+ global.InstanceDashboard.init(!!revisit);
+ }
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
global.initStrategyRollForm();
}
diff --git a/lib/common/static/instance_live.js b/lib/common/static/instance_live.js
index b948542..f942c0d 100644
--- a/lib/common/static/instance_live.js
+++ b/lib/common/static/instance_live.js
@@ -30,6 +30,9 @@
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
global.OptionsPanelLive.refreshSoft(options);
}
+ if (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.refreshSoft === "function") {
+ global.InstanceDashboard.refreshSoft(options);
+ }
}
function scheduleRefresh(opts) {
diff --git a/lib/common/static/instance_page.css b/lib/common/static/instance_page.css
index a777ef6..d41025e 100644
--- a/lib/common/static/instance_page.css
+++ b/lib/common/static/instance_page.css
@@ -269,3 +269,20 @@
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
#embed-page-root{min-height:120px;position:relative}
.embed-tab-pane[hidden]{display:none!important}
+.inst-dash-card{grid-column:1/-1}
+.inst-dash-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:8px}
+.inst-dash-desc{margin:0;font-size:.82rem}
+.inst-dash-head-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
+.inst-dash-status{min-height:1.2em;margin:0 0 10px}
+.inst-dash-sections{display:flex;flex-direction:column;gap:14px}
+.inst-dash-section{padding:12px;background:#141923;border:1px solid #2a3150;border-radius:10px}
+.inst-dash-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
+.inst-dash-section-head h3{margin:0;font-size:.95rem;color:#dbe4ff}
+.inst-dash-count{display:inline-block;min-width:1.4em;padding:1px 7px;margin-left:4px;border-radius:999px;background:#1f3a5a;color:#8fc8ff;font-size:.75rem;font-weight:600}
+.inst-dash-empty{margin:0;padding:12px;text-align:center;border:1px dashed #2a3348;border-radius:8px;font-size:.84rem}
+.inst-dash-list{display:flex;flex-direction:column;gap:8px}
+.inst-dash-item{display:block;width:100%;text-align:left;padding:10px 12px;border-radius:8px;border:1px solid #2a3150;background:#1a2034;color:#eaeaea;cursor:pointer}
+.inst-dash-item:hover{border-color:#3d4f78;background:#1e2740}
+.inst-dash-item-title{font-weight:600;font-size:.9rem;color:#dbe4ff}
+.inst-dash-item-sub{font-size:.78rem;margin-top:3px}
+.inst-dash-item-meta{font-size:.78rem;margin-top:6px;color:#a8b0d8;font-variant-numeric:tabular-nums}
diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js
index cf235fb..e43676d 100644
--- a/lib/common/static/instance_settings_prefs.js
+++ b/lib/common/static/instance_settings_prefs.js
@@ -19,8 +19,18 @@
return data;
}
+ /** 默认关闭的导航开关:缺失时按 false,不能用 !== false */
+ const NAV_DEFAULT_OFF = { show_nav_dashboard: true };
+
+ function navPrefShow(display, key) {
+ if (!key) return true;
+ if (NAV_DEFAULT_OFF[key]) return display[key] === true;
+ return display[key] !== false;
+ }
+
function applyDisplayToNav(display) {
const map = {
+ dashboard: "show_nav_dashboard",
strategy: "show_nav_strategy",
strategy_records: "show_nav_strategy_records",
records: "show_nav_records",
@@ -37,7 +47,7 @@
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
const key = map[tab];
if (!key) return;
- const show = display[key] !== false;
+ const show = navPrefShow(display, key);
a.classList.toggle("nav-hidden", !show);
a.style.display = show ? "" : "none";
});
@@ -47,6 +57,7 @@
function pageNavAllowed(tab) {
const d = DISPLAY();
const map = {
+ dashboard: "show_nav_dashboard",
strategy: "show_nav_strategy",
strategy_records: "show_nav_strategy_records",
records: "show_nav_records",
@@ -61,7 +72,7 @@
};
const key = map[tab];
if (!key) return true;
- return d[key] !== false;
+ return navPrefShow(d, key);
}
function displayPrefsRoot() {
@@ -129,7 +140,9 @@
const cb = document.createElement("input");
cb.type = "checkbox";
cb.dataset.prefKey = item.key;
- cb.checked = display[item.key] !== false;
+ cb.checked = NAV_DEFAULT_OFF[item.key]
+ ? display[item.key] === true
+ : display[item.key] !== false;
label.appendChild(cb);
label.appendChild(document.createTextNode(" " + item.label));
grid.appendChild(label);
diff --git a/lib/hub/hub_bridge.py b/lib/hub/hub_bridge.py
index ae62133..9ec93d8 100644
--- a/lib/hub/hub_bridge.py
+++ b/lib/hub/hub_bridge.py
@@ -72,6 +72,7 @@ def install_instance_theme_static(app) -> None:
"instance_stats.js": "application/javascript; charset=utf-8",
"instance_live.js": "application/javascript; charset=utf-8",
"instance_settings_prefs.js": "application/javascript; charset=utf-8",
+ "instance_dashboard.js": "application/javascript; charset=utf-8",
"options_panel.js": "application/javascript; charset=utf-8",
"order_entry_model.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8",
diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py
new file mode 100644
index 0000000..846c8b7
--- /dev/null
+++ b/lib/instance/instance_dashboard_lib.py
@@ -0,0 +1,312 @@
+"""实例数据看板:本户活跃监控 / 持仓只读聚合."""
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from typing import Any, Callable, Optional
+
+
+def _row_dict(row: Any) -> dict[str, Any]:
+ if row is None:
+ return {}
+ if isinstance(row, dict):
+ return dict(row)
+ try:
+ return dict(row)
+ except Exception:
+ return {}
+
+
+def _safe_float(v: Any) -> Optional[float]:
+ try:
+ if v is None or v == "":
+ return None
+ return float(v)
+ except (TypeError, ValueError):
+ return None
+
+
+def _dir_label(direction: Any) -> str:
+ d = str(direction or "").strip().lower()
+ if d == "short":
+ return "做空"
+ if d == "long":
+ return "做多"
+ return str(direction or "-")
+
+
+def _format_order_item(od: dict[str, Any]) -> dict[str, Any]:
+ try:
+ from lib.strategy.strategy_trade_labels import apply_order_monitor_source_labels
+
+ od = apply_order_monitor_source_labels(od)
+ except Exception:
+ pass
+ try:
+ from lib.trade.entry_model_lib import enrich_entry_model_display
+
+ enrich_entry_model_display(od)
+ except Exception:
+ pass
+ sym = od.get("exchange_symbol") or od.get("symbol") or "-"
+ direction = str(od.get("direction") or "long").lower()
+ mt = od.get("monitor_type_display") or od.get("monitor_type") or ""
+ kst = od.get("key_signal_type") or ""
+ title = f"{sym} {_dir_label(direction)}"
+ bits = [x for x in (mt, kst) if x]
+ subtitle = " · ".join(bits) if bits else ""
+ entry = _safe_float(od.get("trigger_price"))
+ sl = _safe_float(od.get("stop_loss"))
+ tp = _safe_float(od.get("take_profit"))
+ return {
+ "id": od.get("id"),
+ "kind": "order",
+ "tab": "trade",
+ "title": title,
+ "subtitle": subtitle,
+ "symbol": sym,
+ "direction": direction,
+ "direction_label": _dir_label(direction),
+ "entry": entry,
+ "stop_loss": sl,
+ "take_profit": tp,
+ "status": od.get("status") or "active",
+ }
+
+
+def _format_key_item(kd: dict[str, Any]) -> dict[str, Any]:
+ sym = kd.get("exchange_symbol") or kd.get("symbol") or "-"
+ direction = str(kd.get("direction") or "long").lower()
+ signal = kd.get("signal_type") or kd.get("key_signal_type") or kd.get("monitor_type") or ""
+ upper = _safe_float(kd.get("upper"))
+ lower = _safe_float(kd.get("lower"))
+ subtitle_parts = []
+ if signal:
+ subtitle_parts.append(str(signal))
+ if upper is not None or lower is not None:
+ subtitle_parts.append(
+ f"上{upper if upper is not None else '-'} / 下{lower if lower is not None else '-'}"
+ )
+ return {
+ "id": kd.get("id"),
+ "kind": "key",
+ "tab": "key_monitor",
+ "title": f"{sym} {_dir_label(direction)}",
+ "subtitle": " · ".join(subtitle_parts),
+ "symbol": sym,
+ "direction": direction,
+ "direction_label": _dir_label(direction),
+ "upper": upper,
+ "lower": lower,
+ "status": kd.get("status") or "active",
+ }
+
+
+def _format_trend_item(td: dict[str, Any]) -> dict[str, Any]:
+ sym = td.get("exchange_symbol") or td.get("symbol") or "-"
+ direction = str(td.get("direction") or "long").lower()
+ status = td.get("status") or "active"
+ entry = _safe_float(td.get("entry_price") or td.get("trigger_price"))
+ return {
+ "id": td.get("id"),
+ "kind": "trend",
+ "tab": "strategy",
+ "title": f"趋势回调 {sym} {_dir_label(direction)}",
+ "subtitle": f"状态 {status}",
+ "symbol": sym,
+ "direction": direction,
+ "direction_label": _dir_label(direction),
+ "entry": entry,
+ "status": status,
+ }
+
+
+def _format_roll_item(rd: dict[str, Any]) -> dict[str, Any]:
+ sym = rd.get("exchange_symbol") or rd.get("symbol") or "-"
+ direction = str(rd.get("direction") or "long").lower()
+ status = rd.get("status") or "active"
+ return {
+ "id": rd.get("id"),
+ "kind": "roll",
+ "tab": "strategy",
+ "title": f"顺势加仓 {sym} {_dir_label(direction)}",
+ "subtitle": f"状态 {status}",
+ "symbol": sym,
+ "direction": direction,
+ "direction_label": _dir_label(direction),
+ "status": status,
+ }
+
+
+def _format_options_item(p: dict[str, Any]) -> dict[str, Any]:
+ inst = p.get("inst_id") or p.get("instId") or "-"
+ opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
+ label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
+ upl = _safe_float(p.get("upl"))
+ net = None
+ try:
+ from lib.options.options_positions_lib import net_pnl_from_display_row
+
+ net = net_pnl_from_display_row(p)
+ except Exception:
+ net = None
+ pnl = net if net is not None else upl
+ pos = _safe_float(p.get("pos"))
+ return {
+ "id": inst,
+ "kind": "options",
+ "tab": "options",
+ "title": f"{inst} {label}",
+ "subtitle": f"张数 {pos if pos is not None else '-'}",
+ "inst_id": inst,
+ "opt_type": opt_type,
+ "pnl": round(pnl, 4) if pnl is not None else None,
+ "pos": pos,
+ }
+
+
+def _format_hedge_item(plan: dict[str, Any]) -> dict[str, Any]:
+ pid = plan.get("id")
+ underlying = plan.get("underlying") or "-"
+ plan_type = plan.get("plan_type") or ""
+ status = plan.get("status") or ""
+ summary = plan.get("contracts_summary") or ""
+ return {
+ "id": pid,
+ "kind": "hedge_plan",
+ "tab": "hedge_plan",
+ "title": f"对冲 #{pid} {underlying}",
+ "subtitle": " · ".join(x for x in (plan_type, status, summary) if x),
+ "underlying": underlying,
+ "plan_type": plan_type,
+ "status": status,
+ }
+
+
+def _table_exists(conn, name: str) -> bool:
+ try:
+ row = conn.execute(
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
+ (name,),
+ ).fetchone()
+ return bool(row)
+ except Exception:
+ return False
+
+
+def collect_orders(conn) -> list[dict[str, Any]]:
+ if not _table_exists(conn, "order_monitors"):
+ return []
+ rows = conn.execute(
+ "SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC"
+ ).fetchall()
+ return [_format_order_item(_row_dict(r)) for r in rows]
+
+
+def collect_keys(conn) -> list[dict[str, Any]]:
+ if not _table_exists(conn, "key_monitors"):
+ return []
+ rows = conn.execute("SELECT * FROM key_monitors ORDER BY id DESC").fetchall()
+ return [_format_key_item(_row_dict(r)) for r in rows]
+
+
+def collect_trends(conn) -> list[dict[str, Any]]:
+ if not _table_exists(conn, "trend_pullback_plans"):
+ return []
+ try:
+ rows = conn.execute(
+ "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC"
+ ).fetchall()
+ except Exception:
+ return []
+ return [_format_trend_item(_row_dict(r)) for r in rows]
+
+
+def collect_rolls(conn) -> list[dict[str, Any]]:
+ if not _table_exists(conn, "roll_groups") or not _table_exists(conn, "order_monitors"):
+ return []
+ try:
+ rows = conn.execute(
+ """SELECT g.* FROM roll_groups g
+ INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active'
+ WHERE g.status='active' ORDER BY g.id DESC"""
+ ).fetchall()
+ except Exception:
+ return []
+ return [_format_roll_item(_row_dict(r)) for r in rows]
+
+
+def collect_hedge_plans(conn) -> list[dict[str, Any]]:
+ if not _table_exists(conn, "hedge_plans"):
+ return []
+ try:
+ from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans
+
+ rows: list[dict[str, Any]] = []
+ for status in ("opening", "active", "partial"):
+ rows.extend(list_plans(conn, status=status, limit=80))
+ rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
+ plans = attach_legs_to_plans(conn, rows)
+ return [_format_hedge_item(p) for p in plans]
+ except Exception:
+ return []
+
+
+def collect_options_items(
+ fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
+) -> list[dict[str, Any]]:
+ if not callable(fetch_options_positions):
+ return []
+ try:
+ raw = fetch_options_positions() or []
+ except Exception:
+ return []
+ out: list[dict[str, Any]] = []
+ for p in raw:
+ if not isinstance(p, dict):
+ continue
+ out.append(_format_options_item(p))
+ return out
+
+
+def build_instance_dashboard_payload(
+ conn,
+ *,
+ fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
+ hedge_enabled: bool = False,
+) -> dict[str, Any]:
+ orders = collect_orders(conn)
+ keys = collect_keys(conn)
+ trends = collect_trends(conn)
+ rolls = collect_rolls(conn)
+ strategy_items = trends + rolls
+ options_items = collect_options_items(fetch_options_positions)
+ hedge_items = collect_hedge_plans(conn) if hedge_enabled else []
+ now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
+ return {
+ "ok": True,
+ "updated_at": now,
+ "orders": {"title": "实盘下单", "count": len(orders), "items": orders, "tab": "trade"},
+ "keys": {"title": "关键位监控", "count": len(keys), "items": keys, "tab": "key_monitor"},
+ "strategy": {
+ "title": "策略交易",
+ "count": len(strategy_items),
+ "items": strategy_items,
+ "trends": trends,
+ "rolls": rolls,
+ "tab": "strategy",
+ },
+ "options": {
+ "title": "期权持仓",
+ "count": len(options_items),
+ "items": options_items,
+ "visible": len(options_items) > 0,
+ "tab": "options",
+ },
+ "hedge_plan": {
+ "title": "对冲计划",
+ "count": len(hedge_items),
+ "items": hedge_items,
+ "visible": len(hedge_items) > 0,
+ "tab": "hedge_plan",
+ },
+ }
diff --git a/lib/instance/instance_dashboard_register.py b/lib/instance/instance_dashboard_register.py
new file mode 100644
index 0000000..d38142a
--- /dev/null
+++ b/lib/instance/instance_dashboard_register.py
@@ -0,0 +1,31 @@
+"""注册 GET /api/instance/dashboard(三所共用)."""
+from __future__ import annotations
+
+from typing import Any, Callable, Optional
+
+from flask import Flask, jsonify
+
+
+def register_instance_dashboard_routes(
+ app: Flask,
+ *,
+ login_required: Callable,
+ get_db: Callable,
+ fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
+ hedge_enabled: bool = False,
+) -> None:
+ from lib.instance.instance_dashboard_lib import build_instance_dashboard_payload
+
+ @app.route("/api/instance/dashboard")
+ @login_required
+ def api_instance_dashboard():
+ conn = get_db()
+ try:
+ payload = build_instance_dashboard_payload(
+ conn,
+ fetch_options_positions=fetch_options_positions,
+ hedge_enabled=bool(hedge_enabled),
+ )
+ return jsonify(payload)
+ finally:
+ conn.close()
diff --git a/lib/instance/instance_display_prefs_lib.py b/lib/instance/instance_display_prefs_lib.py
index f3f4d4e..f75b985 100644
--- a/lib/instance/instance_display_prefs_lib.py
+++ b/lib/instance/instance_display_prefs_lib.py
@@ -8,6 +8,7 @@ from lib.instance.runtime_settings_lib import runtime_get_prefix, runtime_set_ma
DISPLAY_RUNTIME_PREFIX = "display."
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
+ "show_nav_dashboard": False,
"show_nav_strategy": True,
"show_nav_strategy_records": True,
"show_nav_records": True,
@@ -25,6 +26,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
}
DISPLAY_LABELS: dict[str, str] = {
+ "show_nav_dashboard": "数据看板",
"show_nav_strategy": "策略交易",
"show_nav_strategy_records": "策略交易记录",
"show_nav_records": "交易记录与复盘",
@@ -42,6 +44,7 @@ DISPLAY_LABELS: dict[str, str] = {
}
NAV_TAB_ALLOWED: dict[str, str] = {
+ "dashboard": "show_nav_dashboard",
"strategy": "show_nav_strategy",
"strategy_records": "show_nav_strategy_records",
"records": "show_nav_records",
@@ -103,6 +106,7 @@ def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
def display_meta_for_ui() -> list[dict[str, Any]]:
nav_keys = [
+ "show_nav_dashboard",
"show_nav_strategy",
"show_nav_strategy_records",
"show_nav_records",
diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py
index 812900b..0753ee7 100644
--- a/lib/instance/instance_embed_lib.py
+++ b/lib/instance/instance_embed_lib.py
@@ -11,6 +11,7 @@ from flask import Flask, Response, jsonify, redirect, request, session
from jinja2 import ChoiceLoader, FileSystemLoader
EMBED_TABS: tuple[str, ...] = (
+ "dashboard",
"key_monitor",
"trade",
"strategy",
@@ -28,6 +29,7 @@ EMBED_TABS: tuple[str, ...] = (
PATH_TO_EMBED_TAB: dict[str, str] = {
"/": "trade",
"/trade": "trade",
+ "/dashboard": "dashboard",
"/key_monitor": "key_monitor",
"/strategy": "strategy",
"/strategy/trend": "strategy",
diff --git a/lib/instance/templates/dashboard_panel.html b/lib/instance/templates/dashboard_panel.html
new file mode 100644
index 0000000..2447aea
--- /dev/null
+++ b/lib/instance/templates/dashboard_panel.html
@@ -0,0 +1,15 @@
+{# 实例数据看板:只读活跃监控总览 #}
+
+
+
+
数据看板
+
本户活跃监控总览 · 只读 · 期权/对冲有数据才显示
+
+
+ —
+
+
+
+
+
+
diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html
index 2e135ea..d21cf90 100644
--- a/lib/instance/templates/embed_page_fragment.html
+++ b/lib/instance/templates/embed_page_fragment.html
@@ -81,7 +81,9 @@
{% endmacro %}
- {% if page == 'key_monitor' %}
+ {% if page == 'dashboard' %}
+ {% include 'dashboard_panel.html' %}
+ {% elif page == 'key_monitor' %}
{% include 'key_monitor_panel.html' %}
{% elif page == 'trade' %}
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index a067834..55b34ff 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -6,7 +6,7 @@
-
+
@@ -29,6 +29,7 @@
加密货币|交易监控 + AI复盘一体化