From 20c05b6e7c01c13ec5d0601788f9b263d4ea579a Mon Sep 17 00:00:00 2001 From: xxm Date: Fri, 21 Aug 2026 22:34:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(messenger):=20=E5=85=A8=E9=83=A877?= =?UTF-8?q?=E4=B8=AA=E6=B4=BB=E8=B7=83cron=E8=84=9A=E6=9C=AC=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E7=BB=9F=E4=B8=80=E6=B6=88=E6=81=AF=E9=80=9A=E9=81=93?= =?UTF-8?q?(install=5Fstdio=5Fhook)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile-scripts/accumulation_scanner.py | 7 + .../profile-scripts/agent_spiral_watchdog.py | 7 + deploy/profile-scripts/anomaly_monitor.py | 7 + deploy/profile-scripts/build_panel_hk.py | 7 + deploy/profile-scripts/candidate_filter.py | 7 + .../profile-scripts/capital_flow_collector.py | 433 ++++----- deploy/profile-scripts/clean_watchlist.py | 7 + deploy/profile-scripts/closing_brief.py | 7 + .../collect_evaluation_data.py | 821 +++++++++--------- deploy/profile-scripts/cron_to_xmpp.py | 7 + deploy/profile-scripts/daily_kline_hk.py | 7 + deploy/profile-scripts/daily_kline_sh.py | 7 + deploy/profile-scripts/daily_kline_sz_a.py | 7 + deploy/profile-scripts/daily_kline_sz_b.py | 7 + deploy/profile-scripts/data_governance.py | 193 ++-- deploy/profile-scripts/db_daily_backup.py | 7 + deploy/profile-scripts/deploy_guard.py | 7 + deploy/profile-scripts/divergence_detector.py | 7 + deploy/profile-scripts/factor_engine.py | 7 + deploy/profile-scripts/fix_gateway_port.py | 7 + .../functional_health_check.py | 7 + .../fundamentals_full_refresh.py | 7 + .../profile-scripts/fundamentals_refresh.py | 7 + deploy/profile-scripts/hardcode_scanner.py | 7 + .../profile-scripts/health_monitor_daily.py | 7 + deploy/profile-scripts/hk_connect_list.py | 7 + deploy/profile-scripts/hk_flow_collector.py | 7 + .../hk_fundamentals_collector.py | 7 + .../hk_fundamentals_history_collector.py | 7 + deploy/profile-scripts/hk_scanner.py | 7 + deploy/profile-scripts/hk_sector_collector.py | 7 + .../profile-scripts/intraday_health_check.py | 493 +++++------ deploy/profile-scripts/kanban_xmpp_bridge.py | 7 + deploy/profile-scripts/live_data_collector.py | 7 + .../macro_context_collector.py | 7 + .../profile-scripts/macro_signal_consumer.py | 7 + deploy/profile-scripts/market_insight.py | 7 + deploy/profile-scripts/market_regime.py | 7 + deploy/profile-scripts/market_watch.py | 7 + deploy/profile-scripts/memory_guardian.py | 7 + deploy/profile-scripts/mofin_health.py | 7 + .../profile-scripts/morning_health_check.py | 7 + .../profile-scripts/news_collector_full_b0.py | 7 + .../profile-scripts/news_collector_full_b1.py | 7 + .../profile-scripts/news_collector_full_b2.py | 7 + .../profile-scripts/news_collector_full_b3.py | 7 + .../profile-scripts/news_collector_full_b4.py | 7 + .../profile-scripts/news_collector_full_b5.py | 7 + .../profile-scripts/news_collector_full_b6.py | 7 + .../profile-scripts/news_collector_full_b7.py | 7 + deploy/profile-scripts/opening_brief.py | 7 + deploy/profile-scripts/per_stock_reassess.py | 7 + deploy/profile-scripts/pool_news_collector.py | 7 + deploy/profile-scripts/preflight_verify.py | 7 + .../profile-scripts/premarket_full_review.py | 7 + deploy/profile-scripts/price_monitor.py | 7 + deploy/profile-scripts/promote_candidates.py | 7 + .../profile-scripts/refresh_macro_context.py | 7 + deploy/profile-scripts/refresh_mtf_cache.py | 7 + deploy/profile-scripts/regime_tracker.py | 7 + .../profile-scripts/review_needed_watchdog.py | 7 + .../profile-scripts/sector_enrich_cninfo.py | 7 + .../profile-scripts/sector_index_builder.py | 7 + deploy/profile-scripts/self_repair.py | 7 + deploy/profile-scripts/stale_detector.py | 7 + .../strategy-staleness-check.py | 7 + deploy/profile-scripts/strategy_alert.py | 7 + .../profile-scripts/strategy_effectiveness.py | 7 + deploy/profile-scripts/strategy_executor.py | 7 + .../profile-scripts/strategy_period_rollup.py | 7 + deploy/profile-scripts/strategy_review.py | 593 ++++++------- deploy/profile-scripts/strategy_router.py | 7 + deploy/profile-scripts/system_audit.py | 597 ++++++------- .../profile-scripts/system_hygiene_audit.py | 7 + deploy/profile-scripts/vacuum_state_db.py | 7 + .../verify_reassess_pipeline.py | 7 + deploy/profile-scripts/watchlist_auto_exit.py | 7 + 77 files changed, 2083 insertions(+), 1544 deletions(-) diff --git a/deploy/profile-scripts/accumulation_scanner.py b/deploy/profile-scripts/accumulation_scanner.py index 689f9656..f2bf6def 100644 --- a/deploy/profile-scripts/accumulation_scanner.py +++ b/deploy/profile-scripts/accumulation_scanner.py @@ -18,6 +18,13 @@ from pathlib import Path from datetime import datetime, timedelta from collections import defaultdict +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") UA = "Mozilla/5.0" diff --git a/deploy/profile-scripts/agent_spiral_watchdog.py b/deploy/profile-scripts/agent_spiral_watchdog.py index 89b109d5..7b70639b 100644 --- a/deploy/profile-scripts/agent_spiral_watchdog.py +++ b/deploy/profile-scripts/agent_spiral_watchdog.py @@ -13,6 +13,13 @@ v1 动作:XMPP 告警 + 日志(不自动 kill——kill 需要 hermes 会话 import json, os, sqlite3, glob from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + STATE_FILE = "/home/hmo/MoFin/gateway/logs/spiral_watchdog_state.json" # 2026-08-19 僵尸会话检测:记录每 session 上次 msg 数,停滞不增长 = 僵尸(非螺旋) ZOMBIE_GROWTH = 0.001 # 若本轮 msgs 相对上次增长 < 0.1%(停滞)→ 僵尸跳过 diff --git a/deploy/profile-scripts/anomaly_monitor.py b/deploy/profile-scripts/anomaly_monitor.py index 461786c9..8024f455 100644 --- a/deploy/profile-scripts/anomaly_monitor.py +++ b/deploy/profile-scripts/anomaly_monitor.py @@ -18,6 +18,13 @@ import sqlite3, os, sys from datetime import datetime from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, str(Path(__file__).resolve().parent)) DB = "/home/hmo/MoFin/data/mofin.db" diff --git a/deploy/profile-scripts/build_panel_hk.py b/deploy/profile-scripts/build_panel_hk.py index a14f8abe..85308ca2 100644 --- a/deploy/profile-scripts/build_panel_hk.py +++ b/deploy/profile-scripts/build_panel_hk.py @@ -10,6 +10,13 @@ import sys, sqlite3 import numpy as np import pandas as pd + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") DB = "/home/hmo/MoFin/data/mofin.db" diff --git a/deploy/profile-scripts/candidate_filter.py b/deploy/profile-scripts/candidate_filter.py index f1a22dad..f285f17b 100644 --- a/deploy/profile-scripts/candidate_filter.py +++ b/deploy/profile-scripts/candidate_filter.py @@ -13,6 +13,13 @@ import sys, json, urllib.request, sqlite3, re, time from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") UA = "Mozilla/5.0" diff --git a/deploy/profile-scripts/capital_flow_collector.py b/deploy/profile-scripts/capital_flow_collector.py index 75914edf..0564412c 100644 --- a/deploy/profile-scripts/capital_flow_collector.py +++ b/deploy/profile-scripts/capital_flow_collector.py @@ -1,213 +1,220 @@ -#!/usr/bin/env python3 -"""capital_flow_collector.py — 个股资金流数据采集器 - -每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。 -输出到 capital_flow_cache.json 供 price_monitor 和报告使用。 - -API: push2his.eastmoney.com 个股资金流日线 -""" -import json, os, sys, time, urllib.request -from datetime import datetime -from urllib.request import urlopen, Request -from concurrent.futures import ThreadPoolExecutor, as_completed -from threading import Semaphore -from mofin_db import get_conn, write_capital_flow_cache - -DATA_DIR = "/home/hmo/web-dashboard/data" -CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json" - -UA = "Mozilla/5.0" -# 限速器:最多5个并发,每请求后强制间隔0.3s -RATE_LIMIT = Semaphore(5) -MIN_INTERVAL = 0.3 -_last_req = 0 - -def _rate_limited_request(url, referer="https://data.eastmoney.com/"): - """带速率限制的HTTP GET,用Semaphore控制并发数""" - global _last_req - with RATE_LIMIT: - elapsed = time.time() - _last_req - if elapsed < MIN_INTERVAL: - time.sleep(MIN_INTERVAL - elapsed) - proxy_handler = urllib.request.ProxyHandler({}) - opener = urllib.request.build_opener(proxy_handler) - req = Request(url, headers={"User-Agent": UA, "Referer": referer}) - try: - resp = opener.open(req, timeout=8) - _last_req = time.time() - return json.loads(resp.read().decode("utf-8")) - except Exception: - return None - -# eastmoney secid: 1=上海 0=深圳 -def secid(code): - code = str(code).strip() - if code.startswith(("6", "9")): - return f"1.{code}" - return f"0.{code}" - -def fetch_flow(code, days=5): - """拉取个股近N日资金流(Sina MoneyFlow ssl_qsfx_zjlrqs,最新在前 — - eastmoney 在 246 不可达、ssl_qsfx_lscjfb 数据停在2020年(2026-07-22 实证)""" - code = str(code).strip() - if code.startswith(("6", "9")): - dm = f"sh{code}" - elif len(code) == 5 and code[0] in "01": - return None # 港股 sina 不支持资金流 - else: - dm = f"sz{code}" - url = ("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/" - f"MoneyFlow.ssl_qsfx_zjlrqs?daima={dm}") - data = _rate_limited_request(url, referer="https://finance.sina.com.cn") - if not data or not isinstance(data, list) or not data: - return None - result = [] - for d in data[:days]: # 最新在前,取近N日 - try: - r0n = float(d.get("r0_net", 0) or 0) - r1n = float(d.get("r1_net", 0) or 0) - r2n = float(d.get("r2_net", 0) or 0) - r3n = float(d.get("r3_net", 0) or 0) - result.append({ - "date": d.get("opendate", ""), - "main_net": r0n + r1n, - "super_large": r0n, - "large": r1n, - "medium": r2n, - "small": r3n, - }) - except Exception: - continue - result.reverse() # 转为时间升序(analyze_flow 以 [-1] 为最新日) - return result or None - -def fetch_flow_intraday(code): - """拉取当日分时资金流(用于盘中判断)""" - sid = secid(code) - url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120" - try: - resp = urlopen(url, timeout=5) - data = json.loads(resp.read().decode("utf-8")) - klines = data.get("data", {}).get("klines", []) - if not klines: - return None - latest = klines[-1].split(",") - return { - "main_net": float(latest[1]), - "super_large": float(latest[2]), - "large": float(latest[3]), - } - except: - return None - -def analyze_flow(flow_data): - """分析资金流模式""" - if not flow_data or len(flow_data) < 2: - return {} - - result = {"alerts": [], "pattern": ""} - - # 最近两日对比 - d1 = flow_data[-1] # 最新日 - d2 = flow_data[-2] # 前一日 - - # 超大单信号 - sl1 = d1["super_large"] - sl2 = d2["super_large"] - - # 连续形态判断 - main_trend = sum(d["main_net"] for d in flow_data[-3:]) - sl_trend = sum(d["super_large"] for d in flow_data[-3:]) - - # 1. 主力连续流入 - if main_trend > 50000000 and sl1 > 0 and sl2 > 0: - result["pattern"] = "主力持续流入" - result["alerts"].append("主力连续3日净流入") - - # 2. 超大单突然转向(连续流入→流出 或 流出→流入) - if sl1 * sl2 < 0: # 方向反转 - if sl1 > 0 and sl2 < 0: - result["pattern"] = "超大单由出转入" - result["alerts"].append("超大单转为净买入(暗示消息即将落地)") - elif sl1 < 0 and sl2 > 0: - result["pattern"] = "超大单由入转出" - result["alerts"].append("超大单转为净卖出(利好出货嫌疑)") - - # 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成) - # 4. 单日暴量 - max_sl = max(abs(d["super_large"]) for d in flow_data) - if max_sl == abs(sl1) and abs(sl1) > 100000000: - result["pattern"] = "单日资金暴量" - result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿") - - return result - -def main(): - codes = set() - # 读取持仓+自选(从DB直接读,替代已删除的mo_data) - try: - import sqlite3 - _db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db") - for row in _db.execute("SELECT DISTINCT code FROM holdings WHERE is_active=1").fetchall(): - if row[0]: codes.add(row[0]) - for row in _db.execute("SELECT DISTINCT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略'").fetchall(): - if row[0]: codes.add(row[0]) - _db.close() - except: - pass - - all_flows = {} - - # 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔) - code_list = sorted(codes) - if not code_list: - print("[capital_flow] 无代码需要采集") - return - - def fetch_one(code): - flow = fetch_flow(code, days=5) - if flow: - analysis = analyze_flow(flow) or {} - # 聚合成 build_prompt 需要的字段(此前只有 alerts/pattern,12维资金面恒为0) - net = round(sum(d["main_net"] for d in flow) / 1e4, 1) - main_f = round(sum(d["super_large"] for d in flow) / 1e4, 1) - retail = round(sum(d["medium"] + d["small"] for d in flow) / 1e4, 1) - analysis["net_flow"] = net - analysis["main_force"] = main_f - analysis["retail_flow"] = retail - analysis["trend"] = analysis.get("pattern") or \ - ("主力流入" if main_f > 0 else ("主力流出" if main_f < 0 else "中性")) - return (code, { - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), - "flow": flow, - "analysis": analysis, - }) - return (code, None) - - with ThreadPoolExecutor(max_workers=5) as pool: - futures = {pool.submit(fetch_one, c): c for c in code_list} - for f in as_completed(futures): - code, result = f.result() - if result: - all_flows[code] = result - - # 写缓存 - cache = { - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), - "stocks": all_flows, - } - # 写 DB(替代 capital_flow_cache.json) - conn = get_conn() - write_capital_flow_cache(conn, cache) - conn.close() - print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成") - - # 2026-08-13 资金流突变处理(老莫设计):正面突变→选股分析,负面突变(持仓)→重评+XMPP报告 - try: - from fund_flow_alert import process_flow_alerts - process_flow_alerts(all_flows) - except Exception as e: - print(f"[资金流突变处理异常] {e}", flush=True) - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""capital_flow_collector.py — 个股资金流数据采集器 + +每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。 +输出到 capital_flow_cache.json 供 price_monitor 和报告使用。 + +API: push2his.eastmoney.com 个股资金流日线 +""" +import json, os, sys, time, urllib.request +from datetime import datetime +from urllib.request import urlopen, Request +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Semaphore +from mofin_db import get_conn, write_capital_flow_cache + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +DATA_DIR = "/home/hmo/web-dashboard/data" +CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json" + +UA = "Mozilla/5.0" +# 限速器:最多5个并发,每请求后强制间隔0.3s +RATE_LIMIT = Semaphore(5) +MIN_INTERVAL = 0.3 +_last_req = 0 + +def _rate_limited_request(url, referer="https://data.eastmoney.com/"): + """带速率限制的HTTP GET,用Semaphore控制并发数""" + global _last_req + with RATE_LIMIT: + elapsed = time.time() - _last_req + if elapsed < MIN_INTERVAL: + time.sleep(MIN_INTERVAL - elapsed) + proxy_handler = urllib.request.ProxyHandler({}) + opener = urllib.request.build_opener(proxy_handler) + req = Request(url, headers={"User-Agent": UA, "Referer": referer}) + try: + resp = opener.open(req, timeout=8) + _last_req = time.time() + return json.loads(resp.read().decode("utf-8")) + except Exception: + return None + +# eastmoney secid: 1=上海 0=深圳 +def secid(code): + code = str(code).strip() + if code.startswith(("6", "9")): + return f"1.{code}" + return f"0.{code}" + +def fetch_flow(code, days=5): + """拉取个股近N日资金流(Sina MoneyFlow ssl_qsfx_zjlrqs,最新在前 — + eastmoney 在 246 不可达、ssl_qsfx_lscjfb 数据停在2020年(2026-07-22 实证)""" + code = str(code).strip() + if code.startswith(("6", "9")): + dm = f"sh{code}" + elif len(code) == 5 and code[0] in "01": + return None # 港股 sina 不支持资金流 + else: + dm = f"sz{code}" + url = ("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/" + f"MoneyFlow.ssl_qsfx_zjlrqs?daima={dm}") + data = _rate_limited_request(url, referer="https://finance.sina.com.cn") + if not data or not isinstance(data, list) or not data: + return None + result = [] + for d in data[:days]: # 最新在前,取近N日 + try: + r0n = float(d.get("r0_net", 0) or 0) + r1n = float(d.get("r1_net", 0) or 0) + r2n = float(d.get("r2_net", 0) or 0) + r3n = float(d.get("r3_net", 0) or 0) + result.append({ + "date": d.get("opendate", ""), + "main_net": r0n + r1n, + "super_large": r0n, + "large": r1n, + "medium": r2n, + "small": r3n, + }) + except Exception: + continue + result.reverse() # 转为时间升序(analyze_flow 以 [-1] 为最新日) + return result or None + +def fetch_flow_intraday(code): + """拉取当日分时资金流(用于盘中判断)""" + sid = secid(code) + url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120" + try: + resp = urlopen(url, timeout=5) + data = json.loads(resp.read().decode("utf-8")) + klines = data.get("data", {}).get("klines", []) + if not klines: + return None + latest = klines[-1].split(",") + return { + "main_net": float(latest[1]), + "super_large": float(latest[2]), + "large": float(latest[3]), + } + except: + return None + +def analyze_flow(flow_data): + """分析资金流模式""" + if not flow_data or len(flow_data) < 2: + return {} + + result = {"alerts": [], "pattern": ""} + + # 最近两日对比 + d1 = flow_data[-1] # 最新日 + d2 = flow_data[-2] # 前一日 + + # 超大单信号 + sl1 = d1["super_large"] + sl2 = d2["super_large"] + + # 连续形态判断 + main_trend = sum(d["main_net"] for d in flow_data[-3:]) + sl_trend = sum(d["super_large"] for d in flow_data[-3:]) + + # 1. 主力连续流入 + if main_trend > 50000000 and sl1 > 0 and sl2 > 0: + result["pattern"] = "主力持续流入" + result["alerts"].append("主力连续3日净流入") + + # 2. 超大单突然转向(连续流入→流出 或 流出→流入) + if sl1 * sl2 < 0: # 方向反转 + if sl1 > 0 and sl2 < 0: + result["pattern"] = "超大单由出转入" + result["alerts"].append("超大单转为净买入(暗示消息即将落地)") + elif sl1 < 0 and sl2 > 0: + result["pattern"] = "超大单由入转出" + result["alerts"].append("超大单转为净卖出(利好出货嫌疑)") + + # 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成) + # 4. 单日暴量 + max_sl = max(abs(d["super_large"]) for d in flow_data) + if max_sl == abs(sl1) and abs(sl1) > 100000000: + result["pattern"] = "单日资金暴量" + result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿") + + return result + +def main(): + codes = set() + # 读取持仓+自选(从DB直接读,替代已删除的mo_data) + try: + import sqlite3 + _db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db") + for row in _db.execute("SELECT DISTINCT code FROM holdings WHERE is_active=1").fetchall(): + if row[0]: codes.add(row[0]) + for row in _db.execute("SELECT DISTINCT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略'").fetchall(): + if row[0]: codes.add(row[0]) + _db.close() + except: + pass + + all_flows = {} + + # 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔) + code_list = sorted(codes) + if not code_list: + print("[capital_flow] 无代码需要采集") + return + + def fetch_one(code): + flow = fetch_flow(code, days=5) + if flow: + analysis = analyze_flow(flow) or {} + # 聚合成 build_prompt 需要的字段(此前只有 alerts/pattern,12维资金面恒为0) + net = round(sum(d["main_net"] for d in flow) / 1e4, 1) + main_f = round(sum(d["super_large"] for d in flow) / 1e4, 1) + retail = round(sum(d["medium"] + d["small"] for d in flow) / 1e4, 1) + analysis["net_flow"] = net + analysis["main_force"] = main_f + analysis["retail_flow"] = retail + analysis["trend"] = analysis.get("pattern") or \ + ("主力流入" if main_f > 0 else ("主力流出" if main_f < 0 else "中性")) + return (code, { + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "flow": flow, + "analysis": analysis, + }) + return (code, None) + + with ThreadPoolExecutor(max_workers=5) as pool: + futures = {pool.submit(fetch_one, c): c for c in code_list} + for f in as_completed(futures): + code, result = f.result() + if result: + all_flows[code] = result + + # 写缓存 + cache = { + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "stocks": all_flows, + } + # 写 DB(替代 capital_flow_cache.json) + conn = get_conn() + write_capital_flow_cache(conn, cache) + conn.close() + print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成") + + # 2026-08-13 资金流突变处理(老莫设计):正面突变→选股分析,负面突变(持仓)→重评+XMPP报告 + try: + from fund_flow_alert import process_flow_alerts + process_flow_alerts(all_flows) + except Exception as e: + print(f"[资金流突变处理异常] {e}", flush=True) + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/clean_watchlist.py b/deploy/profile-scripts/clean_watchlist.py index 3635aa91..8f53cd9d 100644 --- a/deploy/profile-scripts/clean_watchlist.py +++ b/deploy/profile-scripts/clean_watchlist.py @@ -13,6 +13,13 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mo_data import read_portfolio, read_decisions, read_watchlist from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + def main(): holding_codes = set() diff --git a/deploy/profile-scripts/closing_brief.py b/deploy/profile-scripts/closing_brief.py index 88ae6320..ff997b15 100644 --- a/deploy/profile-scripts/closing_brief.py +++ b/deploy/profile-scripts/closing_brief.py @@ -8,6 +8,13 @@ import sys import subprocess from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + script_dir = Path(__file__).parent.resolve() target = script_dir / "generate_report.py" diff --git a/deploy/profile-scripts/collect_evaluation_data.py b/deploy/profile-scripts/collect_evaluation_data.py index 293ca5f8..687eed19 100644 --- a/deploy/profile-scripts/collect_evaluation_data.py +++ b/deploy/profile-scripts/collect_evaluation_data.py @@ -1,407 +1,414 @@ -#!/usr/bin/env python3 -"""collect_evaluation_data.py — 六维评估原始数据采集 - -纯数据收集脚本(no_agent),不做任何评估/判断/RR计算。 -输出:data/evaluation_input.json — 供 21:00 LLM cron 使用。 - -采集内容: -D1 宏观环境 — 五大指数(上证/深证/恒生/恒科/A50) -D2 行业表现 — 持仓+自选按行业分组 -D3 技术面(当前) — 今开/今高/今低/昨收/现价/成交量 -D4 基本面 — PE/PB/总市值/52周高/52周低 -D5 消息面 — (此脚本不采集,LLM cron web_search) -D6 资金面 — 成交额/换手率/量比 - -日期:2026-06-18 v1 — 初始版本 -""" - -import json -import urllib.request -import os -import sys -import re -from datetime import datetime -from pathlib import Path - -DATA_DIR = Path(__file__).parent.parent / "data" -PROFILES_PATH = DATA_DIR / "stock_profiles.json" -OUTPUT_PATH = DATA_DIR / "evaluation_input.json" - -UA = "Mozilla/5.0" - - -def load_json(path, default=None): - try: - with open(path, encoding="utf-8") as f: - return json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - return {} if default is None else default - - -def save_json(path, data): - Path(path).parent.mkdir(parents=True, exist_ok=True) - with open(path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - - -def fetch_tencent_data(symbols): - """批量拉行情。DB 优先,腾讯 API fallback""" - if not symbols: - return {} - # DB 优先 - try: - from mofin_db import get_prices_batch_from_db - db = get_prices_batch_from_db(symbols) - if db: - return {code: {"name": "", "price": p, "prev_close": 0, "change_pct": chg or 0, - "high": 0, "low": 0} for code, (p, chg) in db.items()} - except: pass - # Fallback: 腾讯 - code_map = {} - query_symbols = [] - for c in symbols: - sym = f"hk{c}" if len(c) == 5 else f"sh{c}" if c.startswith(("5", "6", "9")) else f"sz{c}" - query_symbols.append(sym) - code_map[sym] = c - url = f"http://qt.gtimg.cn/q={','.join(query_symbols)}" - try: - req = urllib.request.Request(url, headers={"User-Agent": UA}) - resp = urllib.request.urlopen(req, timeout=15) - text = resp.read().decode("gbk") - except Exception as e: - print(f"行情拉取失败: {e}", file=sys.stderr) - return {} - result = {} - for line in text.strip().split("\n"): - line = line.strip() - if not line or "=" not in line: - continue - raw = line.split("=", 1)[1].strip().strip('"').strip(";") - fields = raw.split("~") - if len(fields) < 35: - continue - sym = line.split("=", 1)[0].strip().lstrip("v_") - orig = code_map.get(sym) - if not orig: - continue - # 统一格式(A股和港股字段长度不同) - result[orig] = fields - return result - - -def fetch_indices(): - """拉五大指数""" - index_codes = { - "sh000001": "上证指数", - "sz399001": "深证成指", - "sz399006": "创业板指", - "hkHSI": "恒生指数", - "hkHSTECH": "恒生科技", - } - idx_map = {} - for c, n in index_codes.items(): - sym = c # 已经是完整符号 - idx_map[sym] = n - url = f"http://qt.gtimg.cn/q={','.join(index_codes.keys())}" - try: - req = urllib.request.Request(url, headers={"User-Agent": UA}) - resp = urllib.request.urlopen(req, timeout=10) - text = resp.read().decode("gbk") - except Exception as e: - print(f"指数拉取失败: {e}", file=sys.stderr) - return {} - result = {} - for line in text.strip().split("\n"): - line = line.strip() - if not line or "=" not in line: - continue - raw = line.split("=", 1)[1].strip().strip('"').strip(";") - fields = raw.split("~") - if len(fields) < 33: - continue - sym = line.split("=", 1)[0].strip().lstrip("v_") - name = idx_map.get(sym, sym) - result[name] = { - "price": safe_float(fields[3]), - "prev_close": safe_float(fields[4]), - "change_pct": safe_float(fields[32]), - "high": safe_float(fields[33]), - "low": safe_float(fields[34]), - "timestamp": fields[30] if len(fields) > 30 else "", - } - return result - - -def safe_float(v): - try: - return float(v) if v else None - except (ValueError, TypeError): - return None - - -def parse_stock_data(code, fields, is_hk=False): - """从腾讯 API 字段解析为结构化数据""" - # fields可能是dict(已解析)或list(原始split) - if isinstance(fields, dict): - data = { - "code": code, - "name": fields.get("name", code), - "price": safe_float(fields.get("price", 0)), - "prev_close": safe_float(fields.get("prev_close", 0)), - "open": safe_float(fields.get("open", 0)) if not is_hk else None, - "change_pct": safe_float(fields.get("change_pct", 0)), - "high": safe_float(fields.get("high", 0)), - "low": safe_float(fields.get("low", 0)), - "volume": safe_float(fields.get("volume", 0)), - } - else: - data = { - "code": code, - "name": fields[1] if len(fields) > 1 else code, - "price": safe_float(fields[3]), - "prev_close": safe_float(fields[4]), - "open": safe_float(fields[5]) if not is_hk else None, - "change_pct": safe_float(fields[32]), - "high": safe_float(fields[33]), - "low": safe_float(fields[34]), - "volume": safe_float(fields[6]), - } - # A股特有字段 (index 35+) - if not is_hk: - if isinstance(fields, dict): - data["turnover_rate"] = safe_float(fields.get("turnover_rate", 0)) - data["amplitude"] = safe_float(fields.get("amplitude", 0)) - data["pe"] = safe_float(fields.get("pe", 0)) - data["market_cap"] = safe_float(fields.get("market_cap", 0)) - else: - data["turnover_rate"] = safe_float(fields[38]) if len(fields) > 38 else 0 - data["amplitude"] = safe_float(fields[39]) if len(fields) > 39 else 0 - data["pe"] = safe_float(fields[39]) if len(fields) > 39 else 0 - data["market_cap"] = safe_float(fields[45]) if len(fields) > 45 else 0 - # 港股特有 - if is_hk: - data["market"] = "HK" - if isinstance(fields, dict): - data["high_52w"] = safe_float(fields.get("high_52w", 0)) - data["low_52w"] = safe_float(fields.get("low_52w", 0)) - data["amplitude"] = safe_float(fields.get("amplitude", 0)) - else: - data["high_52w"] = safe_float(fields[48]) if len(fields) > 48 else 0 - data["low_52w"] = safe_float(fields[49]) if len(fields) > 49 else 0 - data["amplitude"] = safe_float(fields[43]) if len(fields) > 43 else 0 - # 港股特有字段 - if is_hk: - if isinstance(fields, dict): - data["pe"] = safe_float(fields.get("pe", 0)) - data["total_market_cap"] = safe_float(fields.get("total_market_cap", 0)) - data["high_52w"] = safe_float(fields.get("high_52w", 0)) - data["low_52w"] = safe_float(fields.get("low_52w", 0)) - else: - if len(fields) > 70: - data["pe"] = safe_float(fields[71]) - data["total_market_cap"] = safe_float(fields[69]) - data["high_52w"] = safe_float(fields[48]) - data["low_52w"] = safe_float(fields[49]) - return data - - -def get_sector_mapping(profiles, decisions): - """ - 从 stock_profiles.json 和 decisions.json 建立 - {code: {name, sector, business, market, type}} 映射 - """ - mapping = {} - # 先读 stock_profiles - profile_list = profiles.get("profiles", []) if isinstance(profiles, dict) else profiles - if isinstance(profile_list, list): - for p in profile_list: - code = p.get("code", "") - if code: - mapping[code] = { - "name": p.get("name", ""), - "sector": p.get("sector", ""), - "business": p.get("business", ""), - "market": p.get("market", ""), - "type": p.get("type", ""), - } - # 再补全 decisions.json 中的信息 - for d in decisions.get("decisions", []): - code = d.get("code", "") - if code and code not in mapping: - trig = d.get("trigger", {}) - mapping[code] = { - "name": d.get("name", code), - "sector": trig.get("sector_name", d.get("sector_name", "")), - "business": "", - "market": "港股" if len(code) == 5 else "A股", - "type": d.get("type", "持仓策略"), - } - return mapping - - -def get_portfolio_info(portfolio): - """建立 {code: {cost, shares, position_pct}} 映射""" - result = {} - for h in portfolio.get("holdings", []): - code = h.get("code", "") - result[code] = { - "cost": h.get("cost", 0), - "shares": h.get("shares", 0), - "position_pct": h.get("position_pct", 0), - } - return result - - -def get_decisions_info(decisions): - """提取 decisions.json 中的策略参数""" - return decisions.get("decisions", []) - - -def run(): - # 加载数据 - from mo_data import read_decisions, read_portfolio - decisions = read_decisions() - portfolio = read_portfolio() - profiles = load_json(PROFILES_PATH, {"profiles": []}) - - # 获取行业映射 - sector_mapping = get_sector_mapping(profiles, decisions) - - # 获取持仓信息 - portfolio_info = get_portfolio_info(portfolio) - - # 收集所有代码 - all_codes = set() - for d in decisions.get("decisions", []): - code = d.get("code", "") - if code: - all_codes.add(code) - for h in portfolio.get("holdings", []): - code = h.get("code", "") - if code: - all_codes.add(code) - - # 区分 A/H 股 - a_codes = [c for c in all_codes if len(c) != 5] - hk_codes = [c for c in all_codes if len(c) == 5] - - # 拉行情 - a_prices = fetch_tencent_data(a_codes) if a_codes else {} - hk_prices = fetch_tencent_data(hk_codes) if hk_codes else {} - - # 拉指数 - index_data = fetch_indices() - - # 解析个股数据 - stock_data = {} - for code in a_codes: - if code in a_prices: - stock_data[code] = parse_stock_data(code, a_prices[code], is_hk=False) - for code in hk_codes: - if code in hk_prices: - stock_data[code] = parse_stock_data(code, hk_prices[code], is_hk=True) - - # 组装输出 - stocks = [] - all_codes_sorted = sorted(all_codes) - - for code in all_codes_sorted: - raw = stock_data.get(code, {}) - sector_info = sector_mapping.get(code, {}) - port = portfolio_info.get(code, {}) - strategy = None - for d in decisions.get("decisions", []): - if d.get("code") == code: - trig = d.get("trigger", {}) - strategy = { - "action": trig.get("action", d.get("action", "")), - "entry_zone": trig.get("entry_zone", ""), - "stop_loss": trig.get("stop_loss", d.get("stop_loss", "")), - "take_profit": trig.get("take_profit", d.get("take_profit", "")), - "type": d.get("type", "持仓策略"), - "tech_snapshot": trig.get("tech_snapshot", d.get("tech_snapshot", "")), - } - break - - stock_entry = { - "code": code, - "name": raw.get("name", sector_info.get("name", code)), - "market": "港股" if len(code) == 5 else "A股", - "type": sector_info.get("type", "持仓策略"), - "sector": sector_info.get("sector", ""), - "business": sector_info.get("business", ""), - # 当天行情 - "price": raw.get("price"), - "prev_close": raw.get("prev_close"), - "open": raw.get("open"), - "high": raw.get("high"), - "low": raw.get("low"), - "change_pct": raw.get("change_pct"), - "volume": raw.get("volume"), - # 基本面 - "pe": raw.get("pe"), - "total_market_cap": raw.get("total_market_cap"), - "high_52w": raw.get("high_52w"), - "low_52w": raw.get("low_52w"), - "turnover_rate": raw.get("turnover_rate"), - "amplitude": raw.get("amplitude"), - # 持仓 - "cost": port.get("cost", 0), - "shares": port.get("shares", 0), - "position_pct": port.get("position_pct", 0), - # 现策略 - "strategy": strategy, - } - # 浮亏% - cost = port.get("cost", 0) - price = raw.get("price", 0) - if cost > 0 and price > 0: - stock_entry["pnl_pct"] = round((price - cost) / cost * 100, 2) - else: - stock_entry["pnl_pct"] = None - - stocks.append(stock_entry) - - # 按行业分组统计 - sector_groups = {} - for s in stocks: - sector = s.get("sector", "未分类") - if sector not in sector_groups: - sector_groups[sector] = [] - sector_groups[sector].append({ - "code": s["code"], - "name": s["name"], - "change_pct": s["change_pct"], - "pnl_pct": s["pnl_pct"], - "type": s["type"], - }) - - # 汇总 - total = len(stocks) - up_count = sum(1 for s in stocks if s["change_pct"] is not None and s["change_pct"] > 0) - down_count = sum(1 for s in stocks if s["change_pct"] is not None and s["change_pct"] < 0) - deep_loss = sum(1 for s in stocks if s["pnl_pct"] is not None and s["pnl_pct"] < -20) - - output = { - "collected_at": datetime.now().isoformat(), - "total_stocks": total, - "summary": { - "up_count": up_count, - "down_count": down_count, - "deep_loss_count": deep_loss, - "holdings_count": len(portfolio_info), - "watchlist_count": total - len(portfolio_info), - }, - "index_data": index_data, - "sector_groups": sector_groups, - "stocks": stocks, - } - - save_json(OUTPUT_PATH, output) - print(f"数据收集完成: {total}只股票, {len(index_data)}个指数, {len(sector_groups)}个行业分组") - print(f" 上涨{up_count} 下跌{down_count} 深套{deep_loss}") - print(f" 输出: {OUTPUT_PATH}") - - -if __name__ == "__main__": - run() +#!/usr/bin/env python3 +"""collect_evaluation_data.py — 六维评估原始数据采集 + +纯数据收集脚本(no_agent),不做任何评估/判断/RR计算。 +输出:data/evaluation_input.json — 供 21:00 LLM cron 使用。 + +采集内容: +D1 宏观环境 — 五大指数(上证/深证/恒生/恒科/A50) +D2 行业表现 — 持仓+自选按行业分组 +D3 技术面(当前) — 今开/今高/今低/昨收/现价/成交量 +D4 基本面 — PE/PB/总市值/52周高/52周低 +D5 消息面 — (此脚本不采集,LLM cron web_search) +D6 资金面 — 成交额/换手率/量比 + +日期:2026-06-18 v1 — 初始版本 +""" + +import json +import urllib.request +import os +import sys +import re +from datetime import datetime +from pathlib import Path + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +DATA_DIR = Path(__file__).parent.parent / "data" +PROFILES_PATH = DATA_DIR / "stock_profiles.json" +OUTPUT_PATH = DATA_DIR / "evaluation_input.json" + +UA = "Mozilla/5.0" + + +def load_json(path, default=None): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {} if default is None else default + + +def save_json(path, data): + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def fetch_tencent_data(symbols): + """批量拉行情。DB 优先,腾讯 API fallback""" + if not symbols: + return {} + # DB 优先 + try: + from mofin_db import get_prices_batch_from_db + db = get_prices_batch_from_db(symbols) + if db: + return {code: {"name": "", "price": p, "prev_close": 0, "change_pct": chg or 0, + "high": 0, "low": 0} for code, (p, chg) in db.items()} + except: pass + # Fallback: 腾讯 + code_map = {} + query_symbols = [] + for c in symbols: + sym = f"hk{c}" if len(c) == 5 else f"sh{c}" if c.startswith(("5", "6", "9")) else f"sz{c}" + query_symbols.append(sym) + code_map[sym] = c + url = f"http://qt.gtimg.cn/q={','.join(query_symbols)}" + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + resp = urllib.request.urlopen(req, timeout=15) + text = resp.read().decode("gbk") + except Exception as e: + print(f"行情拉取失败: {e}", file=sys.stderr) + return {} + result = {} + for line in text.strip().split("\n"): + line = line.strip() + if not line or "=" not in line: + continue + raw = line.split("=", 1)[1].strip().strip('"').strip(";") + fields = raw.split("~") + if len(fields) < 35: + continue + sym = line.split("=", 1)[0].strip().lstrip("v_") + orig = code_map.get(sym) + if not orig: + continue + # 统一格式(A股和港股字段长度不同) + result[orig] = fields + return result + + +def fetch_indices(): + """拉五大指数""" + index_codes = { + "sh000001": "上证指数", + "sz399001": "深证成指", + "sz399006": "创业板指", + "hkHSI": "恒生指数", + "hkHSTECH": "恒生科技", + } + idx_map = {} + for c, n in index_codes.items(): + sym = c # 已经是完整符号 + idx_map[sym] = n + url = f"http://qt.gtimg.cn/q={','.join(index_codes.keys())}" + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + resp = urllib.request.urlopen(req, timeout=10) + text = resp.read().decode("gbk") + except Exception as e: + print(f"指数拉取失败: {e}", file=sys.stderr) + return {} + result = {} + for line in text.strip().split("\n"): + line = line.strip() + if not line or "=" not in line: + continue + raw = line.split("=", 1)[1].strip().strip('"').strip(";") + fields = raw.split("~") + if len(fields) < 33: + continue + sym = line.split("=", 1)[0].strip().lstrip("v_") + name = idx_map.get(sym, sym) + result[name] = { + "price": safe_float(fields[3]), + "prev_close": safe_float(fields[4]), + "change_pct": safe_float(fields[32]), + "high": safe_float(fields[33]), + "low": safe_float(fields[34]), + "timestamp": fields[30] if len(fields) > 30 else "", + } + return result + + +def safe_float(v): + try: + return float(v) if v else None + except (ValueError, TypeError): + return None + + +def parse_stock_data(code, fields, is_hk=False): + """从腾讯 API 字段解析为结构化数据""" + # fields可能是dict(已解析)或list(原始split) + if isinstance(fields, dict): + data = { + "code": code, + "name": fields.get("name", code), + "price": safe_float(fields.get("price", 0)), + "prev_close": safe_float(fields.get("prev_close", 0)), + "open": safe_float(fields.get("open", 0)) if not is_hk else None, + "change_pct": safe_float(fields.get("change_pct", 0)), + "high": safe_float(fields.get("high", 0)), + "low": safe_float(fields.get("low", 0)), + "volume": safe_float(fields.get("volume", 0)), + } + else: + data = { + "code": code, + "name": fields[1] if len(fields) > 1 else code, + "price": safe_float(fields[3]), + "prev_close": safe_float(fields[4]), + "open": safe_float(fields[5]) if not is_hk else None, + "change_pct": safe_float(fields[32]), + "high": safe_float(fields[33]), + "low": safe_float(fields[34]), + "volume": safe_float(fields[6]), + } + # A股特有字段 (index 35+) + if not is_hk: + if isinstance(fields, dict): + data["turnover_rate"] = safe_float(fields.get("turnover_rate", 0)) + data["amplitude"] = safe_float(fields.get("amplitude", 0)) + data["pe"] = safe_float(fields.get("pe", 0)) + data["market_cap"] = safe_float(fields.get("market_cap", 0)) + else: + data["turnover_rate"] = safe_float(fields[38]) if len(fields) > 38 else 0 + data["amplitude"] = safe_float(fields[39]) if len(fields) > 39 else 0 + data["pe"] = safe_float(fields[39]) if len(fields) > 39 else 0 + data["market_cap"] = safe_float(fields[45]) if len(fields) > 45 else 0 + # 港股特有 + if is_hk: + data["market"] = "HK" + if isinstance(fields, dict): + data["high_52w"] = safe_float(fields.get("high_52w", 0)) + data["low_52w"] = safe_float(fields.get("low_52w", 0)) + data["amplitude"] = safe_float(fields.get("amplitude", 0)) + else: + data["high_52w"] = safe_float(fields[48]) if len(fields) > 48 else 0 + data["low_52w"] = safe_float(fields[49]) if len(fields) > 49 else 0 + data["amplitude"] = safe_float(fields[43]) if len(fields) > 43 else 0 + # 港股特有字段 + if is_hk: + if isinstance(fields, dict): + data["pe"] = safe_float(fields.get("pe", 0)) + data["total_market_cap"] = safe_float(fields.get("total_market_cap", 0)) + data["high_52w"] = safe_float(fields.get("high_52w", 0)) + data["low_52w"] = safe_float(fields.get("low_52w", 0)) + else: + if len(fields) > 70: + data["pe"] = safe_float(fields[71]) + data["total_market_cap"] = safe_float(fields[69]) + data["high_52w"] = safe_float(fields[48]) + data["low_52w"] = safe_float(fields[49]) + return data + + +def get_sector_mapping(profiles, decisions): + """ + 从 stock_profiles.json 和 decisions.json 建立 + {code: {name, sector, business, market, type}} 映射 + """ + mapping = {} + # 先读 stock_profiles + profile_list = profiles.get("profiles", []) if isinstance(profiles, dict) else profiles + if isinstance(profile_list, list): + for p in profile_list: + code = p.get("code", "") + if code: + mapping[code] = { + "name": p.get("name", ""), + "sector": p.get("sector", ""), + "business": p.get("business", ""), + "market": p.get("market", ""), + "type": p.get("type", ""), + } + # 再补全 decisions.json 中的信息 + for d in decisions.get("decisions", []): + code = d.get("code", "") + if code and code not in mapping: + trig = d.get("trigger", {}) + mapping[code] = { + "name": d.get("name", code), + "sector": trig.get("sector_name", d.get("sector_name", "")), + "business": "", + "market": "港股" if len(code) == 5 else "A股", + "type": d.get("type", "持仓策略"), + } + return mapping + + +def get_portfolio_info(portfolio): + """建立 {code: {cost, shares, position_pct}} 映射""" + result = {} + for h in portfolio.get("holdings", []): + code = h.get("code", "") + result[code] = { + "cost": h.get("cost", 0), + "shares": h.get("shares", 0), + "position_pct": h.get("position_pct", 0), + } + return result + + +def get_decisions_info(decisions): + """提取 decisions.json 中的策略参数""" + return decisions.get("decisions", []) + + +def run(): + # 加载数据 + from mo_data import read_decisions, read_portfolio + decisions = read_decisions() + portfolio = read_portfolio() + profiles = load_json(PROFILES_PATH, {"profiles": []}) + + # 获取行业映射 + sector_mapping = get_sector_mapping(profiles, decisions) + + # 获取持仓信息 + portfolio_info = get_portfolio_info(portfolio) + + # 收集所有代码 + all_codes = set() + for d in decisions.get("decisions", []): + code = d.get("code", "") + if code: + all_codes.add(code) + for h in portfolio.get("holdings", []): + code = h.get("code", "") + if code: + all_codes.add(code) + + # 区分 A/H 股 + a_codes = [c for c in all_codes if len(c) != 5] + hk_codes = [c for c in all_codes if len(c) == 5] + + # 拉行情 + a_prices = fetch_tencent_data(a_codes) if a_codes else {} + hk_prices = fetch_tencent_data(hk_codes) if hk_codes else {} + + # 拉指数 + index_data = fetch_indices() + + # 解析个股数据 + stock_data = {} + for code in a_codes: + if code in a_prices: + stock_data[code] = parse_stock_data(code, a_prices[code], is_hk=False) + for code in hk_codes: + if code in hk_prices: + stock_data[code] = parse_stock_data(code, hk_prices[code], is_hk=True) + + # 组装输出 + stocks = [] + all_codes_sorted = sorted(all_codes) + + for code in all_codes_sorted: + raw = stock_data.get(code, {}) + sector_info = sector_mapping.get(code, {}) + port = portfolio_info.get(code, {}) + strategy = None + for d in decisions.get("decisions", []): + if d.get("code") == code: + trig = d.get("trigger", {}) + strategy = { + "action": trig.get("action", d.get("action", "")), + "entry_zone": trig.get("entry_zone", ""), + "stop_loss": trig.get("stop_loss", d.get("stop_loss", "")), + "take_profit": trig.get("take_profit", d.get("take_profit", "")), + "type": d.get("type", "持仓策略"), + "tech_snapshot": trig.get("tech_snapshot", d.get("tech_snapshot", "")), + } + break + + stock_entry = { + "code": code, + "name": raw.get("name", sector_info.get("name", code)), + "market": "港股" if len(code) == 5 else "A股", + "type": sector_info.get("type", "持仓策略"), + "sector": sector_info.get("sector", ""), + "business": sector_info.get("business", ""), + # 当天行情 + "price": raw.get("price"), + "prev_close": raw.get("prev_close"), + "open": raw.get("open"), + "high": raw.get("high"), + "low": raw.get("low"), + "change_pct": raw.get("change_pct"), + "volume": raw.get("volume"), + # 基本面 + "pe": raw.get("pe"), + "total_market_cap": raw.get("total_market_cap"), + "high_52w": raw.get("high_52w"), + "low_52w": raw.get("low_52w"), + "turnover_rate": raw.get("turnover_rate"), + "amplitude": raw.get("amplitude"), + # 持仓 + "cost": port.get("cost", 0), + "shares": port.get("shares", 0), + "position_pct": port.get("position_pct", 0), + # 现策略 + "strategy": strategy, + } + # 浮亏% + cost = port.get("cost", 0) + price = raw.get("price", 0) + if cost > 0 and price > 0: + stock_entry["pnl_pct"] = round((price - cost) / cost * 100, 2) + else: + stock_entry["pnl_pct"] = None + + stocks.append(stock_entry) + + # 按行业分组统计 + sector_groups = {} + for s in stocks: + sector = s.get("sector", "未分类") + if sector not in sector_groups: + sector_groups[sector] = [] + sector_groups[sector].append({ + "code": s["code"], + "name": s["name"], + "change_pct": s["change_pct"], + "pnl_pct": s["pnl_pct"], + "type": s["type"], + }) + + # 汇总 + total = len(stocks) + up_count = sum(1 for s in stocks if s["change_pct"] is not None and s["change_pct"] > 0) + down_count = sum(1 for s in stocks if s["change_pct"] is not None and s["change_pct"] < 0) + deep_loss = sum(1 for s in stocks if s["pnl_pct"] is not None and s["pnl_pct"] < -20) + + output = { + "collected_at": datetime.now().isoformat(), + "total_stocks": total, + "summary": { + "up_count": up_count, + "down_count": down_count, + "deep_loss_count": deep_loss, + "holdings_count": len(portfolio_info), + "watchlist_count": total - len(portfolio_info), + }, + "index_data": index_data, + "sector_groups": sector_groups, + "stocks": stocks, + } + + save_json(OUTPUT_PATH, output) + print(f"数据收集完成: {total}只股票, {len(index_data)}个指数, {len(sector_groups)}个行业分组") + print(f" 上涨{up_count} 下跌{down_count} 深套{deep_loss}") + print(f" 输出: {OUTPUT_PATH}") + + +if __name__ == "__main__": + run() diff --git a/deploy/profile-scripts/cron_to_xmpp.py b/deploy/profile-scripts/cron_to_xmpp.py index 3fa286f0..eb6e0654 100644 --- a/deploy/profile-scripts/cron_to_xmpp.py +++ b/deploy/profile-scripts/cron_to_xmpp.py @@ -15,6 +15,13 @@ import sys from datetime import datetime from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # 使用绝对路径,不受 profile 环境变量影响 REAL_HOME = Path("/home/hmo") diff --git a/deploy/profile-scripts/daily_kline_hk.py b/deploy/profile-scripts/daily_kline_hk.py index 71069946..5d9f0f7a 100644 --- a/deploy/profile-scripts/daily_kline_hk.py +++ b/deploy/profile-scripts/daily_kline_hk.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """daily_kline_hk.py — 港股通日K采集(包装入口,2026-08-14 港股接入)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["daily_kline_collector.py", "hk"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/daily_kline_collector.py", run_name="__main__") diff --git a/deploy/profile-scripts/daily_kline_sh.py b/deploy/profile-scripts/daily_kline_sh.py index 7e1fb119..e8dd76b7 100644 --- a/deploy/profile-scripts/daily_kline_sh.py +++ b/deploy/profile-scripts/daily_kline_sh.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """daily_kline_sh.py — 全市场日K采集(上海,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["daily_kline_collector.py", "--prefix", "sh"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/daily_kline_collector.py", run_name="__main__") diff --git a/deploy/profile-scripts/daily_kline_sz_a.py b/deploy/profile-scripts/daily_kline_sz_a.py index 752760a6..e29b7639 100644 --- a/deploy/profile-scripts/daily_kline_sz_a.py +++ b/deploy/profile-scripts/daily_kline_sz_a.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """daily_kline_sz_a.py — 全市场日K采集(深圳前半批(0开头),包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["daily_kline_collector.py", "--prefix", "sz_a"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/daily_kline_collector.py", run_name="__main__") diff --git a/deploy/profile-scripts/daily_kline_sz_b.py b/deploy/profile-scripts/daily_kline_sz_b.py index fba14af1..2ca17ef5 100644 --- a/deploy/profile-scripts/daily_kline_sz_b.py +++ b/deploy/profile-scripts/daily_kline_sz_b.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """daily_kline_sz_b.py — 全市场日K采集(深圳后半批(3开头),包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["daily_kline_collector.py", "--prefix", "sz_b"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/daily_kline_collector.py", run_name="__main__") diff --git a/deploy/profile-scripts/data_governance.py b/deploy/profile-scripts/data_governance.py index 42041628..bf10f449 100644 --- a/deploy/profile-scripts/data_governance.py +++ b/deploy/profile-scripts/data_governance.py @@ -1,93 +1,100 @@ -#!/usr/bin/env python3 -"""data_governance.py — MoFin 数据治理 (no_agent) - -1. holding_strategies 去重归档 -2. 检查缺失策略的持仓 -3. 报告数据健康状况 -""" - -import json, sqlite3 -from pathlib import Path -from datetime import datetime -from mo_data import read_portfolio, read_decisions, read_watchlist - -BASE = Path("/home/hmo/MoFin") -DATA = BASE / "data" -DB_PATH = DATA / "mofin.db" - - -def clean_holding_strategies(conn): - """归档旧策略,只保留每只股票最新一条""" - codes = conn.execute( - "SELECT code, COUNT(*) as cnt, MAX(created_at) as latest " - "FROM holding_strategies GROUP BY code HAVING cnt > 1" - ).fetchall() - - total_archived = 0 - for code, cnt, latest in codes: - # 标记除了最新一条以外的所有记录为已归档 - conn.execute( - "UPDATE holding_strategies SET superseded_at=? " - "WHERE code=? AND created_at 0 and price > 0: - loss = (price - cost) / cost * 100 - if loss < -25: - deep.append((d.get("name",""), d["code"], loss, d.get("stop_loss",0))) - if deep: - print(f"\n🔴 {len(deep)}只深套(>-25%):") - for name, code, loss, sl in deep: - print(f" {name}({code}): {loss:.0f}% 止损={sl}") - else: - print("\n✅ 无深套持仓") - - conn.close() - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""data_governance.py — MoFin 数据治理 (no_agent) + +1. holding_strategies 去重归档 +2. 检查缺失策略的持仓 +3. 报告数据健康状况 +""" + +import json, sqlite3 +from pathlib import Path +from datetime import datetime +from mo_data import read_portfolio, read_decisions, read_watchlist + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +BASE = Path("/home/hmo/MoFin") +DATA = BASE / "data" +DB_PATH = DATA / "mofin.db" + + +def clean_holding_strategies(conn): + """归档旧策略,只保留每只股票最新一条""" + codes = conn.execute( + "SELECT code, COUNT(*) as cnt, MAX(created_at) as latest " + "FROM holding_strategies GROUP BY code HAVING cnt > 1" + ).fetchall() + + total_archived = 0 + for code, cnt, latest in codes: + # 标记除了最新一条以外的所有记录为已归档 + conn.execute( + "UPDATE holding_strategies SET superseded_at=? " + "WHERE code=? AND created_at 0 and price > 0: + loss = (price - cost) / cost * 100 + if loss < -25: + deep.append((d.get("name",""), d["code"], loss, d.get("stop_loss",0))) + if deep: + print(f"\n🔴 {len(deep)}只深套(>-25%):") + for name, code, loss, sl in deep: + print(f" {name}({code}): {loss:.0f}% 止损={sl}") + else: + print("\n✅ 无深套持仓") + + conn.close() + + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/db_daily_backup.py b/deploy/profile-scripts/db_daily_backup.py index 2abde9b1..a1ab7807 100644 --- a/deploy/profile-scripts/db_daily_backup.py +++ b/deploy/profile-scripts/db_daily_backup.py @@ -12,6 +12,13 @@ import sqlite3, os, glob from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + SRC = "/home/hmo/MoFin/data/mofin.db" DST_DIR = "/home/hmo/MoFin/data/backups" KEEP = 14 diff --git a/deploy/profile-scripts/deploy_guard.py b/deploy/profile-scripts/deploy_guard.py index feec7353..e11c5782 100644 --- a/deploy/profile-scripts/deploy_guard.py +++ b/deploy/profile-scripts/deploy_guard.py @@ -17,6 +17,13 @@ import json, os, subprocess, sys, glob from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + REPO = "/home/hmo/MoFin" CANONICAL = f"{REPO}/deploy/profile-scripts" PROFILE_DIRS = [ diff --git a/deploy/profile-scripts/divergence_detector.py b/deploy/profile-scripts/divergence_detector.py index b146aa53..6408531b 100644 --- a/deploy/profile-scripts/divergence_detector.py +++ b/deploy/profile-scripts/divergence_detector.py @@ -17,6 +17,13 @@ divergence_detector.py — 跨市场背离监测器(no_agent) import sys, json, re, datetime, os, requests from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + BASE = Path("/home/hmo/MoFin") DATA = BASE / "data" DB_PATH = DATA / "mofin.db" diff --git a/deploy/profile-scripts/factor_engine.py b/deploy/profile-scripts/factor_engine.py index 4ed454ba..9dc2181d 100644 --- a/deploy/profile-scripts/factor_engine.py +++ b/deploy/profile-scripts/factor_engine.py @@ -22,6 +22,13 @@ from datetime import datetime, timedelta sys.path.insert(0, "/home/hmo/MoFin") from backtest_framework import (calc_ma, calc_rsi, calc_macd, calc_atr, + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass calc_trend_strength, calc_obv, calc_roc) DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") diff --git a/deploy/profile-scripts/fix_gateway_port.py b/deploy/profile-scripts/fix_gateway_port.py index 89e2dcd9..324139bc 100644 --- a/deploy/profile-scripts/fix_gateway_port.py +++ b/deploy/profile-scripts/fix_gateway_port.py @@ -8,6 +8,13 @@ v3: 2026-08-10 修复自愈反噬死循环(8/3起gateway每5分钟被重启→ import subprocess, sys, time, socket, json, urllib.request, os from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + GATEWAY_PORT = 8643 BOT_PORT = 5805 BOT_SCRIPT = "/home/hmo/xmpp_zhiwei_bot.py" diff --git a/deploy/profile-scripts/functional_health_check.py b/deploy/profile-scripts/functional_health_check.py index 9e491141..d9456e22 100644 --- a/deploy/profile-scripts/functional_health_check.py +++ b/deploy/profile-scripts/functional_health_check.py @@ -10,6 +10,13 @@ import os, sys, json, sqlite3, subprocess from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, '/home/hmo/MoFin') DB = '/home/hmo/MoFin/data/mofin.db' diff --git a/deploy/profile-scripts/fundamentals_full_refresh.py b/deploy/profile-scripts/fundamentals_full_refresh.py index d0595f0c..0455a96a 100644 --- a/deploy/profile-scripts/fundamentals_full_refresh.py +++ b/deploy/profile-scripts/fundamentals_full_refresh.py @@ -16,6 +16,13 @@ import sys, os, re, time, sqlite3, fcntl, urllib.request from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" BATCH = 100 # 腾讯推荐上限 100/批 diff --git a/deploy/profile-scripts/fundamentals_refresh.py b/deploy/profile-scripts/fundamentals_refresh.py index 89d19a8d..824b70e0 100644 --- a/deploy/profile-scripts/fundamentals_refresh.py +++ b/deploy/profile-scripts/fundamentals_refresh.py @@ -5,6 +5,13 @@ EPS(TTM) = 现价/PE。限速0.15s/只。 """ import sqlite3, subprocess, time +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" def prefix(code): diff --git a/deploy/profile-scripts/hardcode_scanner.py b/deploy/profile-scripts/hardcode_scanner.py index 65863d14..c4830cd0 100644 --- a/deploy/profile-scripts/hardcode_scanner.py +++ b/deploy/profile-scripts/hardcode_scanner.py @@ -14,6 +14,13 @@ hardcode_scanner.py — 自成长扫描器 """ import re, ast, json, os, sys +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + SCAN_DIRS = [ "/home/hmo/.hermes/profiles/position-analyst/scripts", "/home/hmo/MoFin", diff --git a/deploy/profile-scripts/health_monitor_daily.py b/deploy/profile-scripts/health_monitor_daily.py index 7c736fa4..7ab39f76 100644 --- a/deploy/profile-scripts/health_monitor_daily.py +++ b/deploy/profile-scripts/health_monitor_daily.py @@ -14,6 +14,13 @@ import sys, os, sqlite3, fcntl from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, "/home/hmo/MoFin") sys.path.insert(0, "/home/hmo/MoFin/evolution") sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") diff --git a/deploy/profile-scripts/hk_connect_list.py b/deploy/profile-scripts/hk_connect_list.py index 9a3fc373..4c18a03e 100644 --- a/deploy/profile-scripts/hk_connect_list.py +++ b/deploy/profile-scripts/hk_connect_list.py @@ -43,6 +43,13 @@ import urllib.request from datetime import date, datetime from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # ── 配置 ──────────────────────────────────────────────── DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") # 与同目录脚本一致(daily_kline_collector/market_data) API_HOST = "push2delay.eastmoney.com" # 延时镜像;push2 主 host 对 246 被封 diff --git a/deploy/profile-scripts/hk_flow_collector.py b/deploy/profile-scripts/hk_flow_collector.py index 5c6c8faf..fc8bf480 100644 --- a/deploy/profile-scripts/hk_flow_collector.py +++ b/deploy/profile-scripts/hk_flow_collector.py @@ -15,6 +15,13 @@ import sys import time import urllib.request +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = "/home/hmo/MoFin/data/mofin.db" UA = "Mozilla/5.0" SLEEP = 0.3 diff --git a/deploy/profile-scripts/hk_fundamentals_collector.py b/deploy/profile-scripts/hk_fundamentals_collector.py index 24fd87ef..9868b7c8 100644 --- a/deploy/profile-scripts/hk_fundamentals_collector.py +++ b/deploy/profile-scripts/hk_fundamentals_collector.py @@ -14,6 +14,13 @@ import sys import time import urllib.request +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = "/home/hmo/MoFin/data/mofin.db" UA = "Mozilla/5.0" BATCH = 100 diff --git a/deploy/profile-scripts/hk_fundamentals_history_collector.py b/deploy/profile-scripts/hk_fundamentals_history_collector.py index ddfeed30..71c1d776 100644 --- a/deploy/profile-scripts/hk_fundamentals_history_collector.py +++ b/deploy/profile-scripts/hk_fundamentals_history_collector.py @@ -11,6 +11,13 @@ import sqlite3 import sys import time import warnings + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass warnings.filterwarnings("ignore") DB_PATH = "/home/hmo/MoFin/data/mofin.db" diff --git a/deploy/profile-scripts/hk_scanner.py b/deploy/profile-scripts/hk_scanner.py index 53cf0223..41868978 100644 --- a/deploy/profile-scripts/hk_scanner.py +++ b/deploy/profile-scripts/hk_scanner.py @@ -19,6 +19,13 @@ from datetime import datetime from hk_strategies import HK_STRATEGIES, strategies_for_regime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") PANEL = "/tmp/panel_12d_hk.pkl" TD_GUARD = 5 # trend_down 连续>5天暂停超卖(组合级防守) diff --git a/deploy/profile-scripts/hk_sector_collector.py b/deploy/profile-scripts/hk_sector_collector.py index 6c6c64c1..b1bd5c06 100644 --- a/deploy/profile-scripts/hk_sector_collector.py +++ b/deploy/profile-scripts/hk_sector_collector.py @@ -11,6 +11,13 @@ import sqlite3 import sys import time import warnings + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass warnings.filterwarnings("ignore") DB_PATH = "/home/hmo/MoFin/data/mofin.db" diff --git a/deploy/profile-scripts/intraday_health_check.py b/deploy/profile-scripts/intraday_health_check.py index d29bef2c..e5e0b033 100644 --- a/deploy/profile-scripts/intraday_health_check.py +++ b/deploy/profile-scripts/intraday_health_check.py @@ -1,243 +1,250 @@ -#!/usr/bin/env python3 -"""intraday_health_check.py — 盘中高频轻量自检 (no_agent) - -每15分钟检查最关键的活动组件,只查会直接影响盘中运行的。 -发现问题→写TODO(消费管道与每日体检共享)。 -""" - -import json, os, subprocess, urllib.request, sys, socket -from pathlib import Path -from datetime import datetime, timedelta -from mofin_db import get_conn - -# ── MoFin path ───────────────────────────────────────────────────── -sys.path.insert(0, "/home/hmo/MoFin") -from mo_data import read_portfolio, read_decisions, read_watchlist - -BASE = Path("/home/hmo/MoFin") -DATA = BASE / "data" -DB_PATH = DATA / "mofin.db" -CRON_JOBS = Path("/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json") -GATEWAY_URL = "http://localhost:8643/v1/chat/completions" -GATEWAY_KEY = "hermes123" - -ISSUES = [] -OK_COUNT = 0 - - -def log(ok, msg): - global OK_COUNT - if ok: - OK_COUNT += 1 - else: - ISSUES.append(msg) - - -def check_port(port): - try: - r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5) - return f":{port}" in r.stdout - except: - return False - - -def check_http(url, timeout=5): - """检查HTTP可达性,5秒超时防止hang住""" - try: - for k in list(os.environ.keys()): - if 'proxy' in k.lower(): - os.environ.pop(k) - req = urllib.request.Request(url, method="GET") - urllib.request.urlopen(req, timeout=timeout) - return True - except: - return False - - -def db_today_count(table, date_col): - today = datetime.now().strftime("%Y-%m-%d") - try: - conn = get_conn() - r = conn.execute(f"SELECT COUNT(*) FROM {table} WHERE date({date_col}) = ?", (today,)).fetchone() - conn.close() - return r[0] - except: - return -1 - - -def check_price_monitor(): - """价格监控:检查price_monitor cron最近是否运行 + 数据是否更新 - - 注意:price_events 存储的是区间偏离事件(价格穿过买入区/止损/止盈边界), - 不是心跳信号。横盘期/无操作信号时自然不会有新事件。因此不检查event数, - 改为检查 cron 最后运行时间和 DB 数据新鲜度(read_portfolio() 从 mofin.db 读取)。 - """ - # 检查cron最近运行记录 - cron_ok = False - try: - with open(str(CRON_JOBS)) as f: - data = json.load(f) - jobs_list = data.get("jobs", []) if isinstance(data.get("jobs"), list) else [] - if not jobs_list: - jobs_list = list(data.get("jobs", {}).values()) - for job in jobs_list: - if not job: - continue - script = job.get("script") or "" - name = job.get("name") or "" - if "price_monitor" in script or "价格监控" in name: - last_run = job.get("last_run_at") - if last_run: - last_dt = datetime.fromisoformat(last_run) - # 兼容带时区和无时区两种格式 - ref_now = datetime.now(last_dt.tzinfo) if last_dt.tzinfo else datetime.now() - elapsed = (ref_now - last_dt).total_seconds() - if elapsed < 600: # 10分钟内运行过 - cron_ok = True - break - except Exception: - pass - - if not cron_ok: - log(False, "价格监控cron无最近运行记录(>10分钟未运行)") - return - - # 检查 DB 数据新鲜度(read_portfolio() 从 mofin.db 读取) - # 兼容 '2026-07-02 10:43'(price_monitor写入,无秒)和 '2026-07-02 10:43:53'(DB写入,有秒) - def _parse_updated_at(ts: str) -> datetime | None: - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"): - try: - return datetime.strptime(ts, fmt) - except ValueError: - continue - return None - - try: - pf = read_portfolio() - pf_updated = pf.get("updated_at", "") - if pf_updated: - pf_dt = _parse_updated_at(pf_updated) - if pf_dt is None: - log(False, f"价格数据updated_at格式无法解析: {pf_updated}") - else: - seconds_ago = (datetime.now() - pf_dt).total_seconds() - if seconds_ago < 600: # 10分钟内 - log(True, f"价格监控运行正常,数据{int(seconds_ago//60)}分钟前更新") - else: - log(False, f"价格数据{int(seconds_ago)}秒未更新(来自DB)") - else: - log(False, "DB 价格数据缺少updated_at字段") - except Exception as e: - log(False, f"价格数据新鲜度检查失败: {e}") - - -def check_bots(): - zhiwei = subprocess.run(["systemctl", "is-active", "xmpp-zhiwei.service"], - capture_output=True, text=True, timeout=5).stdout.strip() == "active" - log(zhiwei, "知微XMPP Bot离线") - - -def check_gateways(): - log(check_port(8643), "知微Gateway :8643 未监听") - - -def check_signal_pipeline(): - """信号处理管道:检查信号积压 + 宏观风险状态""" - unproc = 0 - try: - conn = get_conn() - # 全量未处理信号 - r = conn.execute("SELECT COUNT(*) FROM signal_news WHERE (processed=0 OR processed IS NULL)").fetchone() - unproc = r[0] - conn.close() - except: - pass - if unproc > 50: - log(False, f"信号积压: {unproc}条未处理") - - # 宏观风险状态检查 - try: - risk_path = DATA / "macro_risk_state.json" - if risk_path.exists(): - risk = json.loads(risk_path.read_text()) - level = risk.get("level", "none") - expired = risk.get("expired", False) - # 提取摘要做原因描述(state.json用signals数组,不是reason字段) - signals = risk.get("signals", []) - reason = "" - if signals and isinstance(signals, list) and len(signals) > 0: - first_sig = signals[0] - summary = first_sig.get("summary", "") - if summary: - reason = summary[:80].replace("\n", " ") - if level == "high" and not expired: - reason_clean = reason.replace("【高风险】", "").strip()[:60] - log(False, f"🔴 宏观风险HIGH: {reason_clean}") - elif level == "high" and expired: - log(True, f"⏳ 宏观风险HIGH已过期(无新信号超过15分钟)") - elif level == "medium": - log(True, f"⚠️ 宏观风险MEDIUM: {reason}") - else: - log(True, "无宏观风险状态文件(可能未生成)") - except: - pass - - -def write_todos(): - if not ISSUES: - return - for msg in ISSUES: - title = f"[盘中自检] {msg}" - try: - conn = get_conn() - # 宏观风险HIGH去重:只要有pending/in_progress的宏观风险TODO,不再新增 - if "宏观风险HIGH" in msg: - exist = conn.execute( - "SELECT id FROM todos WHERE title LIKE '%宏观风险HIGH%' AND status IN ('pending','in_progress') LIMIT 1" - ).fetchone() - else: - exist = conn.execute( - "SELECT id FROM todos WHERE title=? AND status IN ('pending','in_progress')", (title,) - ).fetchone() - if not exist: - conn.execute( - "INSERT INTO todos (title, description, priority, source, status, fix_action) " - "VALUES (?, ?, 'high', 'intraday_check', 'pending', NULL)", - (title, f"盘中自动发现: {msg}")) - conn.commit() - conn.close() - except: - pass - - -def main(): - now = datetime.now() - # 只在交易时段运行 - if now.weekday() >= 5 or now.hour < 9 or now.hour >= 15: - print("[SILENT] 非交易时段") - return - - check_bots() - check_gateways() - if 9 <= now.hour < 16: - # 开盘前10分钟(9:00-9:10)跳过价格新鲜度检查 - # price_monitor 从 09:00 才开始启动,09:01 检查时数据还未更新(前一天收盘数据) - # 给 price_monitor 足够时间完成第一轮数据拉取更新 - if now.hour == 9 and now.minute < 10: - log(True, "开盘初期,价格监控grace period(跳过新鲜度检查)") - else: - check_price_monitor() - check_signal_pipeline() - - write_todos() - - if ISSUES: - print(f"盘中自检 | {now.strftime('%H:%M')} | {len(ISSUES)}项异常:") - for i in ISSUES: - print(f" ⚠️ {i}") - else: - print(f"[SILENT] 盘中自检通过 | {OK_COUNT}项正常") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""intraday_health_check.py — 盘中高频轻量自检 (no_agent) + +每15分钟检查最关键的活动组件,只查会直接影响盘中运行的。 +发现问题→写TODO(消费管道与每日体检共享)。 +""" + +import json, os, subprocess, urllib.request, sys, socket +from pathlib import Path +from datetime import datetime, timedelta +from mofin_db import get_conn + +# ── MoFin path ───────────────────────────────────────────────────── +sys.path.insert(0, "/home/hmo/MoFin") +from mo_data import read_portfolio, read_decisions, read_watchlist + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +BASE = Path("/home/hmo/MoFin") +DATA = BASE / "data" +DB_PATH = DATA / "mofin.db" +CRON_JOBS = Path("/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json") +GATEWAY_URL = "http://localhost:8643/v1/chat/completions" +GATEWAY_KEY = "hermes123" + +ISSUES = [] +OK_COUNT = 0 + + +def log(ok, msg): + global OK_COUNT + if ok: + OK_COUNT += 1 + else: + ISSUES.append(msg) + + +def check_port(port): + try: + r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5) + return f":{port}" in r.stdout + except: + return False + + +def check_http(url, timeout=5): + """检查HTTP可达性,5秒超时防止hang住""" + try: + for k in list(os.environ.keys()): + if 'proxy' in k.lower(): + os.environ.pop(k) + req = urllib.request.Request(url, method="GET") + urllib.request.urlopen(req, timeout=timeout) + return True + except: + return False + + +def db_today_count(table, date_col): + today = datetime.now().strftime("%Y-%m-%d") + try: + conn = get_conn() + r = conn.execute(f"SELECT COUNT(*) FROM {table} WHERE date({date_col}) = ?", (today,)).fetchone() + conn.close() + return r[0] + except: + return -1 + + +def check_price_monitor(): + """价格监控:检查price_monitor cron最近是否运行 + 数据是否更新 + + 注意:price_events 存储的是区间偏离事件(价格穿过买入区/止损/止盈边界), + 不是心跳信号。横盘期/无操作信号时自然不会有新事件。因此不检查event数, + 改为检查 cron 最后运行时间和 DB 数据新鲜度(read_portfolio() 从 mofin.db 读取)。 + """ + # 检查cron最近运行记录 + cron_ok = False + try: + with open(str(CRON_JOBS)) as f: + data = json.load(f) + jobs_list = data.get("jobs", []) if isinstance(data.get("jobs"), list) else [] + if not jobs_list: + jobs_list = list(data.get("jobs", {}).values()) + for job in jobs_list: + if not job: + continue + script = job.get("script") or "" + name = job.get("name") or "" + if "price_monitor" in script or "价格监控" in name: + last_run = job.get("last_run_at") + if last_run: + last_dt = datetime.fromisoformat(last_run) + # 兼容带时区和无时区两种格式 + ref_now = datetime.now(last_dt.tzinfo) if last_dt.tzinfo else datetime.now() + elapsed = (ref_now - last_dt).total_seconds() + if elapsed < 600: # 10分钟内运行过 + cron_ok = True + break + except Exception: + pass + + if not cron_ok: + log(False, "价格监控cron无最近运行记录(>10分钟未运行)") + return + + # 检查 DB 数据新鲜度(read_portfolio() 从 mofin.db 读取) + # 兼容 '2026-07-02 10:43'(price_monitor写入,无秒)和 '2026-07-02 10:43:53'(DB写入,有秒) + def _parse_updated_at(ts: str) -> datetime | None: + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"): + try: + return datetime.strptime(ts, fmt) + except ValueError: + continue + return None + + try: + pf = read_portfolio() + pf_updated = pf.get("updated_at", "") + if pf_updated: + pf_dt = _parse_updated_at(pf_updated) + if pf_dt is None: + log(False, f"价格数据updated_at格式无法解析: {pf_updated}") + else: + seconds_ago = (datetime.now() - pf_dt).total_seconds() + if seconds_ago < 600: # 10分钟内 + log(True, f"价格监控运行正常,数据{int(seconds_ago//60)}分钟前更新") + else: + log(False, f"价格数据{int(seconds_ago)}秒未更新(来自DB)") + else: + log(False, "DB 价格数据缺少updated_at字段") + except Exception as e: + log(False, f"价格数据新鲜度检查失败: {e}") + + +def check_bots(): + zhiwei = subprocess.run(["systemctl", "is-active", "xmpp-zhiwei.service"], + capture_output=True, text=True, timeout=5).stdout.strip() == "active" + log(zhiwei, "知微XMPP Bot离线") + + +def check_gateways(): + log(check_port(8643), "知微Gateway :8643 未监听") + + +def check_signal_pipeline(): + """信号处理管道:检查信号积压 + 宏观风险状态""" + unproc = 0 + try: + conn = get_conn() + # 全量未处理信号 + r = conn.execute("SELECT COUNT(*) FROM signal_news WHERE (processed=0 OR processed IS NULL)").fetchone() + unproc = r[0] + conn.close() + except: + pass + if unproc > 50: + log(False, f"信号积压: {unproc}条未处理") + + # 宏观风险状态检查 + try: + risk_path = DATA / "macro_risk_state.json" + if risk_path.exists(): + risk = json.loads(risk_path.read_text()) + level = risk.get("level", "none") + expired = risk.get("expired", False) + # 提取摘要做原因描述(state.json用signals数组,不是reason字段) + signals = risk.get("signals", []) + reason = "" + if signals and isinstance(signals, list) and len(signals) > 0: + first_sig = signals[0] + summary = first_sig.get("summary", "") + if summary: + reason = summary[:80].replace("\n", " ") + if level == "high" and not expired: + reason_clean = reason.replace("【高风险】", "").strip()[:60] + log(False, f"🔴 宏观风险HIGH: {reason_clean}") + elif level == "high" and expired: + log(True, f"⏳ 宏观风险HIGH已过期(无新信号超过15分钟)") + elif level == "medium": + log(True, f"⚠️ 宏观风险MEDIUM: {reason}") + else: + log(True, "无宏观风险状态文件(可能未生成)") + except: + pass + + +def write_todos(): + if not ISSUES: + return + for msg in ISSUES: + title = f"[盘中自检] {msg}" + try: + conn = get_conn() + # 宏观风险HIGH去重:只要有pending/in_progress的宏观风险TODO,不再新增 + if "宏观风险HIGH" in msg: + exist = conn.execute( + "SELECT id FROM todos WHERE title LIKE '%宏观风险HIGH%' AND status IN ('pending','in_progress') LIMIT 1" + ).fetchone() + else: + exist = conn.execute( + "SELECT id FROM todos WHERE title=? AND status IN ('pending','in_progress')", (title,) + ).fetchone() + if not exist: + conn.execute( + "INSERT INTO todos (title, description, priority, source, status, fix_action) " + "VALUES (?, ?, 'high', 'intraday_check', 'pending', NULL)", + (title, f"盘中自动发现: {msg}")) + conn.commit() + conn.close() + except: + pass + + +def main(): + now = datetime.now() + # 只在交易时段运行 + if now.weekday() >= 5 or now.hour < 9 or now.hour >= 15: + print("[SILENT] 非交易时段") + return + + check_bots() + check_gateways() + if 9 <= now.hour < 16: + # 开盘前10分钟(9:00-9:10)跳过价格新鲜度检查 + # price_monitor 从 09:00 才开始启动,09:01 检查时数据还未更新(前一天收盘数据) + # 给 price_monitor 足够时间完成第一轮数据拉取更新 + if now.hour == 9 and now.minute < 10: + log(True, "开盘初期,价格监控grace period(跳过新鲜度检查)") + else: + check_price_monitor() + check_signal_pipeline() + + write_todos() + + if ISSUES: + print(f"盘中自检 | {now.strftime('%H:%M')} | {len(ISSUES)}项异常:") + for i in ISSUES: + print(f" ⚠️ {i}") + else: + print(f"[SILENT] 盘中自检通过 | {OK_COUNT}项正常") + + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/kanban_xmpp_bridge.py b/deploy/profile-scripts/kanban_xmpp_bridge.py index 87334d42..a4ac5a65 100644 --- a/deploy/profile-scripts/kanban_xmpp_bridge.py +++ b/deploy/profile-scripts/kanban_xmpp_bridge.py @@ -10,6 +10,13 @@ """ import json, os, sqlite3, time, urllib.request +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + KANBAN_DB = "/home/hmo/.hermes/kanban.db" STATE = "/home/hmo/MoFin/gateway/logs/kanban_bridge_state.json" XMPP_SEND = "http://127.0.0.1:5805/" diff --git a/deploy/profile-scripts/live_data_collector.py b/deploy/profile-scripts/live_data_collector.py index 1b042c60..7ab46b38 100644 --- a/deploy/profile-scripts/live_data_collector.py +++ b/deploy/profile-scripts/live_data_collector.py @@ -11,6 +11,13 @@ import sqlite3, json, os, pandas as pd, numpy as np from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" LIVE_DIR = "/home/hmo/MoFin/data/live_data" os.makedirs(LIVE_DIR, exist_ok=True) diff --git a/deploy/profile-scripts/macro_context_collector.py b/deploy/profile-scripts/macro_context_collector.py index 73ff596e..35899ee0 100644 --- a/deploy/profile-scripts/macro_context_collector.py +++ b/deploy/profile-scripts/macro_context_collector.py @@ -21,6 +21,13 @@ import sys, json, hashlib, os, re from datetime import datetime from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DATA_DIR = Path("/home/hmo/MoFin/data") DB_PATH = DATA_DIR / "mofin.db" STATE_PATH = DATA_DIR / "macro_risk_state.json" diff --git a/deploy/profile-scripts/macro_signal_consumer.py b/deploy/profile-scripts/macro_signal_consumer.py index f13a585f..14dbdff1 100644 --- a/deploy/profile-scripts/macro_signal_consumer.py +++ b/deploy/profile-scripts/macro_signal_consumer.py @@ -15,6 +15,13 @@ import sqlite3, json, os, sys, time from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + BASE = Path("/home/hmo/MoFin") DATA = BASE / "data" DB_PATH = DATA / "mofin.db" diff --git a/deploy/profile-scripts/market_insight.py b/deploy/profile-scripts/market_insight.py index fcb7d8ae..64ddb65d 100644 --- a/deploy/profile-scripts/market_insight.py +++ b/deploy/profile-scripts/market_insight.py @@ -15,6 +15,13 @@ import sys from datetime import datetime from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DATA_DIR = Path(__file__).parent.parent / "data" # ── 持仓股 → 行业映射(从 stock_profiles 自动提取) ── diff --git a/deploy/profile-scripts/market_regime.py b/deploy/profile-scripts/market_regime.py index 929d90c9..d9e8cc4d 100644 --- a/deploy/profile-scripts/market_regime.py +++ b/deploy/profile-scripts/market_regime.py @@ -35,6 +35,13 @@ import sqlite3 from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # ── 路径注入:可被 deploy/profile-scripts 下脚本直接 import ── _SCRIPT_DIR = Path(__file__).resolve().parent _MOFIN_ROOT = _SCRIPT_DIR.parent.parent # deploy/profile-scripts → MoFin diff --git a/deploy/profile-scripts/market_watch.py b/deploy/profile-scripts/market_watch.py index 95002ee1..4a152a9f 100644 --- a/deploy/profile-scripts/market_watch.py +++ b/deploy/profile-scripts/market_watch.py @@ -19,6 +19,13 @@ from pathlib import Path from mofin_db import get_conn, init_all_tables, write_market_snapshot from messenger import send as messenger_send +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DATA_DIR = Path(__file__).parent.parent / "data" diff --git a/deploy/profile-scripts/memory_guardian.py b/deploy/profile-scripts/memory_guardian.py index ef5e1508..2e9fa93e 100644 --- a/deploy/profile-scripts/memory_guardian.py +++ b/deploy/profile-scripts/memory_guardian.py @@ -13,6 +13,13 @@ import json, subprocess, urllib.request from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + HOME = Path.home() PROFILE = HOME / ".hermes" / "profiles" / "position-analyst" MEMORY_FILE = PROFILE / "MEMORY.md" diff --git a/deploy/profile-scripts/mofin_health.py b/deploy/profile-scripts/mofin_health.py index 4395a2de..09ac1388 100644 --- a/deploy/profile-scripts/mofin_health.py +++ b/deploy/profile-scripts/mofin_health.py @@ -12,6 +12,13 @@ from pathlib import Path from datetime import datetime, timezone from mofin_db import get_conn +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DATA_DIR = Path("/home/hmo/MoFin/data") WEB_DATA = Path("/home/hmo/web-dashboard/data") STATIC_DIR = Path("/home/hmo/web-dashboard/static") diff --git a/deploy/profile-scripts/morning_health_check.py b/deploy/profile-scripts/morning_health_check.py index dd89543d..f53af5b5 100644 --- a/deploy/profile-scripts/morning_health_check.py +++ b/deploy/profile-scripts/morning_health_check.py @@ -30,6 +30,13 @@ import json, os, sqlite3, subprocess, sys, time, urllib.request from pathlib import Path from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # ── 路径 ── BASE = Path("/home/hmo/MoFin") DATA = BASE / "data" diff --git a/deploy/profile-scripts/news_collector_full_b0.py b/deploy/profile-scripts/news_collector_full_b0.py index 1a281f8b..d964dd7b 100644 --- a/deploy/profile-scripts/news_collector_full_b0.py +++ b/deploy/profile-scripts/news_collector_full_b0.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b0.py — 全市场新闻采集(batch 0,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "0"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b1.py b/deploy/profile-scripts/news_collector_full_b1.py index 268eeb18..b6cd5572 100644 --- a/deploy/profile-scripts/news_collector_full_b1.py +++ b/deploy/profile-scripts/news_collector_full_b1.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b1.py — 全市场新闻采集(batch 1,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "1"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b2.py b/deploy/profile-scripts/news_collector_full_b2.py index 2895c6cd..5eee859f 100644 --- a/deploy/profile-scripts/news_collector_full_b2.py +++ b/deploy/profile-scripts/news_collector_full_b2.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b2.py — 全市场新闻采集(batch 2,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "2"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b3.py b/deploy/profile-scripts/news_collector_full_b3.py index 64df27a8..7add60da 100644 --- a/deploy/profile-scripts/news_collector_full_b3.py +++ b/deploy/profile-scripts/news_collector_full_b3.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b3.py — 全市场新闻采集(batch 3,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "3"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b4.py b/deploy/profile-scripts/news_collector_full_b4.py index 7f8df3fc..7218d088 100644 --- a/deploy/profile-scripts/news_collector_full_b4.py +++ b/deploy/profile-scripts/news_collector_full_b4.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b4.py — 全市场新闻采集(batch 4,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "4"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b5.py b/deploy/profile-scripts/news_collector_full_b5.py index 807b6aea..4d6a8bd1 100644 --- a/deploy/profile-scripts/news_collector_full_b5.py +++ b/deploy/profile-scripts/news_collector_full_b5.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b5.py — 全市场新闻采集(batch 5,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "5"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b6.py b/deploy/profile-scripts/news_collector_full_b6.py index 1262de3a..8e177223 100644 --- a/deploy/profile-scripts/news_collector_full_b6.py +++ b/deploy/profile-scripts/news_collector_full_b6.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b6.py — 全市场新闻采集(batch 6,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "6"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/news_collector_full_b7.py b/deploy/profile-scripts/news_collector_full_b7.py index 6d23896e..8a83caea 100644 --- a/deploy/profile-scripts/news_collector_full_b7.py +++ b/deploy/profile-scripts/news_collector_full_b7.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 """news_collector_full_b7.py — 全市场新闻采集(batch 7,包装入口)""" import sys, runpy + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass sys.argv = ["news_collector_full.py", "--batch", "7"] runpy.run_path("/home/hmo/.hermes/profiles/position-analyst/scripts/news_collector_full.py", run_name="__main__") diff --git a/deploy/profile-scripts/opening_brief.py b/deploy/profile-scripts/opening_brief.py index c58dd7ca..49ebd43b 100644 --- a/deploy/profile-scripts/opening_brief.py +++ b/deploy/profile-scripts/opening_brief.py @@ -7,6 +7,13 @@ import sys import subprocess from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + script_dir = Path(__file__).parent.resolve() target = script_dir / "generate_report.py" diff --git a/deploy/profile-scripts/per_stock_reassess.py b/deploy/profile-scripts/per_stock_reassess.py index 7cb4e57d..4e976561 100644 --- a/deploy/profile-scripts/per_stock_reassess.py +++ b/deploy/profile-scripts/per_stock_reassess.py @@ -40,6 +40,13 @@ from mo_data import read_decisions, read_portfolio from llm_client import call_llm, REASSESS_MODEL from mofin_db import snapshot_strategy_history +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + def _build_full_analysis(code, entry, result): """从重评结果构建完整12维分析文本""" diff --git a/deploy/profile-scripts/pool_news_collector.py b/deploy/profile-scripts/pool_news_collector.py index c4ed4e80..e0977208 100644 --- a/deploy/profile-scripts/pool_news_collector.py +++ b/deploy/profile-scripts/pool_news_collector.py @@ -14,6 +14,13 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from news_collector import fetch_stock_news, init_table +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") SLEEP = 0.4 # 请求间隔(防东财封) RECENT_DAYS = 3 # 只存近 3 天新闻(news3 因子口径) diff --git a/deploy/profile-scripts/preflight_verify.py b/deploy/profile-scripts/preflight_verify.py index e4686ebc..9b749a44 100644 --- a/deploy/profile-scripts/preflight_verify.py +++ b/deploy/profile-scripts/preflight_verify.py @@ -14,6 +14,13 @@ import sys, os, json, sqlite3, subprocess from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + MOFIN = Path("/home/hmo/MoFin") DATA = MOFIN / "data" DB_PATH = DATA / "mofin.db" diff --git a/deploy/profile-scripts/premarket_full_review.py b/deploy/profile-scripts/premarket_full_review.py index c27147db..bc422435 100644 --- a/deploy/profile-scripts/premarket_full_review.py +++ b/deploy/profile-scripts/premarket_full_review.py @@ -59,6 +59,13 @@ print("\n" + "=" * 50) print("🔍 自选退出检查") print("=" * 50) from watchlist_auto_exit import main as auto_exit + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass exited = auto_exit(dry_run=False) # Step 3: 写入摘要供开盘简报引用 diff --git a/deploy/profile-scripts/price_monitor.py b/deploy/profile-scripts/price_monitor.py index 46c118aa..412b8c3a 100644 --- a/deploy/profile-scripts/price_monitor.py +++ b/deploy/profile-scripts/price_monitor.py @@ -78,6 +78,13 @@ except ImportError: # 市场抽象层(阶段1:行情符号/市场判断唯一事实源) from market_config import kline_symbol, market_for_code +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # 策略重评依赖(技术面驱动,非机械百分比) sys.path.insert(0, "/home/hmo/web-dashboard") try: diff --git a/deploy/profile-scripts/promote_candidates.py b/deploy/profile-scripts/promote_candidates.py index 1deaaaef..9de0ca9a 100644 --- a/deploy/profile-scripts/promote_candidates.py +++ b/deploy/profile-scripts/promote_candidates.py @@ -7,6 +7,13 @@ import sys, json, sqlite3, time from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") # 2026-08-10 修复参数:控制单次运行时长,防 hermes 600s 超时杀进程 diff --git a/deploy/profile-scripts/refresh_macro_context.py b/deploy/profile-scripts/refresh_macro_context.py index 7acc4735..de91b7f5 100644 --- a/deploy/profile-scripts/refresh_macro_context.py +++ b/deploy/profile-scripts/refresh_macro_context.py @@ -11,6 +11,13 @@ from pathlib import Path from datetime import datetime from mo_data import get_price +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = Path("/home/hmo/MoFin/data/mofin.db") def fetch_index(code, name): diff --git a/deploy/profile-scripts/refresh_mtf_cache.py b/deploy/profile-scripts/refresh_mtf_cache.py index 88a657d8..f3dee228 100644 --- a/deploy/profile-scripts/refresh_mtf_cache.py +++ b/deploy/profile-scripts/refresh_mtf_cache.py @@ -13,6 +13,13 @@ import os import json from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + # 确保能找到 web-dashboard 模块 sys.path.insert(0, "/home/hmo/web-dashboard") diff --git a/deploy/profile-scripts/regime_tracker.py b/deploy/profile-scripts/regime_tracker.py index 73e577cc..48df9ade 100644 --- a/deploy/profile-scripts/regime_tracker.py +++ b/deploy/profile-scripts/regime_tracker.py @@ -17,6 +17,13 @@ import sqlite3 from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + _SCRIPT_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(_SCRIPT_DIR)) sys.path.insert(0, "/home/hmo/MoFin") diff --git a/deploy/profile-scripts/review_needed_watchdog.py b/deploy/profile-scripts/review_needed_watchdog.py index 88d3511c..92c7f6ef 100644 --- a/deploy/profile-scripts/review_needed_watchdog.py +++ b/deploy/profile-scripts/review_needed_watchdog.py @@ -9,6 +9,13 @@ XMPP 走 alert_helper 统一网关(分级 ACTION)。 """ import sys, json, os, datetime, sqlite3 +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, "/home/hmo/MoFin") os.chdir("/home/hmo/MoFin") DB = "/home/hmo/MoFin/data/mofin.db" diff --git a/deploy/profile-scripts/sector_enrich_cninfo.py b/deploy/profile-scripts/sector_enrich_cninfo.py index f82d2bdb..dab9a2d6 100644 --- a/deploy/profile-scripts/sector_enrich_cninfo.py +++ b/deploy/profile-scripts/sector_enrich_cninfo.py @@ -5,6 +5,13 @@ import sqlite3, time, sys import akshare as ak +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" def main(): diff --git a/deploy/profile-scripts/sector_index_builder.py b/deploy/profile-scripts/sector_index_builder.py index a83fea91..1efbb631 100644 --- a/deploy/profile-scripts/sector_index_builder.py +++ b/deploy/profile-scripts/sector_index_builder.py @@ -18,6 +18,13 @@ import sys, os, sqlite3, fcntl, time from pathlib import Path from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") RECENT_DAYS = 5 # 增量:每次只算最近 5 天(历史已有,每日新增) diff --git a/deploy/profile-scripts/self_repair.py b/deploy/profile-scripts/self_repair.py index 91aff0cf..60cc7495 100644 --- a/deploy/profile-scripts/self_repair.py +++ b/deploy/profile-scripts/self_repair.py @@ -15,6 +15,13 @@ import os, sys, json, subprocess, sqlite3, time from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, '/home/hmo/MoFin') FUNCTIONAL_REPORT = '/home/hmo/MoFin/gateway/logs/functional_health.json' diff --git a/deploy/profile-scripts/stale_detector.py b/deploy/profile-scripts/stale_detector.py index 81587482..ba0f96eb 100644 --- a/deploy/profile-scripts/stale_detector.py +++ b/deploy/profile-scripts/stale_detector.py @@ -17,6 +17,13 @@ from datetime import datetime, timezone sys.path.insert(0, '/home/hmo/MoFin') from mo_data import read_portfolio, read_decisions, read_watchlist, get_price, get_prices_batch +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + def fetch_prices(codes): """统一价格源:优先 stock_quote.py,腾讯API降级为兜底""" diff --git a/deploy/profile-scripts/strategy-staleness-check.py b/deploy/profile-scripts/strategy-staleness-check.py index b0bcd392..274551f5 100644 --- a/deploy/profile-scripts/strategy-staleness-check.py +++ b/deploy/profile-scripts/strategy-staleness-check.py @@ -15,6 +15,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from datetime import datetime from mo_data import read_decisions, read_portfolio +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB_PATH = '/home/hmo/web-dashboard/data/mofin.db' OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json" diff --git a/deploy/profile-scripts/strategy_alert.py b/deploy/profile-scripts/strategy_alert.py index 11927387..01694c3b 100644 --- a/deploy/profile-scripts/strategy_alert.py +++ b/deploy/profile-scripts/strategy_alert.py @@ -14,6 +14,13 @@ import json from pathlib import Path from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" OUT = Path("/home/hmo/MoFin/data/strategy_alerts.json") diff --git a/deploy/profile-scripts/strategy_effectiveness.py b/deploy/profile-scripts/strategy_effectiveness.py index 45bb2be2..d749e092 100644 --- a/deploy/profile-scripts/strategy_effectiveness.py +++ b/deploy/profile-scripts/strategy_effectiveness.py @@ -4,6 +4,13 @@ import os, sys, sqlite3, json from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" def get_expired_strategies(conn): diff --git a/deploy/profile-scripts/strategy_executor.py b/deploy/profile-scripts/strategy_executor.py index 2e39ca56..e31a4f35 100644 --- a/deploy/profile-scripts/strategy_executor.py +++ b/deploy/profile-scripts/strategy_executor.py @@ -30,6 +30,13 @@ import subprocess from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + _SCRIPT_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(_SCRIPT_DIR)) sys.path.insert(0, "/home/hmo/MoFin") diff --git a/deploy/profile-scripts/strategy_period_rollup.py b/deploy/profile-scripts/strategy_period_rollup.py index 18344b0b..5e1338d5 100644 --- a/deploy/profile-scripts/strategy_period_rollup.py +++ b/deploy/profile-scripts/strategy_period_rollup.py @@ -21,6 +21,13 @@ from pathlib import Path from datetime import datetime, timedelta from collections import Counter +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + _SCRIPT_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(_SCRIPT_DIR)) sys.path.insert(0, "/home/hmo/MoFin") diff --git a/deploy/profile-scripts/strategy_review.py b/deploy/profile-scripts/strategy_review.py index 3ae741c6..50bd92f6 100644 --- a/deploy/profile-scripts/strategy_review.py +++ b/deploy/profile-scripts/strategy_review.py @@ -1,293 +1,300 @@ -#!/usr/bin/env python3 -"""strategy_review.py — 三层策略复盘 (no_agent) - -每层独立评估: -1. 信号层 — 买入/卖出/持有的timing对不对? -2. 执行层 — 止损/止盈设得合理吗? -3. 综合层 — 这波操作整体赚钱了吗? - -用法: - python3 scripts/strategy_review.py -""" - -import json, sqlite3, sys, time -from pathlib import Path -from datetime import datetime -from collections import Counter -from mo_data import read_portfolio, read_decisions, read_watchlist, get_price, get_prices_batch - -BASE = Path("/home/hmo/MoFin") -DATA = BASE / "data" -DB_PATH = DATA / "mofin.db" - -# 失败模式定义(执行层) -EXEC_FAILURES = { - "stop_too_tight": {"label": "止损过紧", "fix": "放宽止损到强支撑×0.95,给价格波动留空间"}, - "tp_too_close": {"label": "止盈过近", "fix": "止盈放到更高阻力位,让利润奔跑"}, - "stop_too_loose": {"label": "止损过宽", "fix": "收紧止损,少亏当赢"}, - "tp_too_far": {"label": "止盈过远", "fix": "止盈靠近合理阻力位,提高兑现概率"}, -} - -# 失败模式定义(信号层) -SIGNAL_FAILURES = { - "wrong_direction": {"label": "方向看反", "fix": "检查多周期趋势判断逻辑"}, - "entry_too_early": {"label": "入场过早", "fix": "等缩量确认支撑再入,不追回调"}, - "bad_signal": {"label": "信号误判", "fix": "修正timing_signal合成权重"}, - "regime_mismatch": {"label": "情景错配", "fix": "加入市场情景过滤条件"}, -} - - -def fetch_price(code): - # DB 优先 - try: from mofin_db import get_price_from_db; p, _ = get_price_from_db(code); return p if p else 0 - except: pass - # Fallback: mo_data.get_price - try: - p, _ = get_price(code) - return p if p else 0 - except: - return 0 - - -def evaluate_strategy(s, price): - """三层评估单条策略,返回 (signal_verdict, exec_verdict, overall_verdict, detail)""" - code = s.get("code", "") - name = s.get("name", "") - sl = s.get("stop_loss", 0) or 0 - tp = s.get("take_profit", 0) or 0 - entry_low = s.get("entry_low", 0) or 0 - entry_high = s.get("entry_high", 0) or 0 - cost = s.get("cost", 0) or s.get("avg_price", 0) or 0 - signal = (s.get("timing_signal", "") or s.get("current", "") or "").lower() - created = s.get("created_at", "") or s.get("timestamp", "") - s_type = s.get("type", "") # 持仓策略/自选策略 - - if not created or not price: - return "skip", "skip", "skip", "数据不足", "" - - # 计算运行天数 - try: - days = (datetime.now() - datetime.fromisoformat(created)).days - except: - days = 0 - - # ─── 综合层:赚钱了吗? ─── - if cost > 0 and s_type == "持仓策略": - profit_pct = (price - cost) / cost * 100 - if profit_pct > 5: - overall = "盈利" - elif profit_pct > -5: - overall = "持平" - else: - overall = f"亏损{profit_pct:.0f}%" - elif tp > 0 and price >= tp: - overall = "触止盈" - elif sl > 0 and price <= sl: - overall = "触止损" - else: - overall = "持有中" - - # ─── 信号层:timing对不对? ─── - is_buy_signal = any(kw in signal for kw in ["买入", "加仓", "追涨", "可买"]) - is_sell_signal = any(kw in signal for kw in ["卖出", "减仓", "止损", "离场"]) - is_hold_signal = any(kw in signal for kw in ["持有", "观望", "等待", "持股"]) - - signal_verdict = "待定" - signal_fail = None - - if is_buy_signal or is_hold_signal: - if sl > 0 and price <= sl: - # 买入/持有信号下触发止损 → 信号方向可能错了 - signal_verdict = "存疑" - signal_fail = "wrong_direction" - elif tp > 0 and price >= tp * 0.95: - signal_verdict = "正确" - elif entry_low > 0 and price < entry_low * 0.85: - signal_verdict = "存疑" - signal_fail = "entry_too_early" - elif days > 30 and tp > 0 and price < entry_low: - signal_verdict = "存疑" - signal_fail = "wrong_direction" - else: - signal_verdict = "待定" - elif is_sell_signal: - if sl > 0 and price <= sl: - signal_verdict = "正确" - elif price > (cost or entry_low or 0) * 1.05: - signal_verdict = "存疑" - signal_fail = "bad_signal" - else: - signal_verdict = "待定" - else: - # 无明确信号 - if price > (entry_high or 0): - signal_verdict = "待定(价涨)" - elif sl > 0 and price <= sl * 1.05: - signal_verdict = "待定(近止损)" - else: - signal_verdict = "待定" - - # ─── 执行层:止损/止盈设得好不好? ─── - exec_verdict = "待定" - exec_fail = None - - # 取近期最高/最低价(判断卖飞/洗盘) - recent_high = 0 - recent_low = 0 - sl_recovery = False - if tp > 0 or sl > 0: - try: - prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" - url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={prefix}{code},day,,,60,qfq" - import subprocess as sp - r = sp.run(["curl", "-s", "--max-time", "3", url], capture_output=True, text=True, timeout=5) - if r.returncode == 0 and r.stdout: - data = json.loads(r.stdout) - day_key = 'qfqday' if prefix != 'hk' else 'day' - bars = data.get('data', {}).get(f'{prefix}{code}', {}).get(day_key, []) - if bars: - prices = [(float(b[2]), float(b[3]), b[0]) for b in bars if len(b) > 3] # (high, low, date) - recent_high = max(p[0] for p in prices) - recent_low = min(p[1] for p in prices) - # 检查止损触发后的走势:是否后来反弹了? - if sl > 0: - # 找出价格低于SL的K线 - below_sl = [p for p in prices if p[1] <= sl] - above_sl_later = [p for p in prices if p[1] > sl * 1.03] - if below_sl and above_sl_later: - # 曾跌破SL,但后来涨回去了 → 洗盘 - first_below = min(below_sl, key=lambda x: x[2]) - last_above = max(above_sl_later, key=lambda x: x[2]) - if last_above[2] > first_below[2]: - sl_recovery = True - except: - pass - - if sl > 0 and price <= sl: - if sl_recovery: - exec_verdict = "洗盘(触发后反弹)" - exec_fail = "stop_too_tight" - elif price >= sl * 0.95: - exec_verdict = "临界(差一点触发)" - exec_fail = "stop_too_tight" - else: - exec_verdict = "已触发" - elif tp > 0 and (price >= tp or recent_high >= tp): - # 止盈触发或曾触发过 - max_price = max(price, recent_high) - if max_price <= tp * 1.05: - exec_verdict = "已触发" - else: - overshoot = (max_price - tp) / tp * 100 - exec_verdict = f"卖飞({overshoot:.0f}%)" - exec_fail = "tp_too_close" - elif days > 45 and tp > 0 and price < entry_low: - exec_verdict = "存疑(久未达标)" - exec_fail = "tp_too_far" - elif sl > 0 and price >= entry_low and price <= entry_high: - exec_verdict = "持有中" - else: - exec_verdict = "待定" - - return signal_verdict, exec_verdict, overall, signal_fail, exec_fail - - -def review(): - start = time.time() - decisions = read_decisions() - strategies = decisions.get("decisions", []) - - conn = sqlite3.connect(str(DB_PATH)) - - stats = {"correct": 0, "wrong": 0, "mixed": 0, "pending": 0, "total": 0} - signal_fails = Counter() - exec_fails = Counter() - detail_lines = [] - - for s in strategies: - if s.get("status") == "closed": - continue - stats["total"] += 1 - code = s.get("code", "") - name = s.get("name", "") - price = fetch_price(code) - if not price: - detail_lines.append(f" ⏭️ {name}({code}): 无行情") - stats["pending"] += 1 - continue - - sv, ev, overall, sf, ef = evaluate_strategy(s, price) - - # 综合评级 - if overall in ("盈利", "触止盈"): - if sv == "正确" or "存疑" not in sv: - stats["correct"] += 1 - else: - stats["mixed"] += 1 - elif overall in ("触止损",) and "存疑" in sv: - stats["wrong"] += 1 - elif "存疑" in sv or "存疑" in ev: - stats["wrong"] += 1 - elif overall in ("持有中", "持平"): - stats["mixed"] += 1 - else: - stats["pending"] += 1 - - # 记录失败模式 - if sf: - signal_fails[sf] += 1 - if ef: - exec_fails[ef] += 1 - - # 逐条摘要 - tags = [] - if overall in ("盈利", "触止盈"): - tags.append("✅") - elif overall == "触止损": - tags.append("❌") - else: - tags.append("⏳") - tags.append(f"信号:{sv}") - tags.append(f"执行:{ev}") - tags.append(f"整体:{overall}") - detail_lines.append(f" {' | '.join(tags)} {name}({code})") - - # 写入accuracy_stats - conn.execute( - "INSERT OR REPLACE INTO accuracy_stats (id, total_advice, correct, wrong, partial, pending, " - "accuracy_pct, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)", - (stats["total"], stats["correct"], stats["wrong"], - stats["mixed"], stats["pending"], - round(stats["correct"] / max(stats["total"] - stats["pending"], 1) * 100, 1), - datetime.now().isoformat())) - conn.commit() - conn.close() - - # 输出 - total_eval = stats["total"] - stats["pending"] - accuracy = stats["correct"] / max(total_eval, 1) * 100 - - print(f"策略复盘 | {datetime.now().strftime('%Y-%m-%d')} | {stats['total']}条 | ({time.time()-start:.0f}s)") - print(f" ✅正确 {stats['correct']} | ❌错误 {stats['wrong']} | ⚠️部分 {stats['mixed']} | ⏳待定 {stats['pending']}") - print(f" 综合准确率: {accuracy:.1f}%") - - if signal_fails: - print(f"\n📡 信号层失败模式:") - for mode, cnt in signal_fails.most_common(): - info = SIGNAL_FAILURES.get(mode, {}) - print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") - - if exec_fails: - print(f"\n🎯 执行层失败模式:") - for mode, cnt in exec_fails.most_common(): - info = EXEC_FAILURES.get(mode, {}) - print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") - - if detail_lines: - print(f"\n逐条复盘:") - for line in detail_lines: - print(line) - - -if __name__ == "__main__": - review() +#!/usr/bin/env python3 +"""strategy_review.py — 三层策略复盘 (no_agent) + +每层独立评估: +1. 信号层 — 买入/卖出/持有的timing对不对? +2. 执行层 — 止损/止盈设得合理吗? +3. 综合层 — 这波操作整体赚钱了吗? + +用法: + python3 scripts/strategy_review.py +""" + +import json, sqlite3, sys, time +from pathlib import Path +from datetime import datetime +from collections import Counter +from mo_data import read_portfolio, read_decisions, read_watchlist, get_price, get_prices_batch + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +BASE = Path("/home/hmo/MoFin") +DATA = BASE / "data" +DB_PATH = DATA / "mofin.db" + +# 失败模式定义(执行层) +EXEC_FAILURES = { + "stop_too_tight": {"label": "止损过紧", "fix": "放宽止损到强支撑×0.95,给价格波动留空间"}, + "tp_too_close": {"label": "止盈过近", "fix": "止盈放到更高阻力位,让利润奔跑"}, + "stop_too_loose": {"label": "止损过宽", "fix": "收紧止损,少亏当赢"}, + "tp_too_far": {"label": "止盈过远", "fix": "止盈靠近合理阻力位,提高兑现概率"}, +} + +# 失败模式定义(信号层) +SIGNAL_FAILURES = { + "wrong_direction": {"label": "方向看反", "fix": "检查多周期趋势判断逻辑"}, + "entry_too_early": {"label": "入场过早", "fix": "等缩量确认支撑再入,不追回调"}, + "bad_signal": {"label": "信号误判", "fix": "修正timing_signal合成权重"}, + "regime_mismatch": {"label": "情景错配", "fix": "加入市场情景过滤条件"}, +} + + +def fetch_price(code): + # DB 优先 + try: from mofin_db import get_price_from_db; p, _ = get_price_from_db(code); return p if p else 0 + except: pass + # Fallback: mo_data.get_price + try: + p, _ = get_price(code) + return p if p else 0 + except: + return 0 + + +def evaluate_strategy(s, price): + """三层评估单条策略,返回 (signal_verdict, exec_verdict, overall_verdict, detail)""" + code = s.get("code", "") + name = s.get("name", "") + sl = s.get("stop_loss", 0) or 0 + tp = s.get("take_profit", 0) or 0 + entry_low = s.get("entry_low", 0) or 0 + entry_high = s.get("entry_high", 0) or 0 + cost = s.get("cost", 0) or s.get("avg_price", 0) or 0 + signal = (s.get("timing_signal", "") or s.get("current", "") or "").lower() + created = s.get("created_at", "") or s.get("timestamp", "") + s_type = s.get("type", "") # 持仓策略/自选策略 + + if not created or not price: + return "skip", "skip", "skip", "数据不足", "" + + # 计算运行天数 + try: + days = (datetime.now() - datetime.fromisoformat(created)).days + except: + days = 0 + + # ─── 综合层:赚钱了吗? ─── + if cost > 0 and s_type == "持仓策略": + profit_pct = (price - cost) / cost * 100 + if profit_pct > 5: + overall = "盈利" + elif profit_pct > -5: + overall = "持平" + else: + overall = f"亏损{profit_pct:.0f}%" + elif tp > 0 and price >= tp: + overall = "触止盈" + elif sl > 0 and price <= sl: + overall = "触止损" + else: + overall = "持有中" + + # ─── 信号层:timing对不对? ─── + is_buy_signal = any(kw in signal for kw in ["买入", "加仓", "追涨", "可买"]) + is_sell_signal = any(kw in signal for kw in ["卖出", "减仓", "止损", "离场"]) + is_hold_signal = any(kw in signal for kw in ["持有", "观望", "等待", "持股"]) + + signal_verdict = "待定" + signal_fail = None + + if is_buy_signal or is_hold_signal: + if sl > 0 and price <= sl: + # 买入/持有信号下触发止损 → 信号方向可能错了 + signal_verdict = "存疑" + signal_fail = "wrong_direction" + elif tp > 0 and price >= tp * 0.95: + signal_verdict = "正确" + elif entry_low > 0 and price < entry_low * 0.85: + signal_verdict = "存疑" + signal_fail = "entry_too_early" + elif days > 30 and tp > 0 and price < entry_low: + signal_verdict = "存疑" + signal_fail = "wrong_direction" + else: + signal_verdict = "待定" + elif is_sell_signal: + if sl > 0 and price <= sl: + signal_verdict = "正确" + elif price > (cost or entry_low or 0) * 1.05: + signal_verdict = "存疑" + signal_fail = "bad_signal" + else: + signal_verdict = "待定" + else: + # 无明确信号 + if price > (entry_high or 0): + signal_verdict = "待定(价涨)" + elif sl > 0 and price <= sl * 1.05: + signal_verdict = "待定(近止损)" + else: + signal_verdict = "待定" + + # ─── 执行层:止损/止盈设得好不好? ─── + exec_verdict = "待定" + exec_fail = None + + # 取近期最高/最低价(判断卖飞/洗盘) + recent_high = 0 + recent_low = 0 + sl_recovery = False + if tp > 0 or sl > 0: + try: + prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" + url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={prefix}{code},day,,,60,qfq" + import subprocess as sp + r = sp.run(["curl", "-s", "--max-time", "3", url], capture_output=True, text=True, timeout=5) + if r.returncode == 0 and r.stdout: + data = json.loads(r.stdout) + day_key = 'qfqday' if prefix != 'hk' else 'day' + bars = data.get('data', {}).get(f'{prefix}{code}', {}).get(day_key, []) + if bars: + prices = [(float(b[2]), float(b[3]), b[0]) for b in bars if len(b) > 3] # (high, low, date) + recent_high = max(p[0] for p in prices) + recent_low = min(p[1] for p in prices) + # 检查止损触发后的走势:是否后来反弹了? + if sl > 0: + # 找出价格低于SL的K线 + below_sl = [p for p in prices if p[1] <= sl] + above_sl_later = [p for p in prices if p[1] > sl * 1.03] + if below_sl and above_sl_later: + # 曾跌破SL,但后来涨回去了 → 洗盘 + first_below = min(below_sl, key=lambda x: x[2]) + last_above = max(above_sl_later, key=lambda x: x[2]) + if last_above[2] > first_below[2]: + sl_recovery = True + except: + pass + + if sl > 0 and price <= sl: + if sl_recovery: + exec_verdict = "洗盘(触发后反弹)" + exec_fail = "stop_too_tight" + elif price >= sl * 0.95: + exec_verdict = "临界(差一点触发)" + exec_fail = "stop_too_tight" + else: + exec_verdict = "已触发" + elif tp > 0 and (price >= tp or recent_high >= tp): + # 止盈触发或曾触发过 + max_price = max(price, recent_high) + if max_price <= tp * 1.05: + exec_verdict = "已触发" + else: + overshoot = (max_price - tp) / tp * 100 + exec_verdict = f"卖飞({overshoot:.0f}%)" + exec_fail = "tp_too_close" + elif days > 45 and tp > 0 and price < entry_low: + exec_verdict = "存疑(久未达标)" + exec_fail = "tp_too_far" + elif sl > 0 and price >= entry_low and price <= entry_high: + exec_verdict = "持有中" + else: + exec_verdict = "待定" + + return signal_verdict, exec_verdict, overall, signal_fail, exec_fail + + +def review(): + start = time.time() + decisions = read_decisions() + strategies = decisions.get("decisions", []) + + conn = sqlite3.connect(str(DB_PATH)) + + stats = {"correct": 0, "wrong": 0, "mixed": 0, "pending": 0, "total": 0} + signal_fails = Counter() + exec_fails = Counter() + detail_lines = [] + + for s in strategies: + if s.get("status") == "closed": + continue + stats["total"] += 1 + code = s.get("code", "") + name = s.get("name", "") + price = fetch_price(code) + if not price: + detail_lines.append(f" ⏭️ {name}({code}): 无行情") + stats["pending"] += 1 + continue + + sv, ev, overall, sf, ef = evaluate_strategy(s, price) + + # 综合评级 + if overall in ("盈利", "触止盈"): + if sv == "正确" or "存疑" not in sv: + stats["correct"] += 1 + else: + stats["mixed"] += 1 + elif overall in ("触止损",) and "存疑" in sv: + stats["wrong"] += 1 + elif "存疑" in sv or "存疑" in ev: + stats["wrong"] += 1 + elif overall in ("持有中", "持平"): + stats["mixed"] += 1 + else: + stats["pending"] += 1 + + # 记录失败模式 + if sf: + signal_fails[sf] += 1 + if ef: + exec_fails[ef] += 1 + + # 逐条摘要 + tags = [] + if overall in ("盈利", "触止盈"): + tags.append("✅") + elif overall == "触止损": + tags.append("❌") + else: + tags.append("⏳") + tags.append(f"信号:{sv}") + tags.append(f"执行:{ev}") + tags.append(f"整体:{overall}") + detail_lines.append(f" {' | '.join(tags)} {name}({code})") + + # 写入accuracy_stats + conn.execute( + "INSERT OR REPLACE INTO accuracy_stats (id, total_advice, correct, wrong, partial, pending, " + "accuracy_pct, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)", + (stats["total"], stats["correct"], stats["wrong"], + stats["mixed"], stats["pending"], + round(stats["correct"] / max(stats["total"] - stats["pending"], 1) * 100, 1), + datetime.now().isoformat())) + conn.commit() + conn.close() + + # 输出 + total_eval = stats["total"] - stats["pending"] + accuracy = stats["correct"] / max(total_eval, 1) * 100 + + print(f"策略复盘 | {datetime.now().strftime('%Y-%m-%d')} | {stats['total']}条 | ({time.time()-start:.0f}s)") + print(f" ✅正确 {stats['correct']} | ❌错误 {stats['wrong']} | ⚠️部分 {stats['mixed']} | ⏳待定 {stats['pending']}") + print(f" 综合准确率: {accuracy:.1f}%") + + if signal_fails: + print(f"\n📡 信号层失败模式:") + for mode, cnt in signal_fails.most_common(): + info = SIGNAL_FAILURES.get(mode, {}) + print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") + + if exec_fails: + print(f"\n🎯 执行层失败模式:") + for mode, cnt in exec_fails.most_common(): + info = EXEC_FAILURES.get(mode, {}) + print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") + + if detail_lines: + print(f"\n逐条复盘:") + for line in detail_lines: + print(line) + + +if __name__ == "__main__": + review() diff --git a/deploy/profile-scripts/strategy_router.py b/deploy/profile-scripts/strategy_router.py index 082fdf14..e750706b 100644 --- a/deploy/profile-scripts/strategy_router.py +++ b/deploy/profile-scripts/strategy_router.py @@ -19,6 +19,13 @@ import sys from pathlib import Path from datetime import datetime +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + _SCRIPT_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(_SCRIPT_DIR)) sys.path.insert(0, "/home/hmo/MoFin") diff --git a/deploy/profile-scripts/system_audit.py b/deploy/profile-scripts/system_audit.py index 169880eb..d9b79524 100644 --- a/deploy/profile-scripts/system_audit.py +++ b/deploy/profile-scripts/system_audit.py @@ -1,295 +1,302 @@ -#!/usr/bin/env python3 -"""system_audit.py — MoFin 全局系统审计 - -每日收盘后运行,遍历所有对象生命周期,发现缺口→自动修复/记录。 - -审计维度: - 1. 信号管道 — 今日signal_news产出vs处理量,有积压则预警 - 2. 股票生命周期 — 关注列表是否有条件触发的、自选是否有策略缺失的 - 3. 策略状态 — 过期/偏离/无止损等异常策略 - 4. 建议闭环 — pending超过7天的未执行建议 - 5. 组合健康 — 弱势占比、仓位集中度、现金水位 - 6. 数据管道 — 今日采集是否正常、有无cron报错 - 7. 系统服务 — Dashboard/XMPP/小果API在线状态 - -输出:JSON + 摘要文本,推送给老爸。 -""" - -import json, sqlite3, subprocess, sys, time -from pathlib import Path -from datetime import datetime, timedelta -from mo_data import read_portfolio, read_decisions, read_watchlist - -DATA_DIR = Path("/home/hmo/MoFin/data") -WEB_DATA = Path("/home/hmo/web-dashboard/data") -REPORT = {"timestamp": datetime.now().isoformat(), "issues": [], "fixes": [], "ok": []} - - -def log_issue(area, severity, desc, fix=None): - REPORT["issues"].append({"area": area, "severity": severity, "desc": desc, "suggested_fix": fix}) - - -def log_fix(area, desc): - REPORT["fixes"].append({"area": area, "desc": desc}) - - -def log_ok(area, desc): - REPORT["ok"].append({"area": area, "desc": desc}) - - -# ── 1. 信号管道审计 ── -def audit_signals(conn): - try: - total = conn.execute("SELECT COUNT(*) FROM signal_news").fetchone()[0] - unproc = conn.execute("SELECT COUNT(*) FROM signal_news WHERE source LIKE 'xiaoguo%' AND (processed=0 OR processed IS NULL) AND created_at > datetime('now', '-4 hours')").fetchone()[0] - total_unproc = conn.execute("SELECT COUNT(*) FROM signal_news WHERE (processed=0 OR processed IS NULL)").fetchone()[0] - today = conn.execute("SELECT COUNT(*) FROM signal_news WHERE created_at > datetime('now','-1 day')").fetchone()[0] - log_ok("信号管道", f"信号库{total}条,今日{today}条,未处理{total_unproc}条(xiaoguo={unproc})") - if unproc > 30: - log_issue("信号管道", "HIGH", f"xiaoguo信号堆积{unproc}条,可能处理速度跟不上") - # 检查其他来源信号积压(无consumer的信号源) - other = total_unproc - unproc - if other > 50: - log_issue("信号管道", "MEDIUM", f"其它来源信号积压{other}条(divergence_watch/trend等,可能无consumer)") - except Exception as e: - log_issue("信号管道", "HIGH", f"查询失败: {e}") - - -# ── 2. 股票生命周期审计 ── -def audit_stocks(conn): - # 关注列表 - try: - wl = read_watchlist() - watching = [s for s in wl.get("stocks", []) if s.get("status") == "watching"] - formal = [s for s in wl.get("stocks", []) if s.get("status") != "watching"] - log_ok("股票池", f"正式自选{len(formal)}只, 关注列表{len(watching)}只") - - # 检查持仓中是否有已关闭但未标记的 - closed_holdings = conn.execute("SELECT COUNT(*) FROM holdings WHERE is_active=0").fetchone()[0] - active_holdings = conn.execute("SELECT COUNT(*) FROM holdings WHERE is_active=1").fetchone()[0] - if closed_holdings > 0: - log_ok("股票池", f"持有中{active_holdings}只活跃, {closed_holdings}只已关闭") - except Exception as e: - log_issue("股票池", "MEDIUM", f"查询失败: {e}") - - -# ── 3. 策略状态审计 ── -def audit_strategies(conn): - try: - dec = read_decisions() - active = [d for d in dec.get("decisions", []) if d.get("status") in ("active", "updated")] - stale_count = 0 - no_stop = 0 - for d in active: - # 检查是否有止损 - if not d.get("stop_loss"): - no_stop += 1 - # 检查是否过期(>14天) - ts = d.get("timestamp", "") - if ts: - try: - dt = datetime.fromisoformat(ts) - if (datetime.now() - dt).days > 14: - stale_count += 1 - except: - pass - log_ok("策略", f"活跃策略{len(active)}条") - if stale_count > 0: - log_issue("策略", "MEDIUM", f"{stale_count}条策略超过14天未更新", "运行 stale_detector 触发重评") - if no_stop > 0: - log_issue("策略", "HIGH", f"{no_stop}条活跃策略缺少止损位") - except Exception as e: - log_issue("策略", "HIGH", f"查询失败: {e}") - - -# ── 4. 建议闭环审计 ── -def audit_advice(conn): - try: - dec = read_decisions() - pending = 0 - for d in dec.get("decisions", []): - for a in d.get("advice_timeline", []): - if a.get("status") == "pending": - pending += 1 - if pending > 0: - log_issue("建议", "LOW", f"{pending}条建议待确认/执行", "检查advice_timeline确认是否已执行") - else: - log_ok("建议", "无待处理建议") - except Exception as e: - log_issue("建议", "MEDIUM", f"查询失败: {e}") - - -# ── 5. 组合健康 ── -def audit_portfolio(conn): - try: - pj = read_portfolio() - pos = pj.get("position_pct", 0) - cash = pj.get("cash", 0) - available = pj.get("available_cash", cash) - - log_ok("组合", f"总仓位{pos:.1f}%") - if pos > 90: - log_issue("组合", "MEDIUM", f"仓位{pos:.1f}%超过90%,现金紧张") - elif pos < 30: - log_issue("组合", "LOW", f"仓位仅{pos:.1f}%,现金过多") - except Exception as e: - log_issue("组合", "MEDIUM", f"查询失败: {e}") - - -# ── 8. 编译缓存审计 ── -def audit_cache(): - """检查 __pycache__ 中是否有比 .py 源文件更老的 .pyc(陈旧缓存)。""" - try: - base = Path(__file__).resolve().parent - stale = [] - for pyc in base.rglob("__pycache__/*.pyc"): - py = pyc.with_suffix("") # remove .cpython-*.pyc extension - # The .py file is at parent_of___pycache__ / stem_without_cpython_suffix - # e.g., __pycache__/foo.cpython-312.pyc -> ../foo.py - stem = pyc.stem # e.g. "foo.cpython-312" - # Remove the .cpython-NNN suffix to get original module name - import re - m = re.match(r"^(.*?)\.cpython-\d+", stem) - if not m: - continue - py_path = pyc.parent.parent / f"{m.group(1)}.py" - if py_path.exists() and pyc.stat().st_mtime < py_path.stat().st_mtime: - stale.append(str(py_path.name)) - if stale: - log_issue("编译缓存", "MEDIUM", f"{len(stale)}个陈旧.pyc:{', '.join(stale)}", "删除对应__pycache__/.pyc") - else: - log_ok("编译缓存", "所有.pyc文件与源文件一致") - except Exception as e: - log_issue("编译缓存", "LOW", f"检查失败: {e}") - - -# ── 6. 数据管道审计(端到端,逐条trace) ── -def audit_pipeline(): - """遍历所有关键数据管道,检查生产者→存储→消费者链路是否完整""" - today = datetime.now().strftime("%Y-%m-%d") - conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) - - pipelines = [ - # 管道名, 生产者, 存储位置, 检查SQL/文件, 新鲜度阈值(天) - ("价格数据", "price_monitor(每2分)", "live_prices.updated_at", - "SELECT MAX(updated_at) FROM live_prices", 0.02), # 30分钟内 - ("宏观上下文", "refresh_macro_context(每30分)", "macro_context_log.created_at", - "SELECT MAX(created_at) FROM macro_context_log", 1), # 1天内 - ("市场快照", "market_watch(每10分)", "market_snapshots.created_at", - "SELECT MAX(created_at) FROM market_snapshots", 1), - ("策略评估", "reassess_with_context", "holding_strategies.updated_at(活跃策略)", - "SELECT MAX(updated_at) FROM holding_strategies WHERE status='active'", 2), - ("原始新闻", "macro_context_collector", "macro_raw_news.fetched_at", - "SELECT MAX(fetched_at) FROM macro_raw_news", 1), - ("风险信号", "macro_context_collector", "signal_news.created_at", - "SELECT MAX(created_at) FROM signal_news", 2), - ] - - for name, producer, storage, sql, max_days in pipelines: - try: - row = conn.execute(sql).fetchone() - if not row or not row[0]: - # 特殊处理:收盘后价格数据停止更新是正常行为 - if name == "价格数据": - now = datetime.now() - # 如果是非交易时段(9:30-15:00之外),不做无数据告警 - if now.hour < 9 or now.hour >= 15 or (now.hour == 9 and now.minute < 30): - log_ok("数据管道", f"{name} 收盘后正常停止 → OK (最后已更新)") - continue - log_issue("数据管道", "HIGH", f"{name}: 无数据 ({producer}→{storage})", - fix=f"检查{producer}是否正确运行") - continue - latest = row[0][:19] if len(row[0]) > 19 else row[0] - try: - dt = datetime.fromisoformat(latest) if isinstance(latest, str) else latest - days_old = (datetime.now() - dt).total_seconds() / 86400 - except: - days_old = 999 - if days_old > max_days: - # 收盘后价格数据停止更新是正常行为,放松阈值 - if name == "价格数据": - now = datetime.now() - if now.hour < 9 or now.hour >= 15 or (now.hour == 9 and now.minute < 30): - log_ok("数据管道", f"{name} {days_old*24:.0f}小时前更新(收盘后正常) → OK") - continue - log_issue("数据管道", "HIGH", - f"{name}: {days_old:.0f}天未更新(阈值{max_days}天) 最后{latest} ({producer}→{storage})", - fix=f"检查{producer}输出和{storage}写入逻辑") - else: - log_ok("数据管道", f"{name} {days_old*24:.0f}小时前更新 → OK") - except Exception as e: - log_issue("数据管道", "HIGH", f"{name} 检查失败: {e}") - - # 检查 holding_strategies 表策略数量 - try: - hs_count = conn.execute("SELECT COUNT(*) FROM holding_strategies WHERE status IN ('active','updated')").fetchone()[0] - if hs_count < 5: - log_issue("数据管道", "HIGH", f"holding_strategies 仅{hs_count}条策略(异常)", fix="检查策略写入逻辑") - else: - log_ok("数据管道", f"holding_strategies {hs_count}条策略") - except Exception as e: - log_issue("数据管道", "HIGH", f"holding_strategies检查失败: {e}") - - conn.close() - -def audit_services(): - services = [ - ("Dashboard", "http://127.0.0.1:8899/", "200"), - ("mofin-dashboard", None, "active"), - ("xmpp-zhiwei", None, "active"), - ] - for name, url, expected in services: - try: - if url: - result = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", url], - capture_output=True, text=True, timeout=5) - if result.stdout.strip() == expected: - log_ok("系统服务", f"{name} 正常") - else: - log_issue("系统服务", "HIGH", f"{name} 返回 {result.stdout.strip()} (期望{expected})") - else: - result = subprocess.run(["systemctl", "is-active", name], - capture_output=True, text=True, timeout=5) - if result.stdout.strip() == expected: - log_ok("系统服务", f"{name} 正常") - else: - log_issue("系统服务", "HIGH", f"{name} 状态 {result.stdout.strip()} (期望{expected})") - except Exception as e: - log_issue("系统服务", "HIGH", f"{name} 检查失败: {e}") - - -# ── 执行 ── -def main(): - start = time.time() - conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) - - audit_signals(conn) - audit_stocks(conn) - audit_strategies(conn) - audit_advice(conn) - audit_portfolio(conn) - audit_pipeline() - audit_services() - audit_cache() - - conn.close() - - REPORT["duration"] = f"{time.time()-start:.0f}s" - REPORT["summary"] = f"审计完成: {len(REPORT['issues'])}个问题, {len(REPORT['fixes'])}个已修复, {len(REPORT['ok'])}项正常" - - # 写入文件 - (WEB_DATA / "system_audit_report.json").write_text(json.dumps(REPORT, ensure_ascii=False, indent=2)) - - # 输出摘要(给cron推送用) - print(f"【系统审计】{REPORT['summary']}") - for i in REPORT["issues"]: - print(f" [{i['severity']}] {i['area']}: {i['desc']}") - if REPORT["fixes"]: - for f in REPORT["fixes"]: - print(f" ✅ 已修复: {f['area']}: {f['desc']}") - for o in REPORT["ok"]: - print(f" ✅ {o['area']}: {o['desc']}") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""system_audit.py — MoFin 全局系统审计 + +每日收盘后运行,遍历所有对象生命周期,发现缺口→自动修复/记录。 + +审计维度: + 1. 信号管道 — 今日signal_news产出vs处理量,有积压则预警 + 2. 股票生命周期 — 关注列表是否有条件触发的、自选是否有策略缺失的 + 3. 策略状态 — 过期/偏离/无止损等异常策略 + 4. 建议闭环 — pending超过7天的未执行建议 + 5. 组合健康 — 弱势占比、仓位集中度、现金水位 + 6. 数据管道 — 今日采集是否正常、有无cron报错 + 7. 系统服务 — Dashboard/XMPP/小果API在线状态 + +输出:JSON + 摘要文本,推送给老爸。 +""" + +import json, sqlite3, subprocess, sys, time +from pathlib import Path +from datetime import datetime, timedelta +from mo_data import read_portfolio, read_decisions, read_watchlist + +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + +DATA_DIR = Path("/home/hmo/MoFin/data") +WEB_DATA = Path("/home/hmo/web-dashboard/data") +REPORT = {"timestamp": datetime.now().isoformat(), "issues": [], "fixes": [], "ok": []} + + +def log_issue(area, severity, desc, fix=None): + REPORT["issues"].append({"area": area, "severity": severity, "desc": desc, "suggested_fix": fix}) + + +def log_fix(area, desc): + REPORT["fixes"].append({"area": area, "desc": desc}) + + +def log_ok(area, desc): + REPORT["ok"].append({"area": area, "desc": desc}) + + +# ── 1. 信号管道审计 ── +def audit_signals(conn): + try: + total = conn.execute("SELECT COUNT(*) FROM signal_news").fetchone()[0] + unproc = conn.execute("SELECT COUNT(*) FROM signal_news WHERE source LIKE 'xiaoguo%' AND (processed=0 OR processed IS NULL) AND created_at > datetime('now', '-4 hours')").fetchone()[0] + total_unproc = conn.execute("SELECT COUNT(*) FROM signal_news WHERE (processed=0 OR processed IS NULL)").fetchone()[0] + today = conn.execute("SELECT COUNT(*) FROM signal_news WHERE created_at > datetime('now','-1 day')").fetchone()[0] + log_ok("信号管道", f"信号库{total}条,今日{today}条,未处理{total_unproc}条(xiaoguo={unproc})") + if unproc > 30: + log_issue("信号管道", "HIGH", f"xiaoguo信号堆积{unproc}条,可能处理速度跟不上") + # 检查其他来源信号积压(无consumer的信号源) + other = total_unproc - unproc + if other > 50: + log_issue("信号管道", "MEDIUM", f"其它来源信号积压{other}条(divergence_watch/trend等,可能无consumer)") + except Exception as e: + log_issue("信号管道", "HIGH", f"查询失败: {e}") + + +# ── 2. 股票生命周期审计 ── +def audit_stocks(conn): + # 关注列表 + try: + wl = read_watchlist() + watching = [s for s in wl.get("stocks", []) if s.get("status") == "watching"] + formal = [s for s in wl.get("stocks", []) if s.get("status") != "watching"] + log_ok("股票池", f"正式自选{len(formal)}只, 关注列表{len(watching)}只") + + # 检查持仓中是否有已关闭但未标记的 + closed_holdings = conn.execute("SELECT COUNT(*) FROM holdings WHERE is_active=0").fetchone()[0] + active_holdings = conn.execute("SELECT COUNT(*) FROM holdings WHERE is_active=1").fetchone()[0] + if closed_holdings > 0: + log_ok("股票池", f"持有中{active_holdings}只活跃, {closed_holdings}只已关闭") + except Exception as e: + log_issue("股票池", "MEDIUM", f"查询失败: {e}") + + +# ── 3. 策略状态审计 ── +def audit_strategies(conn): + try: + dec = read_decisions() + active = [d for d in dec.get("decisions", []) if d.get("status") in ("active", "updated")] + stale_count = 0 + no_stop = 0 + for d in active: + # 检查是否有止损 + if not d.get("stop_loss"): + no_stop += 1 + # 检查是否过期(>14天) + ts = d.get("timestamp", "") + if ts: + try: + dt = datetime.fromisoformat(ts) + if (datetime.now() - dt).days > 14: + stale_count += 1 + except: + pass + log_ok("策略", f"活跃策略{len(active)}条") + if stale_count > 0: + log_issue("策略", "MEDIUM", f"{stale_count}条策略超过14天未更新", "运行 stale_detector 触发重评") + if no_stop > 0: + log_issue("策略", "HIGH", f"{no_stop}条活跃策略缺少止损位") + except Exception as e: + log_issue("策略", "HIGH", f"查询失败: {e}") + + +# ── 4. 建议闭环审计 ── +def audit_advice(conn): + try: + dec = read_decisions() + pending = 0 + for d in dec.get("decisions", []): + for a in d.get("advice_timeline", []): + if a.get("status") == "pending": + pending += 1 + if pending > 0: + log_issue("建议", "LOW", f"{pending}条建议待确认/执行", "检查advice_timeline确认是否已执行") + else: + log_ok("建议", "无待处理建议") + except Exception as e: + log_issue("建议", "MEDIUM", f"查询失败: {e}") + + +# ── 5. 组合健康 ── +def audit_portfolio(conn): + try: + pj = read_portfolio() + pos = pj.get("position_pct", 0) + cash = pj.get("cash", 0) + available = pj.get("available_cash", cash) + + log_ok("组合", f"总仓位{pos:.1f}%") + if pos > 90: + log_issue("组合", "MEDIUM", f"仓位{pos:.1f}%超过90%,现金紧张") + elif pos < 30: + log_issue("组合", "LOW", f"仓位仅{pos:.1f}%,现金过多") + except Exception as e: + log_issue("组合", "MEDIUM", f"查询失败: {e}") + + +# ── 8. 编译缓存审计 ── +def audit_cache(): + """检查 __pycache__ 中是否有比 .py 源文件更老的 .pyc(陈旧缓存)。""" + try: + base = Path(__file__).resolve().parent + stale = [] + for pyc in base.rglob("__pycache__/*.pyc"): + py = pyc.with_suffix("") # remove .cpython-*.pyc extension + # The .py file is at parent_of___pycache__ / stem_without_cpython_suffix + # e.g., __pycache__/foo.cpython-312.pyc -> ../foo.py + stem = pyc.stem # e.g. "foo.cpython-312" + # Remove the .cpython-NNN suffix to get original module name + import re + m = re.match(r"^(.*?)\.cpython-\d+", stem) + if not m: + continue + py_path = pyc.parent.parent / f"{m.group(1)}.py" + if py_path.exists() and pyc.stat().st_mtime < py_path.stat().st_mtime: + stale.append(str(py_path.name)) + if stale: + log_issue("编译缓存", "MEDIUM", f"{len(stale)}个陈旧.pyc:{', '.join(stale)}", "删除对应__pycache__/.pyc") + else: + log_ok("编译缓存", "所有.pyc文件与源文件一致") + except Exception as e: + log_issue("编译缓存", "LOW", f"检查失败: {e}") + + +# ── 6. 数据管道审计(端到端,逐条trace) ── +def audit_pipeline(): + """遍历所有关键数据管道,检查生产者→存储→消费者链路是否完整""" + today = datetime.now().strftime("%Y-%m-%d") + conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) + + pipelines = [ + # 管道名, 生产者, 存储位置, 检查SQL/文件, 新鲜度阈值(天) + ("价格数据", "price_monitor(每2分)", "live_prices.updated_at", + "SELECT MAX(updated_at) FROM live_prices", 0.02), # 30分钟内 + ("宏观上下文", "refresh_macro_context(每30分)", "macro_context_log.created_at", + "SELECT MAX(created_at) FROM macro_context_log", 1), # 1天内 + ("市场快照", "market_watch(每10分)", "market_snapshots.created_at", + "SELECT MAX(created_at) FROM market_snapshots", 1), + ("策略评估", "reassess_with_context", "holding_strategies.updated_at(活跃策略)", + "SELECT MAX(updated_at) FROM holding_strategies WHERE status='active'", 2), + ("原始新闻", "macro_context_collector", "macro_raw_news.fetched_at", + "SELECT MAX(fetched_at) FROM macro_raw_news", 1), + ("风险信号", "macro_context_collector", "signal_news.created_at", + "SELECT MAX(created_at) FROM signal_news", 2), + ] + + for name, producer, storage, sql, max_days in pipelines: + try: + row = conn.execute(sql).fetchone() + if not row or not row[0]: + # 特殊处理:收盘后价格数据停止更新是正常行为 + if name == "价格数据": + now = datetime.now() + # 如果是非交易时段(9:30-15:00之外),不做无数据告警 + if now.hour < 9 or now.hour >= 15 or (now.hour == 9 and now.minute < 30): + log_ok("数据管道", f"{name} 收盘后正常停止 → OK (最后已更新)") + continue + log_issue("数据管道", "HIGH", f"{name}: 无数据 ({producer}→{storage})", + fix=f"检查{producer}是否正确运行") + continue + latest = row[0][:19] if len(row[0]) > 19 else row[0] + try: + dt = datetime.fromisoformat(latest) if isinstance(latest, str) else latest + days_old = (datetime.now() - dt).total_seconds() / 86400 + except: + days_old = 999 + if days_old > max_days: + # 收盘后价格数据停止更新是正常行为,放松阈值 + if name == "价格数据": + now = datetime.now() + if now.hour < 9 or now.hour >= 15 or (now.hour == 9 and now.minute < 30): + log_ok("数据管道", f"{name} {days_old*24:.0f}小时前更新(收盘后正常) → OK") + continue + log_issue("数据管道", "HIGH", + f"{name}: {days_old:.0f}天未更新(阈值{max_days}天) 最后{latest} ({producer}→{storage})", + fix=f"检查{producer}输出和{storage}写入逻辑") + else: + log_ok("数据管道", f"{name} {days_old*24:.0f}小时前更新 → OK") + except Exception as e: + log_issue("数据管道", "HIGH", f"{name} 检查失败: {e}") + + # 检查 holding_strategies 表策略数量 + try: + hs_count = conn.execute("SELECT COUNT(*) FROM holding_strategies WHERE status IN ('active','updated')").fetchone()[0] + if hs_count < 5: + log_issue("数据管道", "HIGH", f"holding_strategies 仅{hs_count}条策略(异常)", fix="检查策略写入逻辑") + else: + log_ok("数据管道", f"holding_strategies {hs_count}条策略") + except Exception as e: + log_issue("数据管道", "HIGH", f"holding_strategies检查失败: {e}") + + conn.close() + +def audit_services(): + services = [ + ("Dashboard", "http://127.0.0.1:8899/", "200"), + ("mofin-dashboard", None, "active"), + ("xmpp-zhiwei", None, "active"), + ] + for name, url, expected in services: + try: + if url: + result = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", url], + capture_output=True, text=True, timeout=5) + if result.stdout.strip() == expected: + log_ok("系统服务", f"{name} 正常") + else: + log_issue("系统服务", "HIGH", f"{name} 返回 {result.stdout.strip()} (期望{expected})") + else: + result = subprocess.run(["systemctl", "is-active", name], + capture_output=True, text=True, timeout=5) + if result.stdout.strip() == expected: + log_ok("系统服务", f"{name} 正常") + else: + log_issue("系统服务", "HIGH", f"{name} 状态 {result.stdout.strip()} (期望{expected})") + except Exception as e: + log_issue("系统服务", "HIGH", f"{name} 检查失败: {e}") + + +# ── 执行 ── +def main(): + start = time.time() + conn = sqlite3.connect(str(DATA_DIR / "mofin.db")) + + audit_signals(conn) + audit_stocks(conn) + audit_strategies(conn) + audit_advice(conn) + audit_portfolio(conn) + audit_pipeline() + audit_services() + audit_cache() + + conn.close() + + REPORT["duration"] = f"{time.time()-start:.0f}s" + REPORT["summary"] = f"审计完成: {len(REPORT['issues'])}个问题, {len(REPORT['fixes'])}个已修复, {len(REPORT['ok'])}项正常" + + # 写入文件 + (WEB_DATA / "system_audit_report.json").write_text(json.dumps(REPORT, ensure_ascii=False, indent=2)) + + # 输出摘要(给cron推送用) + print(f"【系统审计】{REPORT['summary']}") + for i in REPORT["issues"]: + print(f" [{i['severity']}] {i['area']}: {i['desc']}") + if REPORT["fixes"]: + for f in REPORT["fixes"]: + print(f" ✅ 已修复: {f['area']}: {f['desc']}") + for o in REPORT["ok"]: + print(f" ✅ {o['area']}: {o['desc']}") + + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/system_hygiene_audit.py b/deploy/profile-scripts/system_hygiene_audit.py index fa8fd2a0..d3a28d5d 100644 --- a/deploy/profile-scripts/system_hygiene_audit.py +++ b/deploy/profile-scripts/system_hygiene_audit.py @@ -16,6 +16,13 @@ import os, sys, json, glob, hashlib, sqlite3, subprocess from datetime import datetime, timedelta from pathlib import Path +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + sys.path.insert(0, '/home/hmo/MoFin') DEPLOY = '/home/hmo/MoFin/deploy/profile-scripts' diff --git a/deploy/profile-scripts/vacuum_state_db.py b/deploy/profile-scripts/vacuum_state_db.py index 34455077..9e0eb839 100644 --- a/deploy/profile-scripts/vacuum_state_db.py +++ b/deploy/profile-scripts/vacuum_state_db.py @@ -5,6 +5,13 @@ """ import sqlite3, os +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DBS = [ "/home/hmo/.hermes/profiles/position-analyst/state.db", "/home/hmo/.hermes/state.db", diff --git a/deploy/profile-scripts/verify_reassess_pipeline.py b/deploy/profile-scripts/verify_reassess_pipeline.py index 910f32ed..04c8adfb 100644 --- a/deploy/profile-scripts/verify_reassess_pipeline.py +++ b/deploy/profile-scripts/verify_reassess_pipeline.py @@ -16,6 +16,13 @@ from pathlib import Path from datetime import datetime, timedelta from urllib.request import Request, urlopen +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + BASE = Path(__file__).parent.parent sys.path.insert(0, str(BASE)) sys.path.insert(0, "/home/hmo/MoFin") diff --git a/deploy/profile-scripts/watchlist_auto_exit.py b/deploy/profile-scripts/watchlist_auto_exit.py index 88ea4468..8e870887 100644 --- a/deploy/profile-scripts/watchlist_auto_exit.py +++ b/deploy/profile-scripts/watchlist_auto_exit.py @@ -10,6 +10,13 @@ import sqlite3, sys, json from datetime import datetime, timedelta +# ── 消息通道统一路由(broadcast/xmpp by delivery) ── +try: + from messenger import install_stdio_hook as _msh + _msh() +except Exception: + pass + DB = "/home/hmo/MoFin/data/mofin.db" def get_signal_rank(signal):