#!/usr/bin/env python3 """ pre-flight-check.py — 策略上线前检查清单的自动化部分 检查三项可自动验证的关卡,输出结构化结果供知微在报告中引用。 只检查,不拦报告。不过的项目标记 @preflight:fail。 用法: python3 pre-flight-check.py [code] 不传 code → 校验版本一致性 + 数据时效 + 全量持仓成本检查 传 code → 只检查单只股票(止损技术位 + R/R + 成本) 依赖: - /home/hmo/projects/MoFin/data/prompts/registry.json(版本一致性) - /home/hmo/web-dashboard/data/decisions.json(策略+成本) - /home/hmo/web-dashboard/data/portfolio.json(持仓数据) 输出格式: ✅ 项目名 — 通过 ❌ 项目名 — 失败原因 ⚠️ 项目名 — 警告 """ import json import sys from pathlib import Path from datetime import datetime, timezone # === 路径 === REGISTRY = Path("/home/hmo/projects/MoFin/data/prompts/registry.json") DECISIONS = Path("/home/hmo/web-dashboard/data/decisions.json") PORTFOLIO = Path("/home/hmo/web-dashboard/data/portfolio.json") # === 检查 7:版本一致性 === def check_version_consistency(): """analysis-rules.depends_on.version == strategy-generation.current_version""" try: reg = json.loads(REGISTRY.read_text()) except Exception as e: return ("⚠️ 版本一致性", f"无法读取 registry.json: {e}") sg = None ar = None for p in reg["prompts"]: if p["id"] == "strategy-generation": sg = p if p["id"] == "analysis-rules": ar = p if not sg or not ar: return ("⚠️ 版本一致性", "registry.json 中缺少 strategy-generation 或 analysis-rules") sg_ver = sg["current_version"] ar_dep_ver = ar.get("depends_on", {}).get("version", "none") if sg_ver == ar_dep_ver: return ("✅ 版本一致性", f"strategy-generation@{sg_ver} == analysis-rules.depends_on@{ar_dep_ver}") else: return ("❌ 版本一致性", f"strategy-generation@{sg_ver} != analysis-rules.depends_on@{ar_dep_ver}。" f"运行 check-prompt-deps.py 修复") # === 检查 1:数据时效 === def check_data_freshness(): """检查 decisions.json 的更新时间是否在合理范围内""" try: mtime = DECISIONS.stat().st_mtime mtime_dt = datetime.fromtimestamp(mtime, tz=timezone.utc) now = datetime.now(timezone.utc) age_minutes = (now - mtime_dt).total_seconds() / 60 except Exception as e: return ("⚠️ 数据时效", f"无法检查 decisions.json: {e}") if age_minutes < 60: return ("✅ 数据时效", f"decisions.json 更新于 {age_minutes:.0f} 分钟前 ({mtime_dt.strftime('%H:%M')})") elif age_minutes < 240: return ("⚠️ 数据时效", f"decisions.json 已 {age_minutes:.0f} 分钟未更新(可能已收盘),数据视为陈旧") else: return ("❌ 数据时效", f"decisions.json 已 {age_minutes:.0f} 分钟未更新,数据过期,请检查数据管线") # === 检查 5:成本有效 === def check_cost_validity(): """检查 decisions.json 中所有持仓的成本是否有效""" try: dec = json.loads(DECISIONS.read_text()) except Exception as e: return ("⚠️ 成本有效性", f"无法读取 decisions.json: {e}") stocks = dec.get("stocks", dec.get("holdings", dec.get("strategies", []))) if not stocks: stocks = dec.get("portfolio", []) issues = [] ok = 0 total = 0 for s in stocks: # 支持 stocks[] 和 strategies[] 两种结构 cost = s.get("cost") or s.get("actual", {}).get("cost", None) code = s.get("code") or s.get("symbol", "?") name = s.get("name", "") if cost is None or cost == 0: issues.append(f"{code} {name}: cost={'null' if cost is None else cost}") else: ok += 1 total += 1 if not issues: return ("✅ 成本有效性", f"全部 {total} 条持仓成本有效") else: return ("⚠️ 成本有效性", f"{len(issues)}/{total} 条持仓成本缺失:{','.join(issues[:5])}" f"{'...' if len(issues) > 5 else ''}") # === 检查 3(单股):止损技术依据 === def check_stop_technical(code): """检查单只股票的止损是否基于技术位(仅对单股模式生效)""" try: dec = json.loads(DECISIONS.read_text()) except Exception as e: return ("⚠️ 止损技术位", f"无法读取 decisions.json: {e}") stocks = dec.get("stocks", dec.get("strategies", [])) for s in stocks: c = s.get("code", "") if c.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", "") == code.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", ""): stop = s.get("stop_loss", s.get("actual", {}).get("stop_loss")) buy_zone = s.get("entry_low", s.get("buy_zone", [None, None])) if isinstance(buy_zone, list): entry_low = buy_zone[0] else: entry_low = buy_zone if stop and entry_low and stop < entry_low * 0.95: return ("⚠️ 止损技术位", f"{code} 止损{stop} < 买入区下沿{entry_low}×0.95," f"检查是否用了固定百分比而非技术位") return ("✅ 止损技术位", f"{code} 止损{stop},需要人工确认是否基于支撑位") return ("⚠️ 止损技术位", f"代码 {code} 未在 decisions.json 中找到") # === 检查 4(单股):R/R 达标 === def check_rr(code, price=None): """检查单只股票的 R/R 是否达标""" try: dec = json.loads(DECISIONS.read_text()) except Exception as e: return ("⚠️ R/R 达标", f"无法读取 decisions.json: {e}") stocks = dec.get("stocks", dec.get("strategies", [])) for s in stocks: c = s.get("code", "") if c.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", "") == code.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", ""): stop = s.get("stop_loss", s.get("actual", {}).get("stop_loss")) target = s.get("take_profit", s.get("actual", {}).get("take_profit")) p = price or s.get("price", s.get("current_price")) if not all([stop, target, p]): return ("⚠️ R/R 达标", f"{code} 缺少止损/止盈/现价之一") risk = p - stop reward = target - p if risk <= 0: return ("❌ R/R 达标", f"{code} 现价{p}已低于止损{stop},无R/R可言") rr = reward / risk cost = s.get("cost", s.get("actual", {}).get("cost")) is_new = (cost is None or cost == 0) min_rr = 2.5 if is_new else 2.0 # 简化处理,弱阻距检测需额外输入 if rr >= min_rr: return ("✅ R/R 达标", f"{code} R/R={rr:.2f} >= {min_rr}") else: return ("❌ R/R 达标", f"{code} R/R={rr:.2f} < {min_rr}") return ("⚠️ R/R 达标", f"代码 {code} 未在 decisions.json 中找到") def main(): print("「策略上线前检查」自动化部分") print(f"检查时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}") print() if len(sys.argv) > 1: # 单股模式 code = sys.argv[1] price = float(sys.argv[2]) if len(sys.argv) > 2 else None results = [ check_stop_technical(code), check_rr(code, price), ] else: # 全量模式 results = [ check_version_consistency(), check_data_freshness(), check_cost_validity(), ] for tag, msg in results: print(f" {tag} — {msg}") # 汇总 fails = sum(1 for tag, _ in results if tag.startswith("❌")) warns = sum(1 for tag, _ in results if tag.startswith("⚠️")) total = len(results) print() if fails == 0 and warns == 0: print(f" ✅ {total}/{total} 项通过") elif fails == 0: print(f" ⚠️ {warns} 项警告,{total - warns} 项通过") else: print(f" ❌ {fails} 项失败,{warns} 项警告,{total - fails - warns} 项通过") if __name__ == "__main__": main()