Add instance data dashboard (default off).
Read-only overview of live orders, key monitors, and strategy; show options/hedge only when present. Toggle via system settings nav prefs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7414,6 +7414,12 @@ def stats_page():
|
|||||||
return render_main_page("stats")
|
return render_main_page("stats")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/dashboard")
|
||||||
|
@login_required
|
||||||
|
def dashboard_page():
|
||||||
|
return render_main_page("dashboard")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/risk_policy")
|
@app.route("/risk_policy")
|
||||||
@login_required
|
@login_required
|
||||||
def risk_policy_page():
|
def risk_policy_page():
|
||||||
@@ -9447,6 +9453,15 @@ register_trade_records_api(
|
|||||||
app_tz=APP_TZ,
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -7230,6 +7230,12 @@ def stats_page():
|
|||||||
return render_main_page("stats")
|
return render_main_page("stats")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/dashboard")
|
||||||
|
@login_required
|
||||||
|
def dashboard_page():
|
||||||
|
return render_main_page("dashboard")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/risk_policy")
|
@app.route("/risk_policy")
|
||||||
@login_required
|
@login_required
|
||||||
def risk_policy_page():
|
def risk_policy_page():
|
||||||
@@ -9314,6 +9320,15 @@ register_trade_records_api(
|
|||||||
app_tz=APP_TZ,
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -6858,6 +6858,15 @@ def stats_page():
|
|||||||
return render_main_page("stats")
|
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")
|
@app.route("/risk_policy")
|
||||||
@login_required
|
@login_required
|
||||||
def risk_policy_page():
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
|||||||
+10
-9
@@ -26,15 +26,16 @@
|
|||||||
|
|
||||||
### 2.1 顶栏导航开关
|
### 2.1 顶栏导航开关
|
||||||
|
|
||||||
| 开关 | 对应 Tab |
|
| 开关 | 对应 Tab | 默认 |
|
||||||
|------|----------|
|
|------|----------|------|
|
||||||
| 策略交易 | 策略交易 |
|
| 数据看板 | 数据看板(本户活跃监控总览) | **关闭** |
|
||||||
| 策略交易记录 | 策略交易记录 |
|
| 策略交易 | 策略交易 | 开 |
|
||||||
| 交易记录与复盘 | 交易记录与复盘 |
|
| 策略交易记录 | 策略交易记录 | 开 |
|
||||||
| 统计分析 | 统计分析 |
|
| 交易记录与复盘 | 交易记录与复盘 | 开 |
|
||||||
| 风控说明 | 风控说明 |
|
| 统计分析 | 统计分析 | 开 |
|
||||||
| env 配置 | env 配置 |
|
| 风控说明 | 风控说明 | 开 |
|
||||||
| 期权 | 期权(仅 OKX 等有期权模块时有效) |
|
| env 配置 | env 配置 | 开 |
|
||||||
|
| 期权 | 期权(仅 OKX 等有期权模块时有效) | 开 |
|
||||||
|
|
||||||
保存后 **立即生效**,无需重启.中控 iframe 内嵌导航同步生效.
|
保存后 **立即生效**,无需重启.中控 iframe 内嵌导航同步生效.
|
||||||
|
|
||||||
|
|||||||
@@ -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, ">")
|
||||||
|
.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 '<span class="' + cls + '">' + sign + n.toFixed(2) + "U</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '<p class="inst-dash-empty muted">暂无</p>';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'<div class="inst-dash-list">' +
|
||||||
|
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 (
|
||||||
|
'<button type="button" class="inst-dash-item" data-dash-tab="' +
|
||||||
|
escapeHtml(it.tab || "") +
|
||||||
|
'">' +
|
||||||
|
'<div class="inst-dash-item-title">' +
|
||||||
|
escapeHtml(it.title || "-") +
|
||||||
|
"</div>" +
|
||||||
|
(it.subtitle
|
||||||
|
? '<div class="inst-dash-item-sub muted">' + escapeHtml(it.subtitle) + "</div>"
|
||||||
|
: "") +
|
||||||
|
(meta.length ? '<div class="inst-dash-item-meta">' + meta.join(" · ") + "</div>" : "") +
|
||||||
|
"</button>"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("") +
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSection(key, sec) {
|
||||||
|
if (!sec) return "";
|
||||||
|
if ((key === "options" || key === "hedge_plan") && !sec.visible) return "";
|
||||||
|
const count = Number(sec.count) || 0;
|
||||||
|
return (
|
||||||
|
'<section class="inst-dash-section" data-dash-section="' +
|
||||||
|
escapeHtml(key) +
|
||||||
|
'">' +
|
||||||
|
'<div class="inst-dash-section-head">' +
|
||||||
|
"<h3>" +
|
||||||
|
escapeHtml(sec.title || key) +
|
||||||
|
' <span class="inst-dash-count">' +
|
||||||
|
count +
|
||||||
|
"</span></h3>" +
|
||||||
|
'<button type="button" class="btn-sm inst-dash-goto" data-dash-tab="' +
|
||||||
|
escapeHtml(sec.tab || "") +
|
||||||
|
'">打开</button>' +
|
||||||
|
"</div>" +
|
||||||
|
renderItems(sec.items || []) +
|
||||||
|
"</section>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
(function (global) {
|
(function (global) {
|
||||||
const TAB_PATH = {
|
const TAB_PATH = {
|
||||||
|
dashboard: "/dashboard",
|
||||||
key_monitor: "/key_monitor",
|
key_monitor: "/key_monitor",
|
||||||
trade: "/trade",
|
trade: "/trade",
|
||||||
strategy: "/strategy",
|
strategy: "/strategy",
|
||||||
@@ -99,6 +100,9 @@
|
|||||||
if (!revisit && tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
|
if (!revisit && tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
|
||||||
global.KeyMonitorForm.init();
|
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") {
|
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
|
||||||
global.initStrategyRollForm();
|
global.initStrategyRollForm();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
|
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
|
||||||
global.OptionsPanelLive.refreshSoft(options);
|
global.OptionsPanelLive.refreshSoft(options);
|
||||||
}
|
}
|
||||||
|
if (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.refreshSoft === "function") {
|
||||||
|
global.InstanceDashboard.refreshSoft(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRefresh(opts) {
|
function scheduleRefresh(opts) {
|
||||||
|
|||||||
@@ -269,3 +269,20 @@
|
|||||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
.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-page-root{min-height:120px;position:relative}
|
||||||
.embed-tab-pane[hidden]{display:none!important}
|
.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}
|
||||||
|
|||||||
@@ -19,8 +19,18 @@
|
|||||||
return data;
|
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) {
|
function applyDisplayToNav(display) {
|
||||||
const map = {
|
const map = {
|
||||||
|
dashboard: "show_nav_dashboard",
|
||||||
strategy: "show_nav_strategy",
|
strategy: "show_nav_strategy",
|
||||||
strategy_records: "show_nav_strategy_records",
|
strategy_records: "show_nav_strategy_records",
|
||||||
records: "show_nav_records",
|
records: "show_nav_records",
|
||||||
@@ -37,7 +47,7 @@
|
|||||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||||
const key = map[tab];
|
const key = map[tab];
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const show = display[key] !== false;
|
const show = navPrefShow(display, key);
|
||||||
a.classList.toggle("nav-hidden", !show);
|
a.classList.toggle("nav-hidden", !show);
|
||||||
a.style.display = show ? "" : "none";
|
a.style.display = show ? "" : "none";
|
||||||
});
|
});
|
||||||
@@ -47,6 +57,7 @@
|
|||||||
function pageNavAllowed(tab) {
|
function pageNavAllowed(tab) {
|
||||||
const d = DISPLAY();
|
const d = DISPLAY();
|
||||||
const map = {
|
const map = {
|
||||||
|
dashboard: "show_nav_dashboard",
|
||||||
strategy: "show_nav_strategy",
|
strategy: "show_nav_strategy",
|
||||||
strategy_records: "show_nav_strategy_records",
|
strategy_records: "show_nav_strategy_records",
|
||||||
records: "show_nav_records",
|
records: "show_nav_records",
|
||||||
@@ -61,7 +72,7 @@
|
|||||||
};
|
};
|
||||||
const key = map[tab];
|
const key = map[tab];
|
||||||
if (!key) return true;
|
if (!key) return true;
|
||||||
return d[key] !== false;
|
return navPrefShow(d, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayPrefsRoot() {
|
function displayPrefsRoot() {
|
||||||
@@ -129,7 +140,9 @@
|
|||||||
const cb = document.createElement("input");
|
const cb = document.createElement("input");
|
||||||
cb.type = "checkbox";
|
cb.type = "checkbox";
|
||||||
cb.dataset.prefKey = item.key;
|
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(cb);
|
||||||
label.appendChild(document.createTextNode(" " + item.label));
|
label.appendChild(document.createTextNode(" " + item.label));
|
||||||
grid.appendChild(label);
|
grid.appendChild(label);
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ def install_instance_theme_static(app) -> None:
|
|||||||
"instance_stats.js": "application/javascript; charset=utf-8",
|
"instance_stats.js": "application/javascript; charset=utf-8",
|
||||||
"instance_live.js": "application/javascript; charset=utf-8",
|
"instance_live.js": "application/javascript; charset=utf-8",
|
||||||
"instance_settings_prefs.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",
|
"options_panel.js": "application/javascript; charset=utf-8",
|
||||||
"order_entry_model.js": "application/javascript; charset=utf-8",
|
"order_entry_model.js": "application/javascript; charset=utf-8",
|
||||||
"focus_chart_page.js": "application/javascript; charset=utf-8",
|
"focus_chart_page.js": "application/javascript; charset=utf-8",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -8,6 +8,7 @@ from lib.instance.runtime_settings_lib import runtime_get_prefix, runtime_set_ma
|
|||||||
DISPLAY_RUNTIME_PREFIX = "display."
|
DISPLAY_RUNTIME_PREFIX = "display."
|
||||||
|
|
||||||
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||||
|
"show_nav_dashboard": False,
|
||||||
"show_nav_strategy": True,
|
"show_nav_strategy": True,
|
||||||
"show_nav_strategy_records": True,
|
"show_nav_strategy_records": True,
|
||||||
"show_nav_records": True,
|
"show_nav_records": True,
|
||||||
@@ -25,6 +26,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DISPLAY_LABELS: dict[str, str] = {
|
DISPLAY_LABELS: dict[str, str] = {
|
||||||
|
"show_nav_dashboard": "数据看板",
|
||||||
"show_nav_strategy": "策略交易",
|
"show_nav_strategy": "策略交易",
|
||||||
"show_nav_strategy_records": "策略交易记录",
|
"show_nav_strategy_records": "策略交易记录",
|
||||||
"show_nav_records": "交易记录与复盘",
|
"show_nav_records": "交易记录与复盘",
|
||||||
@@ -42,6 +44,7 @@ DISPLAY_LABELS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NAV_TAB_ALLOWED: dict[str, str] = {
|
NAV_TAB_ALLOWED: dict[str, str] = {
|
||||||
|
"dashboard": "show_nav_dashboard",
|
||||||
"strategy": "show_nav_strategy",
|
"strategy": "show_nav_strategy",
|
||||||
"strategy_records": "show_nav_strategy_records",
|
"strategy_records": "show_nav_strategy_records",
|
||||||
"records": "show_nav_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]]:
|
def display_meta_for_ui() -> list[dict[str, Any]]:
|
||||||
nav_keys = [
|
nav_keys = [
|
||||||
|
"show_nav_dashboard",
|
||||||
"show_nav_strategy",
|
"show_nav_strategy",
|
||||||
"show_nav_strategy_records",
|
"show_nav_strategy_records",
|
||||||
"show_nav_records",
|
"show_nav_records",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from flask import Flask, Response, jsonify, redirect, request, session
|
|||||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||||
|
|
||||||
EMBED_TABS: tuple[str, ...] = (
|
EMBED_TABS: tuple[str, ...] = (
|
||||||
|
"dashboard",
|
||||||
"key_monitor",
|
"key_monitor",
|
||||||
"trade",
|
"trade",
|
||||||
"strategy",
|
"strategy",
|
||||||
@@ -28,6 +29,7 @@ EMBED_TABS: tuple[str, ...] = (
|
|||||||
PATH_TO_EMBED_TAB: dict[str, str] = {
|
PATH_TO_EMBED_TAB: dict[str, str] = {
|
||||||
"/": "trade",
|
"/": "trade",
|
||||||
"/trade": "trade",
|
"/trade": "trade",
|
||||||
|
"/dashboard": "dashboard",
|
||||||
"/key_monitor": "key_monitor",
|
"/key_monitor": "key_monitor",
|
||||||
"/strategy": "strategy",
|
"/strategy": "strategy",
|
||||||
"/strategy/trend": "strategy",
|
"/strategy/trend": "strategy",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{# 实例数据看板:只读活跃监控总览 #}
|
||||||
|
<div class="card full inst-dash-card" id="instance-dashboard" data-inst-dashboard="1">
|
||||||
|
<div class="inst-dash-head">
|
||||||
|
<div>
|
||||||
|
<h2 style="margin-bottom:4px">数据看板</h2>
|
||||||
|
<p class="muted inst-dash-desc">本户活跃监控总览 · 只读 · 期权/对冲有数据才显示</p>
|
||||||
|
</div>
|
||||||
|
<div class="inst-dash-head-actions">
|
||||||
|
<span class="muted inst-dash-updated" id="inst-dash-updated">—</span>
|
||||||
|
<button type="button" class="btn-sm" id="inst-dash-refresh">刷新</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="inst-dash-status muted" id="inst-dash-status"></p>
|
||||||
|
<div class="inst-dash-sections" id="inst-dash-sections"></div>
|
||||||
|
</div>
|
||||||
@@ -81,7 +81,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{% if page == 'key_monitor' %}
|
{% if page == 'dashboard' %}
|
||||||
|
{% include 'dashboard_panel.html' %}
|
||||||
|
{% elif page == 'key_monitor' %}
|
||||||
{% include 'key_monitor_panel.html' %}
|
{% include 'key_monitor_panel.html' %}
|
||||||
{% elif page == 'trade' %}
|
{% elif page == 'trade' %}
|
||||||
<div class="dual-panel-grid" style="grid-column:1/-1">
|
<div class="dual-panel-grid" style="grid-column:1/-1">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<script src="/static/instance_theme.js?v=50"></script>
|
<script src="/static/instance_theme.js?v=50"></script>
|
||||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
<link rel="stylesheet" href="/static/instance_page.css?v=7">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
||||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||||
<meta name="theme-color" content="#0b0d14">
|
<meta name="theme-color" content="#0b0d14">
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="top-nav embed-top-nav" aria-label="实例导航">
|
<nav class="top-nav embed-top-nav" aria-label="实例导航">
|
||||||
|
<a href="/dashboard" data-embed-tab="dashboard" class="{% if initial_tab == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||||
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||||
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}">实盘下单</a>
|
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}">实盘下单</a>
|
||||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||||
@@ -109,11 +110,12 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
|||||||
<script src="/static/instance_stats.js?v=4"></script>
|
<script src="/static/instance_stats.js?v=4"></script>
|
||||||
{% include 'embed_boot_scripts.html' %}
|
{% include 'embed_boot_scripts.html' %}
|
||||||
<script src="/static/records_review_page.js?v=2"></script>
|
<script src="/static/records_review_page.js?v=2"></script>
|
||||||
|
<script src="/static/instance_dashboard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
<script src="/static/instance_settings_prefs.js?v=14"></script>
|
||||||
<script src="/static/instance_live.js?v=5"></script>
|
<script src="/static/instance_live.js?v=6"></script>
|
||||||
<script src="/static/instance_embed.js?v=24"></script>
|
<script src="/static/instance_embed.js?v=25"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||||
<title>{{ pwa_app_name }}</title>
|
<title>{{ pwa_app_name }}</title>
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
<link rel="stylesheet" href="/static/instance_page.css?v=7">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=95">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
<h1>加密货币|交易监控 + AI复盘一体化</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
|
<a href="/dashboard" data-embed-tab="dashboard" class="{% if page == 'dashboard' %}active{% endif %}"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||||
@@ -155,7 +156,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{% if page == 'key_monitor' %}
|
{% if page == 'dashboard' %}
|
||||||
|
{% include 'dashboard_panel.html' %}
|
||||||
|
{% elif page == 'key_monitor' %}
|
||||||
{% include 'key_monitor_panel.html' %}
|
{% include 'key_monitor_panel.html' %}
|
||||||
{% elif page == 'trade' %}
|
{% elif page == 'trade' %}
|
||||||
<div class="dual-panel-grid" style="grid-column:1/-1">
|
<div class="dual-panel-grid" style="grid-column:1/-1">
|
||||||
@@ -1989,9 +1992,15 @@ tickOrderHoldDurations();
|
|||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/records_review_page.js?v=2"></script>
|
<script src="/static/records_review_page.js?v=2"></script>
|
||||||
|
<script src="/static/instance_dashboard.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
|
{% if page == 'dashboard' %}
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (window.InstanceDashboard) InstanceDashboard.init(true);
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
<script src="/static/instance_settings_prefs.js?v=14"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"""instance_dashboard_lib 单元测试."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from lib.instance.instance_dashboard_lib import build_instance_dashboard_payload
|
||||||
|
|
||||||
|
|
||||||
|
def _mem_conn() -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE order_monitors (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
symbol TEXT,
|
||||||
|
exchange_symbol TEXT,
|
||||||
|
direction TEXT,
|
||||||
|
status TEXT,
|
||||||
|
monitor_type TEXT,
|
||||||
|
key_signal_type TEXT,
|
||||||
|
trigger_price REAL,
|
||||||
|
stop_loss REAL,
|
||||||
|
take_profit REAL
|
||||||
|
);
|
||||||
|
CREATE TABLE key_monitors (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
symbol TEXT,
|
||||||
|
exchange_symbol TEXT,
|
||||||
|
direction TEXT,
|
||||||
|
signal_type TEXT,
|
||||||
|
upper REAL,
|
||||||
|
lower REAL,
|
||||||
|
status TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE trend_pullback_plans (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
symbol TEXT,
|
||||||
|
exchange_symbol TEXT,
|
||||||
|
direction TEXT,
|
||||||
|
status TEXT,
|
||||||
|
entry_price REAL
|
||||||
|
);
|
||||||
|
CREATE TABLE roll_groups (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
order_monitor_id INTEGER,
|
||||||
|
symbol TEXT,
|
||||||
|
exchange_symbol TEXT,
|
||||||
|
direction TEXT,
|
||||||
|
status TEXT
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstanceDashboardLib(unittest.TestCase):
|
||||||
|
def test_empty_sections_and_conditional_hidden(self):
|
||||||
|
conn = _mem_conn()
|
||||||
|
payload = build_instance_dashboard_payload(conn, hedge_enabled=True)
|
||||||
|
self.assertTrue(payload["ok"])
|
||||||
|
self.assertEqual(payload["orders"]["count"], 0)
|
||||||
|
self.assertEqual(payload["keys"]["count"], 0)
|
||||||
|
self.assertEqual(payload["strategy"]["count"], 0)
|
||||||
|
self.assertFalse(payload["options"]["visible"])
|
||||||
|
self.assertFalse(payload["hedge_plan"]["visible"])
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_orders_keys_strategy_and_options_visible(self):
|
||||||
|
conn = _mem_conn()
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO order_monitors (symbol, exchange_symbol, direction, status, monitor_type) "
|
||||||
|
"VALUES ('BTC/USDT', 'BTC/USDT:USDT', 'long', 'active', 'manual')"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO key_monitors (symbol, direction, signal_type, upper, lower, status) "
|
||||||
|
"VALUES ('ETH/USDT', 'short', '箱体突破', 3000, 2800, 'active')"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO trend_pullback_plans (symbol, direction, status, entry_price) "
|
||||||
|
"VALUES ('SOL/USDT', 'long', 'active', 100)"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO order_monitors (id, symbol, direction, status) VALUES (9, 'XRP/USDT', 'short', 'active')"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO roll_groups (order_monitor_id, symbol, direction, status) "
|
||||||
|
"VALUES (9, 'XRP/USDT', 'short', 'active')"
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def fetch_opts():
|
||||||
|
return [{"inst_id": "ETH-USD-260731-3000-C", "opt_type": "C", "pos": 1, "upl": 1.5}]
|
||||||
|
|
||||||
|
payload = build_instance_dashboard_payload(
|
||||||
|
conn,
|
||||||
|
fetch_options_positions=fetch_opts,
|
||||||
|
hedge_enabled=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(payload["orders"]["count"], 2)
|
||||||
|
self.assertEqual(payload["keys"]["count"], 1)
|
||||||
|
self.assertEqual(payload["strategy"]["count"], 2)
|
||||||
|
self.assertTrue(payload["options"]["visible"])
|
||||||
|
self.assertEqual(payload["options"]["count"], 1)
|
||||||
|
self.assertFalse(payload["hedge_plan"]["visible"])
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -15,12 +15,19 @@ class TestInstanceDisplayPrefs(unittest.TestCase):
|
|||||||
prefs = normalize_display_prefs({})
|
prefs = normalize_display_prefs({})
|
||||||
self.assertTrue(prefs["show_nav_env_config"])
|
self.assertTrue(prefs["show_nav_env_config"])
|
||||||
self.assertTrue(prefs["show_settings_password"])
|
self.assertTrue(prefs["show_settings_password"])
|
||||||
|
self.assertFalse(prefs["show_nav_dashboard"])
|
||||||
|
|
||||||
def test_tab_allowed_respects_prefs(self):
|
def test_tab_allowed_respects_prefs(self):
|
||||||
prefs = normalize_display_prefs({"show_nav_stats": False})
|
prefs = normalize_display_prefs({"show_nav_stats": False})
|
||||||
self.assertFalse(tab_allowed("stats", prefs))
|
self.assertFalse(tab_allowed("stats", prefs))
|
||||||
self.assertTrue(tab_allowed("trade", prefs))
|
self.assertTrue(tab_allowed("trade", prefs))
|
||||||
|
|
||||||
|
def test_dashboard_nav_default_off(self):
|
||||||
|
prefs = normalize_display_prefs({})
|
||||||
|
self.assertFalse(tab_allowed("dashboard", prefs))
|
||||||
|
on = normalize_display_prefs({"show_nav_dashboard": True})
|
||||||
|
self.assertTrue(tab_allowed("dashboard", on))
|
||||||
|
|
||||||
|
|
||||||
class TestEnvFileLib(unittest.TestCase):
|
class TestEnvFileLib(unittest.TestCase):
|
||||||
def test_upsert_and_read(self):
|
def test_upsert_and_read(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user