From 5b978e20e4aaadd0041d2548978d190bb4de5b2d Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 12 Aug 2026 10:52:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20F=E5=81=A5=E5=BA=B7=E4=B8=89=E5=B1=82?= =?UTF-8?q?=E6=94=B9=E9=80=A0step3=E2=80=94=E2=80=94=E8=87=AA=E6=A3=80?= =?UTF-8?q?=E4=BD=93=E7=B3=BB=E5=8A=A0=E4=B8=89=E5=B1=82=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E5=8D=A1=E7=89=87(=E9=87=87=E9=9B=86/?= =?UTF-8?q?=E5=8A=A0=E5=B7=A5/=E4=BD=BF=E7=94=A8=E5=90=84=E5=B1=82?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=96=B0=E9=B2=9C=E5=BA=A6+=E8=A6=86?= =?UTF-8?q?=E7=9B=96+=E4=BB=BB=E5=8A=A1=E7=8A=B6=E6=80=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/mofin_health.py | 56 ++++++++++++++++++++++++++ static/mofin_health.html | 31 ++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/deploy/profile-scripts/mofin_health.py b/deploy/profile-scripts/mofin_health.py index 7664076a..d7ac625a 100644 --- a/deploy/profile-scripts/mofin_health.py +++ b/deploy/profile-scripts/mofin_health.py @@ -1144,6 +1144,62 @@ def build_report(): except Exception as _e: self_check["llm_health"] = {"ok": True, "error": str(_e)[:100]} + # ── 2026-08-12 三层架构健康检查(采集/加工/使用三层各自健康,老莫定)── + def _layer_health(): + health = {} + try: + _c = sqlite3.connect(DB_PATH) + # 采集层健康:原始数据新鲜度 + 覆盖范围 + collect = {} + for tname, sql in { + "stock_daily": "SELECT MAX(date), COUNT(DISTINCT code) FROM stock_daily WHERE length(code)=6", + "stock_news": "SELECT MAX(date), COUNT(*) FROM stock_news", + "stock_fundamentals": "SELECT MAX(updated_at), COUNT(*) FROM stock_fundamentals", + "sector_index_daily": "SELECT MAX(date), COUNT(DISTINCT sector) FROM sector_index_daily", + }.items(): + try: + r = _c.execute(sql).fetchone() + collect[tname] = {"latest": r[0], "count": r[1]} + except Exception as e: + collect[tname] = {"latest": None, "count": 0, "error": str(e)[:50]} + health["collect"] = collect + # 加工层健康:衍生指标新鲜度 + 覆盖范围 + process = {} + for tname, sql in { + "stock_indicators": "SELECT MAX(date), COUNT(DISTINCT code) FROM stock_indicators", + "market_indicators": "SELECT MAX(date), COUNT(*) FROM market_indicators", + }.items(): + try: + r = _c.execute(sql).fetchone() + process[tname] = {"latest": r[0], "count": r[1]} + except Exception as e: + process[tname] = {"latest": None, "count": 0, "error": str(e)[:50]} + health["process"] = process + # 使用层健康:业务数据 + 候选管道 + r1 = _c.execute("SELECT COUNT(*), MAX(created_at) FROM candidates").fetchone() + r2 = _c.execute("SELECT COUNT(*) FROM holding_strategies WHERE status='active'").fetchone() + health["use"] = { + "candidates": {"count": r1[0], "latest": r1[1]}, + "active_strategies": r2[0], + } + _c.close() + except Exception as e: + health["error"] = str(e)[:100] + # 任务状态按层汇总(pipelines 已有 layer 字段) + layer_tasks = {} + for p in pipelines: + layer = p.get("layer", "use") + layer_tasks.setdefault(layer, {"ok": 0, "error": 0, "total": 0}) + layer_tasks[layer]["total"] += 1 + if p.get("status") == "ok": + layer_tasks[layer]["ok"] += 1 + elif p.get("status") in ("error", "fail"): + layer_tasks[layer]["error"] += 1 + health["tasks"] = layer_tasks + return health + + self_check["layers_health"] = _layer_health() + # ── 写JSON ── report = { "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"), diff --git a/static/mofin_health.html b/static/mofin_health.html index 1eba3ea6..004ca565 100644 --- a/static/mofin_health.html +++ b/static/mofin_health.html @@ -84,6 +84,37 @@ function renderSelfCheck(sc) { let html = ''; const icon = s => s === 'ok' ? '✅' : s === 'warn' ? '🟡' : s === 'fail' ? '❌' : '⏭'; + // 2026-08-12 三层架构健康(采集/加工/使用三层各自健康,老莫定) + if (sc.layers_health) { + const lh = sc.layers_health; + const tasks = lh.tasks || {}; + const layerMeta = [ + ['collect', '📥 数据采集层', '#3fb950', lh.collect || {}], + ['process', '⚙️ 数据加工层', '#d29922', lh.process || {}], + ['use', '📊 数据使用层', '#58a6ff', lh.use || {}], + ]; + html += `
`; + html += `
🏗️ 三层架构健康(采集→加工→使用)
`; + layerMeta.forEach(([layer, title, color, data]) => { + const t = tasks[layer] || {ok:0, error:0, total:0}; + const tstatus = t.error > 0 ? '❌' : t.ok === t.total ? '✅' : '🟡'; + html += `
`; + html += `${title} 任务 ${tstatus} ✅${t.ok} ❌${t.error} 共${t.total}`; + html += ''; + Object.entries(data).forEach(([k, v]) => { + if (k === 'candidates' || k === 'active_strategies') return; + const latest = (v.latest || '无').toString().slice(0, 16); + const cnt = v.count !== undefined ? v.count : (typeof v === 'number' ? v : '-'); + html += ``; + }); + if (layer === 'use' && data.candidates) { + html += ``; + } + html += '
数据最新覆盖/数量
${k}${latest}${cnt}
candidates${(data.candidates.latest||'').toString().slice(0,16)}${data.candidates.count}(持仓策略${data.active_strategies||0})
'; + }); + html += '
'; + } + // L4 元监控(自检系统的自检) if (sc.meta_watchdog) { const mw = sc.meta_watchdog;