#!/usr/bin/env python3 """mofin_health.py — MoFin 健康监控数据采集 输出JSON供dashboard展示,三个view: tab1: 功能树(逐级展开,每节点绿/黄/红) tab2: 数据实体表(输入/输出流分析,孤立表报警) tab3: 流程/cron映射(状态正常/异常) """ import json, sqlite3, os, sys, re from pathlib import Path from datetime import datetime, timezone DATA_DIR = Path("/home/hmo/MoFin/data") WEB_DATA = Path("/home/hmo/web-dashboard/data") STATIC_DIR = Path("/home/hmo/web-dashboard/static") PROFILE_SCRIPTS = Path("/home/hmo/.hermes/profiles/position-analyst/scripts") CRON_FILES = [ "/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json", "/home/hmo/.hermes/cron/jobs.json", ] # 数据实体作用说明 TABLES_DESC = { "holdings": "当前持仓(权威源)", "holding_strategies": "每只股票的完整策略参数", "portfolio_summary": "总资产/现金/仓位汇总", "portfolio_state": "组合状态快照(只读派生)", "strategy_evaluations": "策略重评历史记录", "strategy_feedback": "策略效果反馈", "watchlist_stocks": "自选股列表", "candidates": "潜力股候选池(小果扫描产出)", "live_prices": "所有持仓+自选最新实时价", "price_events": "价格区间突破事件日志", "market_snapshots": "大盘指数快照(每10分)", "sector_snapshots": "行业板块数据", "sector_signals": "行业信号(趋势检测产出)", "signal_news": "信号相关新闻", "macro_raw_news": "宏观新闻原始数据", "macro_context_log": "宏观上下文(大盘偏向/指数)", "stocks": "全量股票代码", "stock_daily": "日线行情", "stock_weekly": "周线行情", "stock_monthly": "月线行情", "stock_fundamentals": "基本面数据(PE/PB)", "stock_sectors": "股票行业映射", "capital_flow_cache": "资金流缓存", "xiaoguo_scan_tracker": "小果扫描跟踪", "advice_timeline": "建议执行时间线", "accuracy_stats": "建议准确率统计", "todos": "自愈任务队列", "health_check_log": "健康检查日志", "cash_log": "资金变动记录", "mtf_cache": "多周期均线缓存", "state_meta": "系统状态元数据", } JSON_DESC = { "decisions.json": "策略决策(DB→JSON同步,兼容层)", "portfolio.json": "持仓汇总(兼容层)", "market.json": "市场概况数据", "xiaoguo_insights.json": "小果分析洞察", "candidate_pool.json": "潜力股候选池完整数据", "zone_breach.json": "价格区间突破状态", "strategy_staleness_report.json": "策略过期报告", "alerts.json": "告警列表", "macro_risk_state.json": "宏观风险状态(采集器写入)", "capital_flow_cache.json": "资金流缓存", "multi_tf_cache.json": "多周期均线缓存", "macro_context.json": "宏观上下文JSON(旧兼容层)", "system_inventory.json": "全量系统清单", "mofin_health.json": "健康监控数据", } now = datetime.now() def load_cron_jobs(): jobs = [] seen = set() for jf in CRON_FILES: try: for j in json.load(open(jf)).get("jobs", []): jid = j.get("id", "") if jid in seen: continue seen.add(jid) jobs.append(j) except: pass return jobs def get_db_stats(): conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").fetchall() stats = {} for (tname,) in tables: cnt = conn.execute(f"SELECT COUNT(*) FROM \"{tname}\"").fetchone()[0] stats[tname] = cnt conn.close() return stats def scan_data_flows(): """对每个脚本,扫描它读/写了哪些DB表和JSON文件""" flows = {"db_read": {}, "db_write": {}, "json_read": {}, "json_write": {}} for py in sorted(PROFILE_SCRIPTS.glob("*.py")): name = py.stem content = py.read_text(encoding="utf-8", errors="ignore") # DB reads: SELECT FROM reads = set(re.findall(r'FROM\s+(\w+)', content, re.I)) reads |= set(re.findall(r'join\s+(\w+)', content, re.I)) # DB writes: INSERT INTO / UPDATE / DELETE FROM writes = set(re.findall(r'INSERT\s+(?:OR\s+\w+\s+)?INTO\s+(\w+)', content, re.I)) writes |= set(re.findall(r'UPDATE\s+(\w+)', content, re.I)) writes |= set(re.findall(r'DELETE\s+FROM\s+(\w+)', content, re.I)) # JSON reads: json.load/open json_r = set(re.findall(r'(?:json\.load|open)\s*\(\s*["\']([^"\']+\.json)', content)) json_w = set(re.findall(r'(?:json\.dump|json\.dumps)\s*\(', content)) for t in reads: flows["db_read"].setdefault(t, set()).add(name) for t in writes: flows["db_write"].setdefault(t, set()).add(name) for f in json_r: fname = os.path.basename(f) flows["json_read"].setdefault(fname, set()).add(name) if json_w: flows["json_write"].setdefault(name, set()).add(name) return {k: {kk: list(vv) for kk, vv in v.items()} for k, v in flows.items()} def check_scripts(): """检查每个脚本是否有语法错误或明显问题""" issues = {} for py in sorted(PROFILE_SCRIPTS.glob("*.py")): r = os.system(f"python3 -m py_compile {py} 2>/dev/null") issues[py.stem] = "ok" if r == 0 else "syntax_error" return issues def build_report(): cron_jobs = load_cron_jobs() db_stats = get_db_stats() flows = scan_data_flows() script_health = check_scripts() # ── Tab 1: 功能树 ── feature_tree = { "label": "MoFin 系统", "status": "ok", "children": [ {"label": "数据采集", "status": "ok", "children": [ {"label": f"市场快照 (market_watch.py)", "status": "ok" if any(j.get("name")=="市场数据采集" and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"宏观新闻 (macro_context_collector.py)", "status": "ok" if any("宏观采集" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"价格监控 (price_monitor.py)", "status": "ok" if any("价格监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"小果扫描 (xiaoguo_scanner.py)", "status": "ok" if any("小果独立扫描" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"资金流采集 (capital_flow_collector.py)", "status": "ok" if any("资金流" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"宏观上下文刷新 (refresh_macro_context.py)", "status": "ok"}, ]}, {"label": "策略分析", "status": "ok", "children": [ {"label": f"策略重评 (mofin_collect→reassess_with_context)", "status": "ok" if db_stats.get("strategy_evaluations",0) > 200 else "warn"}, {"label": f"持仓自选新鲜度检查", "status": "ok"}, {"label": f"自选买入区提醒 (stale_push_wlin.py)", "status": "ok" if any("自选买入区提醒" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"策略评估 (strategy_evaluator.py)", "status": "ok" if any("策略评估" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"分支自成长 (branch_scanner.py)", "status": "ok"}, {"label": f"元自成长 (meta_growth.py)", "status": "ok"}, ]}, {"label": "推荐推送", "status": "ok", "children": [ {"label": f"MoFin盘前中监控 (LLM cron)", "status": "ok" if any("MoFin盘前中监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"MoFin午后监控 (LLM cron)", "status": "ok" if any("MoFin午后监控" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"cron报告推XMPP (cron_to_xmpp.py)", "status": "ok"}, {"label": f"开盘简报 (LLM cron)", "status": "ok"}, {"label": f"收盘简报 (LLM cron)", "status": "ok"}, {"label": f"市场精选推荐 (LLM cron)", "status": "ok" if any("市场精选推荐" in j.get("name","") and j.get("enabled")==True for j in cron_jobs) else "warn"}, ]}, {"label": "自检/审计", "status": "ok", "children": [ {"label": f"系统全局审计 (system_audit.py)", "status": "ok"}, {"label": f"全局cron健康监控 (cron_health_monitor.py)", "status": "ok"}, {"label": f"重评管道审计 (verify_reassess_pipeline.py)", "status": "ok"}, {"label": f"系统体检 (morning_health_check.py)", "status": "ok"}, {"label": f"盘中自检 (intraday_health_check.py)", "status": "ok"}, {"label": f"记忆守卫 (memory_guardian.py)", "status": "ok"}, {"label": f"硬编码扫描 (hardcode_scanner.py)", "status": "ok"}, ]}, {"label": "风险监控", "status": "ok", "children": [ {"label": f"宏观风险扫描 (LLM cron)", "status": "ok"}, {"label": f"宏观风险信号消费 (macro_signal_consumer.py)", "status": "ok" if any("宏观风险信号消费" in j.get("name","") and j.get("last_status")=="ok" for j in cron_jobs) else "warn"}, {"label": f"跨市场背离检测 (divergence_detector.py)", "status": "ok"}, ]}, {"label": "执行/修复", "status": "ok", "children": [ {"label": f"自愈执行器 (self_todo_executor.py)", "status": "ok"}, {"label": f"策略质量门禁 (review_needed_watchdog.py)", "status": "ok"}, {"label": f"自选自动清理 (clean_watchlist.py)", "status": "warn" if any("自选自动清理" in j.get("name","") and j.get("last_status")=="error" for j in cron_jobs) else "ok"}, {"label": f"建议对账 (advice_reconciliation.py)", "status": "ok"}, ]}, {"label": "系统服务", "status": "ok", "children": [ {"label": "XMPP Bot (zhiwei)", "status": "ok" if os.system("systemctl is-active xmpp-zhiwei >/dev/null 2>&1")==0 else "fail"}, {"label": "Gateway (8643)", "status": "ok" if os.system("ss -tlnp | grep -q 8643")==0 else "fail"}, {"label": "HTTP Bridge (5805)", "status": "ok" if os.system("ss -tlnp | grep -q 5805")==0 else "fail"}, {"label": "Dashboard (8899)", "status": "ok" if os.system("ss -tlnp | grep -q 8899")==0 else "warn"}, {"label": "state.db SQLite", "status": "ok"}, ]}, ] } # 递归计算节点状态 def calc_status(node): if "children" in node: for c in node["children"]: calc_status(c) statuses = [c["status"] for c in node["children"]] if "fail" in statuses: node["status"] = "fail" elif "warn" in statuses: node["status"] = "warn" else: node["status"] = "ok" calc_status(feature_tree) # ── Tab 2: 数据实体表 ── entities = [] for tname, cnt in sorted(db_stats.items()): readers = flows["db_read"].get(tname, []) writers = flows["db_write"].get(tname, []) has_input = len(writers) > 0 has_output = len(readers) > 0 # 排除系统表 is_system = tname.startswith("sqlite_") or tname.startswith("_") orphan = (not has_input or not has_output) and not is_system and cnt > 0 entities.append({ "name": tname, "desc": TABLES_DESC.get(tname, ""), "rows": cnt, "readers": readers[:10], "writers": writers[:10], "has_input": has_input, "has_output": has_output, "orphan": orphan, "warn": orphan or (cnt == 0 and tname not in ("capital_flow_cache","")), }) # JSON文件 json_entities = [] for jf in sorted(WEB_DATA.glob("*.json")): if jf.name == "stocks": continue if jf.stem.startswith("temp_"): continue readers = flows["json_read"].get(jf.name, []) size = jf.stat().st_size / 1024 json_entities.append({ "name": jf.name, "desc": JSON_DESC.get(jf.name, ""), "size_kb": round(size, 1), "readers": readers[:10], "writers": [], # 难以精确追踪 "last_modified": datetime.fromtimestamp(jf.stat().st_mtime).strftime("%m-%d %H:%M"), "warn": len(readers) == 0 and jf.name not in ("portfolio.json", "market.json"), }) # ── Tab 3: 流程/cron映射 ── pipelines = [] for j in sorted(cron_jobs, key=lambda x: x.get("name","")): if not j.get("enabled", True): continue name = j.get("name", "?") script = j.get("script", "") status = j.get("last_status", "unknown") last_run = str(j.get("last_run_at", ""))[:19] schedule = j.get("schedule", {}).get("display", str(j.get("schedule",""))) no_agent = j.get("no_agent", False) pipelines.append({ "name": name, "type": "no_agent" if no_agent else "LLM", "script": script, "schedule": schedule, "status": status, "last_run": last_run, }) # ── 写JSON ── report = { "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"), "feature_tree": feature_tree, "entities": entities, "json_files": json_entities, "pipelines": pipelines, } out_path = WEB_DATA / "mofin_health.json" with open(out_path, "w") as f: json.dump(report, f, ensure_ascii=False, indent=2) # 也写到static目录供dashboard直接serve with open(STATIC_DIR / "mofin_health.json", "w") as f: json.dump(report, f, ensure_ascii=False, indent=2) print(f"[SILENT] mofin_health.json written ({len(entities)} entities, {len(pipelines)} pipelines)") if __name__ == "__main__": build_report()