下面我直接给你一版 可落地的整合代码方案。 目标就是把你现在的 v2.0 升级成: 策略计算 → 生成计划订单 Excel → 生成执行日志 Excel → 生成偏差分析 Excel → 自动生成 Word 执行报告 而且尽量不推翻你现有 v2.0 主体,只是在后面加一个“报告与执行跟踪模块”。 一、先说整体结构 建议你新增 4 个输出文件: strategy_snapshot.xlsx holdings.xlsx planned_orders.xlsx execution_log.xlsx execution_diff.xlsx 再自动生成 1 个 Word: 量化执行报告_YYYY-MM-DD.docx 二、先装依赖 pip install pandas openpyxl python-docx 三、直接可用的代码模块 下面这段你可以单独存成: reporting_module.py 代码如下: import os from pathlib import Path from typing import Dict, List, Any, Optional import pandas as pd from docx import Document from docx.shared import Pt, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH # ========================= # 基础工具 # ========================= OUTPUT_DIR = "daily_reports" def ensure_output_dir() -> Path: path = Path(OUTPUT_DIR) path.mkdir(parents=True, exist_ok=True) return path def safe_float(x, default=0.0): try: if pd.isna(x): return default return float(x) except Exception: return default def safe_int(x, default=0): try: if pd.isna(x): return default return int(x) except Exception: return default def pct_str(x: float) -> str: return f"{x:.2f}%" def money_str(x: float) -> str: return f"{x:,.2f}" # ========================= # 1. Snapshot # ========================= def build_strategy_snapshot( date: str, strategy_version: str, regime_info: Dict[str, Any], total_value: float, actual_exposure: float, hold_count: int, is_rebalance_day: bool, summary: str = "" ) -> pd.DataFrame: row = { "date": date, "strategy_version": strategy_version, "regime": regime_info.get("final_state", ""), "regime_score": safe_float(regime_info.get("score", 0.0)), "target_position": safe_float(regime_info.get("target_exposure", 0.0)), "actual_position": safe_float(actual_exposure), "nav": safe_float(total_value), "hold_count": safe_int(hold_count), "is_rebalance_day": bool(is_rebalance_day), "summary": summary, } return pd.DataFrame([row]) # ========================= # 2. Holdings # ========================= def build_holdings_table( date: str, positions: Dict[str, int], price_df: pd.DataFrame, entry_costs: Optional[Dict[str, float]] = None, rank_map: Optional[Dict[str, int]] = None, suggested_actions: Optional[Dict[str, str]] = None, reasons: Optional[Dict[str, str]] = None ) -> pd.DataFrame: entry_costs = entry_costs or {} rank_map = rank_map or {} suggested_actions = suggested_actions or {} reasons = reasons or {} rows = [] for code, shares in positions.items(): price = safe_float(price_df.loc[date, code]) if (date in price_df.index and code in price_df.columns) else 0.0 cost = safe_float(entry_costs.get(code, 0.0)) pnl_pct = ((price / cost - 1.0) * 100.0) if cost > 0 else 0.0 rows.append({ "date": date, "code": code, "shares": safe_int(shares), "cost_price": round(cost, 2), "last_price": round(price, 2), "market_value": round(price * shares, 2), "pnl_pct": round(pnl_pct, 2), "rank": rank_map.get(code, None), "suggested_action": suggested_actions.get(code, "持有"), "reason": reasons.get(code, ""), }) if not rows: return pd.DataFrame(columns=[ "date", "code", "shares", "cost_price", "last_price", "market_value", "pnl_pct", "rank", "suggested_action", "reason" ]) return pd.DataFrame(rows) # ========================= # 3. Planned Orders # ========================= def attach_execution_zone(ref_price: float, buy_pct: float = 0.02, watch_pct: float = 0.04) -> Dict[str, Optional[float]]: if ref_price <= 0: return { "buy_zone_low": None, "buy_zone_high": None, "watch_zone_high": None, "cancel_above": None, } return { "buy_zone_low": round(ref_price, 2), "buy_zone_high": round(ref_price * (1 + buy_pct), 2), "watch_zone_high": round(ref_price * (1 + watch_pct), 2), "cancel_above": round(ref_price * (1 + watch_pct), 2), } def build_planned_orders( date: str, selected: List[tuple], positions: Dict[str, int], target_weights: Dict[str, float], total_value: float, target_exposure: float, final_state: str, price_df: pd.DataFrame, hold_count_target: int, rank_map: Optional[Dict[str, int]] = None ) -> pd.DataFrame: rank_map = rank_map or {} rows = [] selected_codes = [x[0] for x in selected] target_total_hold = total_value * target_exposure target_shares_map = {} for code, price, score in selected: desired_value = target_total_hold * target_weights.get(code, 0.0) if price > 0: target_shares = int(desired_value / price / 100) * 100 else: target_shares = 0 target_shares_map[code] = target_shares # 卖出 / 减仓 / 持有 for code, current_shares in positions.items(): ref_price = safe_float(price_df.loc[date, code]) if (date in price_df.index and code in price_df.columns) else 0.0 if code not in selected_codes: rows.append({ "date": date, "code": code, "action": "sell", "priority": "high", "current_shares": safe_int(current_shares), "target_shares": 0, "share_change": -safe_int(current_shares), "ref_price": round(ref_price, 2), "buy_zone_low": None, "buy_zone_high": None, "watch_zone_high": None, "cancel_above": None, "rank": rank_map.get(code, None), "reason": "跌出目标池", "regime": final_state }) continue target_shares = target_shares_map.get(code, current_shares) diff = target_shares - current_shares if diff < 0: rows.append({ "date": date, "code": code, "action": "reduce", "priority": "high" if final_state in ["RISK_SEVERE", "RISK_MILD"] else "medium", "current_shares": safe_int(current_shares), "target_shares": safe_int(target_shares), "share_change": safe_int(diff), "ref_price": round(ref_price, 2), "buy_zone_low": None, "buy_zone_high": None, "watch_zone_high": None, "cancel_above": None, "rank": rank_map.get(code, None), "reason": "目标权重下降或风险降仓", "regime": final_state }) else: rows.append({ "date": date, "code": code, "action": "hold", "priority": "low", "current_shares": safe_int(current_shares), "target_shares": safe_int(target_shares), "share_change": 0, "ref_price": round(ref_price, 2), "buy_zone_low": None, "buy_zone_high": None, "watch_zone_high": None, "cancel_above": None, "rank": rank_map.get(code, None), "reason": "继续持有", "regime": final_state }) # 买入 for code, price, score in selected: current_shares = positions.get(code, 0) target_shares = target_shares_map.get(code, 0) diff = target_shares - current_shares if diff > 0: zones = attach_execution_zone(price, buy_pct=0.02, watch_pct=0.04) rows.append({ "date": date, "code": code, "action": "buy", "priority": "medium", "current_shares": safe_int(current_shares), "target_shares": safe_int(target_shares), "share_change": safe_int(diff), "ref_price": round(price, 2), "buy_zone_low": zones["buy_zone_low"], "buy_zone_high": zones["buy_zone_high"], "watch_zone_high": zones["watch_zone_high"], "cancel_above": zones["cancel_above"], "rank": rank_map.get(code, None), "reason": "进入目标池", "regime": final_state }) df = pd.DataFrame(rows) if len(df) == 0: return pd.DataFrame(columns=[ "date", "code", "action", "priority", "current_shares", "target_shares", "share_change", "ref_price", "buy_zone_low", "buy_zone_high", "watch_zone_high", "cancel_above", "rank", "reason", "regime" ]) action_order = {"sell": 0, "reduce": 1, "buy": 2, "hold": 3} df["_order"] = df["action"].map(action_order) df = df.sort_values(by=["_order", "priority", "code"]).drop(columns=["_order"]).reset_index(drop=True) return df # ========================= # 4. Execution Log # ========================= def init_execution_log_template(plan_date: str, next_trade_date: str, planned_orders_df: pd.DataFrame) -> pd.DataFrame: rows = [] for _, r in planned_orders_df.iterrows(): if r["action"] == "hold": continue rows.append({ "trade_date": next_trade_date, "plan_date": plan_date, "code": r["code"], "action_planned": r["action"], "action_executed": "", "planned_shares": safe_int(r["share_change"]), "filled_shares": 0, "filled_price": None, "status": "", # filled / partial / skipped / cancelled "deviation_reason": "", "note": "" }) return pd.DataFrame(rows) # ========================= # 5. Compare Plan vs Execution # ========================= def build_execution_diff(planned_orders_df: pd.DataFrame, execution_log_df: pd.DataFrame) -> pd.DataFrame: if planned_orders_df.empty: return pd.DataFrame() merged = planned_orders_df.merge( execution_log_df, how="left", left_on=["date", "code", "action"], right_on=["plan_date", "code", "action_planned"], suffixes=("_plan", "_exec") ) rows = [] for _, r in merged.iterrows(): planned_change = safe_int(r.get("share_change", 0)) filled_shares = safe_int(r.get("filled_shares", 0)) ref_price = safe_float(r.get("ref_price", 0.0)) filled_price = safe_float(r.get("filled_price", 0.0), default=0.0) planned_abs = abs(planned_change) filled_abs = abs(filled_shares) completion_ratio = (filled_abs / planned_abs) if planned_abs > 0 else 1.0 slippage_pct = 0.0 if ref_price > 0 and filled_price > 0: if r["action"] == "buy": slippage_pct = (filled_price - ref_price) / ref_price * 100.0 elif r["action"] in ["sell", "reduce"]: slippage_pct = (ref_price - filled_price) / ref_price * 100.0 rows.append({ "date": r["date"], "code": r["code"], "planned_action": r["action"], "executed_action": r.get("action_executed", ""), "planned_shares": planned_change, "filled_shares": filled_shares, "completion_ratio": round(completion_ratio, 4), "ref_price": round(ref_price, 2), "filled_price": round(filled_price, 2) if filled_price > 0 else None, "slippage_pct": round(slippage_pct, 2), "status": r.get("status", ""), "deviation_reason": r.get("deviation_reason", ""), }) return pd.DataFrame(rows) # ========================= # 6. Excel Export # ========================= def export_daily_excels( snapshot_df: pd.DataFrame, holdings_df: pd.DataFrame, planned_orders_df: pd.DataFrame, execution_log_df: pd.DataFrame, execution_diff_df: pd.DataFrame, report_date: str ) -> Dict[str, str]: out_dir = ensure_output_dir() files = { "snapshot": str(out_dir / f"strategy_snapshot_{report_date}.xlsx"), "holdings": str(out_dir / f"holdings_{report_date}.xlsx"), "planned_orders": str(out_dir / f"planned_orders_{report_date}.xlsx"), "execution_log": str(out_dir / f"execution_log_{report_date}.xlsx"), "execution_diff": str(out_dir / f"execution_diff_{report_date}.xlsx"), } snapshot_df.to_excel(files["snapshot"], index=False) holdings_df.to_excel(files["holdings"], index=False) planned_orders_df.to_excel(files["planned_orders"], index=False) execution_log_df.to_excel(files["execution_log"], index=False) execution_diff_df.to_excel(files["execution_diff"], index=False) return files # ========================= # 7. Word Report # ========================= def _set_doc_style(doc: Document): style = doc.styles["Normal"] style.font.name = "Microsoft YaHei" style.font.size = Pt(10.5) def _add_title(doc: Document, title: str): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(title) run.bold = True run.font.size = Pt(16) def _add_subtitle(doc: Document, text: str): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(text) run.font.size = Pt(10.5) def _add_df_table(doc: Document, df: pd.DataFrame, title: str, max_rows: Optional[int] = None): doc.add_heading(title, level=1) if df is None or df.empty: doc.add_paragraph("无数据") return if max_rows is not None: df = df.head(max_rows) table = doc.add_table(rows=1, cols=len(df.columns)) table.style = "Table Grid" for i, col in enumerate(df.columns): table.rows[0].cells[i].text = str(col) for _, row in df.iterrows(): cells = table.add_row().cells for i, col in enumerate(df.columns): val = row[col] cells[i].text = "" if pd.isna(val) else str(val) def build_word_report( report_date: str, snapshot_df: pd.DataFrame, holdings_df: pd.DataFrame, planned_orders_df: pd.DataFrame, execution_log_df: pd.DataFrame, execution_diff_df: pd.DataFrame, save_path: Optional[str] = None ) -> str: out_dir = ensure_output_dir() if save_path is None: save_path = str(out_dir / f"量化执行报告_{report_date}.docx") doc = Document() _set_doc_style(doc) _add_title(doc, "量化策略执行报告") _add_subtitle(doc, f"报告日期:{report_date}") # 1. 策略摘要 doc.add_heading("1. 策略摘要", level=1) if snapshot_df is not None and not snapshot_df.empty: row = snapshot_df.iloc[0] doc.add_paragraph(f"策略版本:{row.get('strategy_version', '')}") doc.add_paragraph(f"大势状态:{row.get('regime', '')}") doc.add_paragraph(f"大势评分:{row.get('regime_score', '')}") doc.add_paragraph(f"目标仓位:{pct_str(safe_float(row.get('target_position', 0.0)) * 100)}") doc.add_paragraph(f"实际仓位:{pct_str(safe_float(row.get('actual_position', 0.0)) * 100)}") doc.add_paragraph(f"当前净值:{money_str(safe_float(row.get('nav', 0.0)))}") doc.add_paragraph(f"持仓数量:{row.get('hold_count', 0)}") doc.add_paragraph(f"是否调仓日:{'是' if bool(row.get('is_rebalance_day', False)) else '否'}") if row.get("summary", ""): doc.add_paragraph(f"摘要:{row.get('summary')}") # 2. 当前持仓 _add_df_table(doc, holdings_df, "2. 当前持仓分析", max_rows=30) # 3. 明日执行清单 planned_for_doc = planned_orders_df.copy() if not planned_for_doc.empty: planned_for_doc = planned_for_doc[ ["code", "action", "priority", "current_shares", "target_shares", "share_change", "ref_price", "buy_zone_low", "buy_zone_high", "watch_zone_high", "cancel_above", "reason"] ] _add_df_table(doc, planned_for_doc, "3. 明日执行清单", max_rows=50) # 4. 今日执行记录 execution_for_doc = execution_log_df.copy() if not execution_for_doc.empty: execution_for_doc = execution_for_doc[ ["code", "action_planned", "action_executed", "planned_shares", "filled_shares", "filled_price", "status", "deviation_reason"] ] _add_df_table(doc, execution_for_doc, "4. 今日执行记录", max_rows=50) # 5. 执行偏差分析 diff_for_doc = execution_diff_df.copy() if not diff_for_doc.empty: diff_for_doc = diff_for_doc[ ["code", "planned_action", "executed_action", "planned_shares", "filled_shares", "completion_ratio", "ref_price", "filled_price", "slippage_pct", "status", "deviation_reason"] ] _add_df_table(doc, diff_for_doc, "5. 执行偏差分析", max_rows=50) # 6. 风险提示 doc.add_heading("6. 风险提示", level=1) if snapshot_df is not None and not snapshot_df.empty: regime = snapshot_df.iloc[0].get("regime", "") target_position = safe_float(snapshot_df.iloc[0].get("target_position", 0.0)) * 100 doc.add_paragraph(f"当前处于 {regime} 状态。") doc.add_paragraph(f"建议仓位控制在 {target_position:.0f}% 附近。") doc.add_paragraph("卖出和减仓建议优先于买入建议执行。") doc.add_paragraph("买入单请以参考价和买入区间执行,高开过多的标的不宜追价。") doc.save(save_path) return save_path 四、怎么接进你现在的 v2.0 你现有 run_backtest() 是回测版。 你以后日常跑盘后,建议在“当日最后一个交易日”生成这几张表。 你可以在主程序后面加一个函数,比如: def generate_daily_execution_package( report_date, strategy_version, regime_info, positions, entry_costs, selected, rank_map, target_weights, price_df, total_value, actual_exposure, is_rebalance_day, next_trade_date, execution_log_existing=None ): from reporting_module import ( build_strategy_snapshot, build_holdings_table, build_planned_orders, init_execution_log_template, build_execution_diff, export_daily_excels, build_word_report, ) holdings_df = build_holdings_table( date=report_date, positions=positions, price_df=price_df, entry_costs=entry_costs, rank_map=rank_map, suggested_actions={}, reasons={} ) snapshot_df = build_strategy_snapshot( date=report_date, strategy_version=strategy_version, regime_info=regime_info, total_value=total_value, actual_exposure=actual_exposure, hold_count=len(positions), is_rebalance_day=is_rebalance_day, summary=f"当前 {regime_info['final_state']},目标仓位 {regime_info['target_exposure']:.0%}" ) planned_orders_df = build_planned_orders( date=report_date, selected=selected, positions=positions, target_weights=target_weights, total_value=total_value, target_exposure=regime_info["target_exposure"], final_state=regime_info["final_state"], price_df=price_df, hold_count_target=len(selected), rank_map=rank_map ) if execution_log_existing is None: execution_log_df = init_execution_log_template( plan_date=report_date, next_trade_date=next_trade_date, planned_orders_df=planned_orders_df ) else: execution_log_df = execution_log_existing.copy() execution_diff_df = build_execution_diff(planned_orders_df, execution_log_df) files = export_daily_excels( snapshot_df=snapshot_df, holdings_df=holdings_df, planned_orders_df=planned_orders_df, execution_log_df=execution_log_df, execution_diff_df=execution_diff_df, report_date=report_date ) word_path = build_word_report( report_date=report_date, snapshot_df=snapshot_df, holdings_df=holdings_df, planned_orders_df=planned_orders_df, execution_log_df=execution_log_df, execution_diff_df=execution_diff_df ) return { "snapshot_df": snapshot_df, "holdings_df": holdings_df, "planned_orders_df": planned_orders_df, "execution_log_df": execution_log_df, "execution_diff_df": execution_diff_df, "excel_files": files, "word_report": word_path } 五、你每天怎么用 你的实际工作流可以固定成这样: 当天收盘后 程序跑出: strategy_snapshot_日期.xlsx holdings_日期.xlsx planned_orders_日期.xlsx execution_log_日期.xlsx execution_diff_日期.xlsx 量化执行报告_日期.docx 第二天执行后 你只需要打开: execution_log_日期.xlsx 把这些列填掉: action_executed filled_shares filled_price status deviation_reason note 然后程序下次可以自动重新生成 execution_diff 和新版 Word。 六、你现有程序还建议补一个东西 你要想让持仓分析更准,最好加一个 entry_costs 字典,记录每只持仓的成本价。 否则 Word 里“浮盈亏”会不准。 可以这样维护: entry_costs = { "600188.SH": 20.15, "688256.SH": 1120.50, } 买入时更新,卖完后删除。 七、最建议你优先做的小改动 先别想着一次做到特别大。 按顺序来: 第一步,把 planned_orders.xlsx 自动生成出来。 第二步,把 execution_log.xlsx 当成你的人工回填表。 第三步,自动生成 Word。 这三步跑通,你的系统就已经不是普通回测程序了,而是一个真正能配合实盘的执行系统。