#!/usr/bin/env python3 """ generate_report.py — 模板报告生成器 v2 流程: 代码采集数据 → 构建所有数据段 → 填入模板 → 输出预填模板+参数表 用法: python3 generate_report.py LLM只写分析文本,所有数字由代码保障。 """ import sys, json, subprocess, re from pathlib import Path SCRIPTS_DIR = Path(__file__).parent TEMPLATES_DIR = SCRIPTS_DIR.parent / "templates" def get_report_data(): r = subprocess.run(["python3", str(SCRIPTS_DIR / "prepare_report_data.py")], capture_output=True, text=True, timeout=30) if r.returncode != 0: print(f"ERROR: {r.stderr}", file=sys.stderr) sys.exit(1) return json.loads(r.stdout) def fill_template(text: str, data: dict) -> str: def replacer(m): full_key = m.group(1) # 分离key和format spec if ":" in full_key: key, fmt = full_key.split(":", 1) else: key, fmt = full_key, "" parts = key.split(".") val = data try: for p in parts: val = val[p] if val is None: return "N/A" if fmt: try: return format(val, fmt) except: return str(val) if isinstance(val, float): return f"{val:.2f}" if abs(val) < 10000 else f"{val:.0f}" return str(val) except (KeyError, TypeError): return f"【缺失:{full_key}】" return re.sub(r'\{([^}]+)\}', replacer, text) def build_holdings_table(data) -> str: """生成持仓明细表""" h = data["portfolio"]["holdings"] if not h: return "(空仓)" lines = [] for stk in h: lines.append(f" {stk['code']} {stk['name']} {stk['shares']}股 {stk['price_display']} {stk['mv_display']} {stk['pnl_pct']:+.2f}%") return "\n".join(lines) def build_risk_holdings(data) -> str: """浮亏>20%的持仓""" h = data["portfolio"]["holdings"] risk = [s for s in h if s["pnl_pct"] < -20] if not risk: return "无" lines = [] for s in risk: lines.append(f"🔴 {s['code']} {s['name']} {s['pnl_pct']:+.2f}% {s['price_display']}") return "\n".join(lines) def build_cash_source(data) -> str: """现金来源追溯""" logs = data.get("cash_history", []) if not logs: return "无记录" latest = logs[0] return f"{latest['note']} (验证:{'✅' if latest['verified'] else '❌'})" def build_sections(data) -> dict: """构建所有动态段""" return { "HOLDINGS_TABLE": build_holdings_table(data), "HOLDINGS_RISK": build_risk_holdings(data), "CASH_SOURCE": build_cash_source(data), "CASH_AMOUNT": f"{data['portfolio']['cash']:.0f}", "TOTAL_ASSETS": f"{data['portfolio']['total_assets']:.0f}", "POSITION_PCT": f"{data['portfolio']['position_pct']}", "STOCK_VALUE": f"{data['portfolio']['stock_value_cny']:.0f}", "HOLDINGS_COUNT": str(data['portfolio']['holdings_count']), "GENERATED_AT": data['_meta']['generated_at'], "HK_RATE": str(data['portfolio']['hk_rate']), } def main(): report_type = sys.argv[1] if len(sys.argv) > 1 else "intraday_monitor" # ★ 前置重评:对 holding_strategies 中信号为买入/卖出的股票,先触发重评 try: import sqlite3 _conn = sqlite3.connect(str(SCRIPTS_DIR.parent / "data" / "mofin.db")) _actionable = _conn.execute( "SELECT hs.code, lp.price, hs.entry_low, hs.entry_high FROM holding_strategies hs " "LEFT JOIN live_prices lp ON hs.code = lp.code " "WHERE hs.status='active' " "AND hs.timing_signal IN ('买入','可买入','可加仓','卖出','止盈')" ).fetchall() _conn.close() for _code, _price, _el, _eh in _actionable: # 价格必须在买入区内或附近(不高于上沿20%),否则不触发重评 if _price and _el and _eh and _price > 0 and _el > 0 and _eh > 0: if _price > _eh * 1.20: print(f" ⏭️ {_code}: 价{_price}超买入区上沿+{((_price/_eh)-1)*100:.0f}%,跳过重评") continue try: subprocess.run( ["python3", str(SCRIPTS_DIR / "per_stock_reassess.py"), _code], capture_output=True, timeout=30 ) except: pass except Exception: pass data = get_report_data() sections = build_sections(data) # 读模板 template_path = TEMPLATES_DIR / f"{report_type}.txt" if not template_path.exists(): print(f"ERROR: 模板 {template_path} 不存在", file=sys.stderr) sys.exit(1) template = template_path.read_text(encoding="utf-8") # 先填大段占位符 {SECTION_NAME}(全大写),再填简单占位符 {xxx.yyy} filled = template for key, val in sections.items(): filled = filled.replace(f"{{{key}}}", val) filled = fill_template(filled, data) # 输出 print(filled) print() # 参数表附在最后供LLM参考 print("【参 | 代码采集 | LLM不得修改】") param_table = { "portfolio": {k: data["portfolio"][k] for k in ["total_assets","cash","frozen_cash","stock_value_cny","position_pct","holdings_count","hk_rate"]}, "market": {k: data["market"][k] for k in ["sh_index","sz_index","sh_change","sz_change","advance_decline_ratio","mood"]}, "cash_source": sections["CASH_SOURCE"], "data_integrity": data["_meta"]["data_integrity"], } print(json.dumps(param_table, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()