from __future__ import annotations from pathlib import Path import pandas as pd RUN_ID = "RUN-ANA-WUJI-BASELINE-PILOT-20260607-001" ROOT = Path(__file__).resolve().parents[1] ROLE_TITLES = { "candidate_daily_100d_decision_view": "1. 选股日K图(约100交易日)", "entry_1m_morning_review_view": "2. 买点早盘1分钟复核图", "entry_1m_late_review_view": "3. 买点尾盘1分钟复核图", "entry_1m_buy_decision_view": "4. 买入裁决1分钟图", "exit_daily_signal_review_view": "5. 卖点 / 持仓日K信号图", "exit_1m_sell_decision_view": "6. 卖出裁决1分钟图", } def safe_read_csv(name: str) -> pd.DataFrame: path = ROOT / name return pd.read_csv(path, encoding="utf-8-sig") if path.exists() else pd.DataFrame() def rel_for_case(case_id: str, path: str) -> str: return Path(path).relative_to(f"cases/{case_id}").as_posix() def clean(value: object, default: str = "") -> str: text = str(value).strip() return default if text.lower() in ["", "nan", "none"] else text def has_value(value: object) -> bool: return clean(value) != "" def write_case_image_board(case_id: str, manifest: pd.DataFrame, lots: pd.DataFrame, case_summary: pd.DataFrame) -> None: case_dir = ROOT / "cases" / case_id rows = manifest[manifest.case_id == case_id].copy() summary = case_summary[case_summary.case_id == case_id] lines = [ f"# {case_id} 图片审核板", "", "用途:给人工审核员按图复核本案例从选股、买入、卖点信号到卖出裁决的全链路。", "", ] if not summary.empty: s = summary.iloc[0] lines.extend( [ "## 案例读数", "", f"- 买入 lot:{s.buy_lot_count}", f"- 已闭合 lot:{s.closed_lot_count}", f"- 未闭合 / 待审 lot:{s.unresolved_lot_count}", f"- 当前收益口径:{s.return_boundary}", "", ] ) case_lots = lots[lots.case_id == case_id].copy() if not case_lots.empty: lines.extend(["## 持仓状态", ""]) for _, lot in case_lots.iterrows(): lines.append( f"- {lot.symbol}:{lot.lot_status};买入 {lot.entry_trade_date} {lot.entry_time} @ {float(lot.entry_price):.2f}" + (f";卖出 {lot.exit_trade_date} {lot.exit_time} @ {float(lot.exit_price):.2f}" if has_value(lot.exit_price) else "") ) lines.append("") for role, title in ROLE_TITLES.items(): group = rows[rows.chart_role == role].copy() if group.empty: continue lines.extend([f"## {title}", ""]) for _, row in group.iterrows(): rel = rel_for_case(case_id, row.path) decision = str(row.decision_time) if "decision_time" in row else "" note = str(row.note) if "note" in row else "" status = str(row.status) if "status" in row else "" lines.extend( [ f"### {row.symbol} {decision}", "", f"![{row.symbol}]({rel})", "", f"- 图状态:`{status}`", f"- 说明:{note}", "", ] ) lines.extend( [ "## 审核边界", "", "- 本板是图片第一入口,CSV 和 JSON 是反查材料。", "- 当前仍为结构试点;退出复核执行审核已通过,但 `RETURN_STAT_READY=false`,不得引用完整收益、成功率、胜率或回撤。", "", ] ) (case_dir / "case_image_board.md").write_text("\n".join(lines), encoding="utf-8") def write_case_story_board(case_id: str, manifest: pd.DataFrame, lots: pd.DataFrame, decisions: pd.DataFrame, case_summary: pd.DataFrame) -> None: case_dir = ROOT / "cases" / case_id summary = case_summary[case_summary.case_id == case_id] case_lots = lots[lots.case_id == case_id].copy() case_decisions = decisions[decisions.case_id == case_id].copy() lines = [ f"# {case_id} 一页式故事板", "", f"- 图片审核板:`case_image_board.md`", "- 反查账本:`candidate_ledger.csv`、`decision_log.csv`、`order_ledger.csv`、`position_lot_ledger.csv`、`image_manifest.csv`", "", ] if not summary.empty: s = summary.iloc[0] lines.extend( [ "## 当前结论边界", "", f"- 买入 lot:{s.buy_lot_count}", f"- 已闭合 lot:{s.closed_lot_count}", f"- 未闭合 / 待审 lot:{s.unresolved_lot_count}", f"- 闭合 lot 账户贡献合计:{float(s.account_return_closed_lots):.4%}(只用于账本复算,不是最终收益)", f"- RETURN_STAT_READY:{s.strict_baseline_return_ready_flag}", "", ] ) lines.extend(["## 操作时间线", ""]) if case_decisions.empty: lines.append("- 本案例无买卖裁决,仅保留候选 / 不开仓证据。") else: for _, row in case_decisions.iterrows(): decision_time = clean(row.decision_time, "无裁决时间") price = f" @ {float(row.price):.2f}" if has_value(row.price) else "" image = f";图:`{row.evidence_image_path}`" if has_value(row.evidence_image_path) else "" lines.append( f"- {row.decision_stage} / {row.action_status}:{row.symbol} {decision_time}{price};{row.decision_reason_cn}{image}" ) lines.append("") if not case_lots.empty: lines.extend(["## Lot 收口", ""]) for _, lot in case_lots.iterrows(): contribution = "" if str(lot.account_return_contribution_pct).strip() in ["", "nan"] else f";账户贡献 {float(lot.account_return_contribution_pct):.4%}" lines.append( f"- {lot.trade_lot_id} / {lot.symbol}:{lot.lot_status};买入 {lot.entry_trade_date} {lot.entry_time} @ {float(lot.entry_price):.2f}" + (f";卖出 {lot.exit_trade_date} {lot.exit_time} @ {float(lot.exit_price):.2f}" if has_value(lot.exit_price) else "") + contribution ) lines.append("") role_counts = manifest[manifest.case_id == case_id].chart_role.value_counts().to_dict() lines.extend( [ "## 图片清单概览", "", *[f"- {ROLE_TITLES.get(role, role)}:{count} 张" for role, count in role_counts.items()], "", "## 审核提示", "", "- 先看 `case_image_board.md` 的图,再回查账本。", "- 若看到日 K / 分钟线触发不一致或数据缺口,应按待审项处理,不得自行补收益。", "", ] ) (case_dir / "case_story_board.md").write_text("\n".join(lines), encoding="utf-8") def write_root_story_board(manifest: pd.DataFrame, lots: pd.DataFrame, case_summary: pd.DataFrame) -> None: lines = [ f"# {RUN_ID} 案例故事板总入口", "", "用途:给人工审核员从一个入口进入 7 个小样本案例;每个案例优先看图片审核板,再回查账本。", "", "## 总体边界", "", "- 当前阶段:`STRUCTURE_PILOT_EXIT_REVIEW_RESOLVED_SELF_CHECK_DONE`", "- 执行审核:尚未提交", "- `RETURN_STAT_READY=false`;不得引用完整 baseline 收益率、成功率、胜率或回撤。", "", "## 案例入口", "", "| case_id | 买入lot | 已闭合 | 待审/未闭合 | 图片板 | 故事板 |", "|---|---:|---:|---:|---|---|", ] for _, row in case_summary.sort_values("case_id").iterrows(): lines.append( f"| {row.case_id} | {row.buy_lot_count} | {row.closed_lot_count} | {row.unresolved_lot_count} | [图片板](cases/{row.case_id}/case_image_board.md) | [故事板](cases/{row.case_id}/case_story_board.md) |" ) no_trade_cases = sorted(set(manifest.case_id.unique()) - set(case_summary.case_id.unique())) for case_id in no_trade_cases: lines.append(f"| {case_id} | 0 | 0 | 0 | [图片板](cases/{case_id}/case_image_board.md) | [故事板](cases/{case_id}/case_story_board.md) |") lines.extend( [ "", "## 图片角色统计", "", ] ) for role, count in manifest.chart_role.value_counts().items(): lines.append(f"- {ROLE_TITLES.get(role, role)}:{count} 张") lines.extend( [ "", "## Lot 状态统计", "", ] ) for status, count in lots.lot_status.value_counts().items(): lines.append(f"- {status}:{count}") lines.append("") (ROOT / "case_story_board.md").write_text("\n".join(lines), encoding="utf-8") def write_root_image_board(manifest: pd.DataFrame, lots: pd.DataFrame, case_summary: pd.DataFrame) -> None: non_real_statuses = ["WINDOW_END_VALUATION_ONLY", "EXIT_DATA_GAP_HELD", "SELL_REVIEW_DATA_MISMATCH_HELD", "EXIT_REVIEW_HELD"] non_real_counts = lots[lots.lot_status.isin(non_real_statuses)].lot_status.value_counts() if non_real_counts.empty: non_real_summary = "当前没有非真实 SELL lot 保留。" else: parts = [f"{status} {count} 笔" for status, count in non_real_counts.items()] non_real_summary = f"当前仍有 {int(non_real_counts.sum())} 笔非真实 SELL lot 保留:{';'.join(parts)};不纳入完整收益统计。" lines = [ "# RUN 图片审核入口", "", f"run_id:`{RUN_ID}`", "阶段:`STRUCTURE_PILOT_EXIT_REVIEW_RESOLVED_SELF_CHECK_DONE`", "", "本入口用于人工审核图片链路。当前已生成候选日 K、买入 1 分钟复核、买入裁决、卖点日 K 信号、卖出 1 分钟裁决、订单账本和账户账本;退出复核执行审核已通过,但 `RETURN_STAT_READY=false`,不得引用完整收益结论。", "", "## 总体边界", "", "- `RETURN_STAT_READY=false`", f"- {non_real_summary}", "- 根入口只做导航;逐图审核请进入各案例 `case_image_board.md`。", "", "## 案例入口", "", "| case_id | 买入lot | 已闭合 | 待审/未闭合 | 图片板 | 故事板 |", "|---|---:|---:|---:|---|---|", ] for _, row in case_summary.sort_values("case_id").iterrows(): lines.append( f"| {row.case_id} | {row.buy_lot_count} | {row.closed_lot_count} | {row.unresolved_lot_count} | [图片板](cases/{row.case_id}/case_image_board.md) | [故事板](cases/{row.case_id}/case_story_board.md) |" ) no_trade_cases = sorted(set(manifest.case_id.unique()) - set(case_summary.case_id.unique())) for case_id in no_trade_cases: lines.append(f"| {case_id} | 0 | 0 | 0 | [图片板](cases/{case_id}/case_image_board.md) | [故事板](cases/{case_id}/case_story_board.md) |") lines.extend(["", "## 图片角色统计", ""]) for role, count in manifest.chart_role.value_counts().items(): lines.append(f"- {ROLE_TITLES.get(role, role)}:{count} 张") lines.extend(["", "## Lot 状态统计", ""]) for status, count in lots.lot_status.value_counts().items(): lines.append(f"- {status}:{count}") lines.append("") (ROOT / "case_image_board.md").write_text("\n".join(lines), encoding="utf-8") def main() -> None: manifest = safe_read_csv("image_manifest.csv") lots = safe_read_csv("position_lot_ledger.csv") decisions = safe_read_csv("decision_log.csv") case_summary = safe_read_csv("case_summary.csv") all_case_ids = sorted(manifest.case_id.dropna().unique().tolist()) for case_id in all_case_ids: write_case_image_board(case_id, manifest, lots, case_summary) write_case_story_board(case_id, manifest, lots, decisions, case_summary) write_root_story_board(manifest, lots, case_summary) write_root_image_board(manifest, lots, case_summary) if __name__ == "__main__": main()