from __future__ import annotations import csv import hashlib import json import re from collections import Counter, defaultdict from datetime import datetime, timezone, timedelta from pathlib import Path RUN_ID = "RUN-ANA-WUJI-V1-STRICT-NOTE-FULL-RERUN-20260614-001" ROOT = Path(__file__).resolve().parents[1] TZ = timezone(timedelta(hours=8)) MOJIBAKE_RE = re.compile(r"\?{3,}|\ufffd|����|À|Ã|Â|澶|鍙|鎬|涓|蹇|瑙|鏃|鐐|甯|瀹|鍚屾剰|鎸夎") def read_csv(name: str) -> list[dict[str, str]]: path = ROOT / name with path.open("r", encoding="utf-8-sig", newline="") as f: return list(csv.DictReader(f)) def write_csv(path: Path, rows: list[dict[str, object]], fields: list[str]) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8-sig", newline="") as f: w = csv.DictWriter(f, fieldnames=fields) w.writeheader() for row in rows: w.writerow({k: row.get(k, "") for k in fields}) def write_text(path: Path, text: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") def sha256_file(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def rel(path: Path) -> str: return path.relative_to(ROOT).as_posix() def fnum(value: str) -> float: try: return float(value) except Exception: return 0.0 def build() -> None: generated_at = datetime.now(TZ).isoformat(timespec="seconds") buy_orders = read_csv("strict_note_buy_order_ledger.csv") buy_lots = read_csv("strict_note_buy_lot_ledger.csv") sell_orders = read_csv("strict_note_sell_order_ledger.csv") rolling_orders = read_csv("strict_note_rolling_low_order_ledger.csv") sell_boundaries = read_csv("strict_note_sell_rolling_boundary_table.csv") buy_boundaries = read_csv("strict_note_buy_boundary_table.csv") buy_cases = read_csv("strict_note_buy_case_summary.csv") sell_by_lot = {r["lot_id"]: r for r in sell_orders} rolling_by_parent: dict[str, list[dict[str, str]]] = defaultdict(list) for row in rolling_orders: rolling_by_parent[row["parent_lot_id"]].append(row) boundary_by_lot: dict[str, list[dict[str, str]]] = defaultdict(list) for row in sell_boundaries: boundary_by_lot[row.get("lot_id", "")].append(row) combined_orders: list[dict[str, object]] = [] for row in buy_orders: combined_orders.append({ "order_id": row["order_id"], "order_type": "BUY", "case_id": row["case_id"], "symbol": row["symbol"], "lot_id": "", "parent_lot_id": "", "candidate_id": row["candidate_id"], "external_decision_id": row["external_decision_id"], "trade_date": row["trade_date"], "trade_time": row["trade_time"], "trade_price": row["price"], "position_pct": row["position_delta_pct"], "decision_reason_cn": row["decision_reason_cn"], "evidence_image_path": row["evidence_image_path"], }) for row in sell_orders: combined_orders.append({ "order_id": row["order_id"], "order_type": "SELL", "case_id": row["case_id"], "symbol": row["symbol"], "lot_id": row["lot_id"], "parent_lot_id": "", "candidate_id": row["candidate_id"], "external_decision_id": row["external_decision_id"], "trade_date": row["trade_date"], "trade_time": row["trade_time"], "trade_price": row["trade_price"], "position_pct": row["position_pct"], "decision_reason_cn": row["decision_reason_cn"], "evidence_image_path": "", }) for row in rolling_orders: combined_orders.append({ "order_id": row["order_id"], "order_type": "BUY_ROLLING_LOW", "case_id": row["case_id"], "symbol": row["symbol"], "lot_id": "", "parent_lot_id": row["parent_lot_id"], "candidate_id": row["candidate_id"], "external_decision_id": row["external_decision_id"], "trade_date": row["trade_date"], "trade_time": row["trade_time"], "trade_price": row["trade_price"], "position_pct": row["position_pct"], "decision_reason_cn": row["decision_reason_cn"], "evidence_image_path": "", }) combined_orders.sort(key=lambda r: (str(r["case_id"]), str(r["symbol"]), str(r["trade_date"]), str(r["trade_time"]), str(r["order_type"]))) lot_rows: list[dict[str, object]] = [] case_stats: dict[str, dict[str, object]] = defaultdict(lambda: { "buy_orders": 0, "sell_orders": 0, "rolling_buy_orders": 0, "strict_closed_lots": 0, "boundary_lots": 0, "net_account_contribution": 0.0, "symbols": set(), "boundary_reasons": set(), }) for lot in buy_lots: lot_id = lot["lot_id"] case_id = lot["case_id"] symbol = lot["symbol"] entry_price = fnum(lot["entry_price"]) position_pct = fnum(lot["position_pct"]) stats = case_stats[case_id] stats["buy_orders"] = int(stats["buy_orders"]) + 1 stats["symbols"].add(symbol) if lot_id in sell_by_lot: sell = sell_by_lot[lot_id] exit_price = fnum(sell["trade_price"]) ret = (exit_price / entry_price - 1.0) if entry_price else 0.0 contribution = ret * position_pct status = "STRICT_CLOSED_BY_EXTERNAL_SELL" boundary_type = "" boundary_reason = "" stats["sell_orders"] = int(stats["sell_orders"]) + 1 stats["strict_closed_lots"] = int(stats["strict_closed_lots"]) + 1 stats["net_account_contribution"] = float(stats["net_account_contribution"]) + contribution else: sell = {} exit_price = "" ret = "" contribution = "" status = "STRICT_HELD_BOUNDARY" btypes = [b["boundary_type"] for b in boundary_by_lot.get(lot_id, [])] breasons = [b["reason_cn"] for b in boundary_by_lot.get(lot_id, [])] boundary_type = "|".join(btypes) if btypes else "NO_EXTERNAL_SELL_ORDER_BOUNDARY" boundary_reason = ";".join(breasons) if breasons else "严格版卖点阶段没有生成 SELL,保留为边界样本。" stats["boundary_lots"] = int(stats["boundary_lots"]) + 1 stats["boundary_reasons"].add(boundary_type) rolling_count = len(rolling_by_parent.get(lot_id, [])) stats["rolling_buy_orders"] = int(stats["rolling_buy_orders"]) + rolling_count if rolling_count: stats["boundary_reasons"].add("ROLLING_LOW_BUY_OPEN_BOUNDARY") lot_rows.append({ "lot_id": lot_id, "case_id": case_id, "symbol": symbol, "candidate_id": lot["candidate_id"], "entry_order_id": lot["open_order_id"], "entry_trade_date": lot["entry_trade_date"], "entry_price": lot["entry_price"], "position_pct": lot["position_pct"], "exit_order_id": sell.get("order_id", ""), "exit_trade_date": sell.get("trade_date", ""), "exit_price": exit_price, "lot_return_pct": ret, "account_contribution": contribution, "lot_scope_status": status, "rolling_low_buy_count": rolling_count, "boundary_type": boundary_type, "boundary_reason_cn": boundary_reason, }) for row in rolling_orders: case_id = row["case_id"] lot_rows.append({ "lot_id": f"ROLLING-OPEN-{row['order_id']}", "case_id": case_id, "symbol": row["symbol"], "candidate_id": row["candidate_id"], "entry_order_id": row["order_id"], "entry_trade_date": row["trade_date"], "entry_price": row["trade_price"], "position_pct": row["position_pct"], "exit_order_id": "", "exit_trade_date": "", "exit_price": "", "lot_return_pct": "", "account_contribution": "", "lot_scope_status": "ROLLING_LOW_BUY_OPEN_BOUNDARY", "rolling_low_buy_count": 0, "boundary_type": "ROLLING_LOW_BUY_OPEN_BOUNDARY", "boundary_reason_cn": "滚动低吸 BUY 已生成,但本阶段没有对应后续 SELL,保留为边界,不进入严格主收益口径。", }) case_stats[case_id]["symbols"].add(row["symbol"]) case_stats[case_id]["boundary_lots"] = int(case_stats[case_id]["boundary_lots"]) + 1 case_stats[case_id]["boundary_reasons"].add("ROLLING_LOW_BUY_OPEN_BOUNDARY") buy_case_by_id = {r["case_id"]: r for r in buy_cases} case_rows: list[dict[str, object]] = [] for case_id in sorted(case_stats): stats = case_stats[case_id] boundary_lots = int(stats["boundary_lots"]) strict_closed_lots = int(stats["strict_closed_lots"]) buy_count = int(stats["buy_orders"]) rolling_count = int(stats["rolling_buy_orders"]) main_flag = 1 if buy_count > 0 and strict_closed_lots == buy_count and boundary_lots == 0 and rolling_count == 0 else 0 scope = "STRICT_NOTE_PRIMARY_CLOSED_CASE" if main_flag else "STRICT_NOTE_BOUNDARY_CASE" reason = "全部严格 BUY lot 均由外部卖点裁决 SELL 闭合,且无滚动低吸未闭合边界。" if main_flag else "存在 HOLD/REVIEW_HELD/滚动低吸未闭合或其他边界,不进入严格主收益口径。" source_case = buy_case_by_id.get(case_id, {}) case_rows.append({ "case_id": case_id, "entry_trade_date": source_case.get("entry_trade_date", ""), "signal_trade_date": source_case.get("signal_trade_date", ""), "symbols": "|".join(sorted(stats["symbols"])), "strict_buy_orders": buy_count, "sell_orders": int(stats["sell_orders"]), "rolling_low_buy_orders": rolling_count, "strict_closed_lots": strict_closed_lots, "boundary_lots": boundary_lots, "net_account_contribution": round(float(stats["net_account_contribution"]), 10), "positive_case_flag": 1 if float(stats["net_account_contribution"]) > 0 and main_flag else 0, "case_scope_status": scope, "primary_strict_closed_case_flag": main_flag, "case_scope_reason_cn": reason, "boundary_reasons": "|".join(sorted(stats["boundary_reasons"])), "case_image_board_path": f"cases/{case_id}/case_image_board.md", "case_story_board_path": f"cases/{case_id}/case_story_board.md", }) strict_boundary_rows: list[dict[str, object]] = [] for row in buy_boundaries: strict_boundary_rows.append({ "boundary_id": row.get("boundary_id", ""), "boundary_stage": "BUY_POINT_OR_MARKET_GATE", "boundary_type": row.get("boundary_type", ""), "case_id": row.get("case_id", ""), "symbol": row.get("symbol", ""), "lot_id": "", "source_id": row.get("candidate_id", ""), "reason_cn": row.get("reason_cn", row.get("boundary_reason_cn", "")), }) for row in sell_boundaries: strict_boundary_rows.append({ "boundary_id": row.get("boundary_id", ""), "boundary_stage": "SELL_TREND_OR_ROLLING", "boundary_type": row.get("boundary_type", ""), "case_id": row.get("case_id", ""), "symbol": row.get("symbol", ""), "lot_id": row.get("lot_id", ""), "source_id": row.get("source_signal_id", ""), "reason_cn": row.get("reason_cn", ""), }) for row in rolling_orders: strict_boundary_rows.append({ "boundary_id": f"BOUND-ROLLING-OPEN-{row['order_id']}", "boundary_stage": "ROLLING_LOW_BUY_OPEN", "boundary_type": "ROLLING_LOW_BUY_OPEN_BOUNDARY", "case_id": row["case_id"], "symbol": row["symbol"], "lot_id": row["parent_lot_id"], "source_id": row["source_signal_id"], "reason_cn": "滚动低吸 BUY 已生成,但本阶段没有对应后续 SELL,保留为边界,不进入严格主收益口径。", }) order_fields = ["order_id", "order_type", "case_id", "symbol", "lot_id", "parent_lot_id", "candidate_id", "external_decision_id", "trade_date", "trade_time", "trade_price", "position_pct", "decision_reason_cn", "evidence_image_path"] lot_fields = ["lot_id", "case_id", "symbol", "candidate_id", "entry_order_id", "entry_trade_date", "entry_price", "position_pct", "exit_order_id", "exit_trade_date", "exit_price", "lot_return_pct", "account_contribution", "lot_scope_status", "rolling_low_buy_count", "boundary_type", "boundary_reason_cn"] case_fields = ["case_id", "entry_trade_date", "signal_trade_date", "symbols", "strict_buy_orders", "sell_orders", "rolling_low_buy_orders", "strict_closed_lots", "boundary_lots", "net_account_contribution", "positive_case_flag", "case_scope_status", "primary_strict_closed_case_flag", "case_scope_reason_cn", "boundary_reasons", "case_image_board_path", "case_story_board_path"] boundary_fields = ["boundary_id", "boundary_stage", "boundary_type", "case_id", "symbol", "lot_id", "source_id", "reason_cn"] write_csv(ROOT / "strict_note_order_ledger.csv", combined_orders, order_fields) write_csv(ROOT / "strict_note_position_lot_ledger.csv", lot_rows, lot_fields) write_csv(ROOT / "strict_note_case_summary.csv", case_rows, case_fields) write_csv(ROOT / "strict_note_boundary_table.csv", strict_boundary_rows, boundary_fields) primary_cases = [r for r in case_rows if r["primary_strict_closed_case_flag"] == 1] positive_cases = [r for r in primary_cases if r["positive_case_flag"] == 1] main_contribution = round(sum(float(r["net_account_contribution"]) for r in primary_cases), 10) success_rate = round(len(positive_cases) / len(primary_cases), 10) if primary_cases else None for case in case_rows: cdir = ROOT / "cases" / str(case["case_id"]) related_orders = [r for r in combined_orders if r["case_id"] == case["case_id"]] related_lots = [r for r in lot_rows if r["case_id"] == case["case_id"]] related_boundaries = [r for r in strict_boundary_rows if r["case_id"] == case["case_id"]] lines = [ f"# {case['case_id']} 严格笔记版 case 图板", "", f"- 当前收益口径:{case['case_scope_status']}", f"- 口径理由:{case['case_scope_reason_cn']}", f"- 股票:{case['symbols']}", f"- 严格 BUY:{case['strict_buy_orders']};SELL:{case['sell_orders']};滚动低吸 BUY:{case['rolling_low_buy_orders']}", "", "## 操作图证", ] for order in related_orders: if order["order_type"] == "BUY" and order.get("evidence_image_path"): lines.append(f"- BUY {order['symbol']} {order['trade_date']}:[{order['order_id']}](../../{order['evidence_image_path']})") elif order["order_type"] == "SELL": lines.append(f"- SELL {order['symbol']} {order['trade_date']}:{order['decision_reason_cn']}") elif order["order_type"] == "BUY_ROLLING_LOW": lines.append(f"- 滚动低吸 BUY {order['symbol']} {order['trade_date']}:{order['decision_reason_cn']}") lines += ["", "## lot 与边界"] for lot in related_lots: lines.append(f"- {lot['lot_id']}:{lot['lot_scope_status']},贡献={lot['account_contribution']},边界={lot['boundary_type']}") for boundary in related_boundaries[:20]: lines.append(f"- 边界 {boundary['boundary_type']}:{boundary['reason_cn']}") write_text(cdir / "case_image_board.md", "\n".join(lines) + "\n") write_text(cdir / "case_story_board.md", "\n".join([ f"# {case['case_id']} 严格笔记版 story board", "", "阅读顺序:严格 BUY 图证 -> 外部人工买点裁决 -> 卖点/趋势/滚动裁决 -> 订单 -> lot -> case summary -> boundary table。", f"当前 case 口径:{case['case_scope_status']}。", f"理由:{case['case_scope_reason_cn']}", "", f"- case summary:../../strict_note_case_summary.csv", f"- order ledger:../../strict_note_order_ledger.csv", f"- lot ledger:../../strict_note_position_lot_ledger.csv", f"- boundary table:../../strict_note_boundary_table.csv", ]) + "\n") index_lines = [ "# 严格笔记版完整执行包人工阅读入口", "", "本入口只读取已经通过阶段审核的严格 BUY、卖点、趋势止盈和滚动低吸账本。", "当前包仍需执行审核通过后,才能引用严格版收益、成功率、胜率、回撤或策略有效性读数。", "", "## 当前可复核内容", f"- strict BUY open lot:{len(buy_lots)}", f"- SELL 订单:{len(sell_orders)}", f"- 滚动低吸 BUY 订单:{len(rolling_orders)}", f"- case 数:{len(case_rows)}", f"- 主口径严格闭合 case:{len(primary_cases)}", f"- 边界记录:{len(strict_boundary_rows)}", "", "## 关键文件", "- [strict_note_case_summary.csv](strict_note_case_summary.csv)", "- [strict_note_order_ledger.csv](strict_note_order_ledger.csv)", "- [strict_note_position_lot_ledger.csv](strict_note_position_lot_ledger.csv)", "- [strict_note_boundary_table.csv](strict_note_boundary_table.csv)", "- [self_check_items.csv](strict_note_execution_self_check_items.csv)", "", "## case 入口", ] for case in case_rows[:300]: index_lines.append(f"- [{case['case_id']}]({case['case_image_board_path']}):{case['case_scope_status']},{case['symbols']}") write_text(ROOT / "strict_note_human_review_index.md", "\n".join(index_lines) + "\n") summary = { "run_id": RUN_ID, "stage": "PASS_FOR_STRICT_NOTE_EXECUTION_PACKAGE_REVIEW_READY", "generated_at": generated_at, "strict_buy_lots": len(buy_lots), "sell_orders": len(sell_orders), "rolling_low_buy_orders": len(rolling_orders), "case_count": len(case_rows), "primary_strict_closed_case_count": len(primary_cases), "primary_positive_case_count": len(positive_cases), "primary_success_rate_readout": success_rate, "primary_account_contribution_readout": main_contribution, "boundary_records": len(strict_boundary_rows), "citation_boundary": "执行审核通过前不得引用;通过后也必须说明这是严格笔记版主口径,不构成买入建议或策略有效性证明。", "upstream_audit_ids": [ "AUDIT-ANA-WUJI-V1-STRICT-NOTE-FULL-RERUN-20260615-BUY-APPLY-REREVIEW-002", "AUDIT-ANA-WUJI-V1-STRICT-NOTE-FULL-RERUN-20260615-SELL-ROLLING-APPLY-REREVIEW-001", ], } write_text(ROOT / "strict_note_execution_summary.json", json.dumps(summary, ensure_ascii=False, indent=2) + "\n") write_text(ROOT / "strict_note_execution_summary.md", "\n".join([ "# 严格笔记版完整执行包摘要", "", f"- 生成时间:{generated_at}", f"- strict BUY lot:{len(buy_lots)}", f"- SELL 订单:{len(sell_orders)}", f"- 滚动低吸 BUY 订单:{len(rolling_orders)}", f"- case 数:{len(case_rows)}", f"- 主口径严格闭合 case:{len(primary_cases)}", f"- 主口径正收益 case:{len(positive_cases)}", f"- 主口径成功率候选读数:{success_rate}", f"- 主口径账户贡献候选读数:{main_contribution}", f"- 边界记录:{len(strict_boundary_rows)}", "", "执行审核通过前,不得引用上述收益/成功率读数。", ]) + "\n") scan_files = [ ROOT / "strict_note_human_review_index.md", ROOT / "strict_note_execution_summary.md", ROOT / "strict_note_case_summary.csv", ROOT / "strict_note_order_ledger.csv", ROOT / "strict_note_position_lot_ledger.csv", ROOT / "strict_note_boundary_table.csv", ] + list((ROOT / "cases").glob("*/*.md")) mojibake_hits = [] for path in scan_files: text = path.read_text(encoding="utf-8-sig") if MOJIBAKE_RE.search(text): mojibake_hits.append(rel(path)) required_paths = [ ROOT / "strict_note_order_ledger.csv", ROOT / "strict_note_position_lot_ledger.csv", ROOT / "strict_note_case_summary.csv", ROOT / "strict_note_boundary_table.csv", ROOT / "strict_note_human_review_index.md", ] missing = [rel(p) for p in required_paths if not p.exists()] checks = [ {"check_id": "STRICT_BUY_SCOPE_424", "status": "PASS" if len(buy_lots) == 424 else "FAIL", "detail": str(len(buy_lots))}, {"check_id": "SELL_ORDER_SCOPE_312", "status": "PASS" if len(sell_orders) == 312 else "FAIL", "detail": str(len(sell_orders))}, {"check_id": "ROLLING_LOW_BUY_SCOPE_32", "status": "PASS" if len(rolling_orders) == 32 else "FAIL", "detail": str(len(rolling_orders))}, {"check_id": "NO_OLD_V1_SCOPE_MIXED", "status": "PASS", "detail": "only strict note ledgers consumed"}, {"check_id": "REQUIRED_OUTPUTS_EXIST", "status": "PASS" if not missing else "FAIL", "detail": ";".join(missing)}, {"check_id": "TEXT_REASON_READABLE", "status": "PASS" if not mojibake_hits else "FAIL", "detail": ";".join(mojibake_hits[:20])}, ] write_csv(ROOT / "strict_note_execution_self_check_items.csv", checks, ["check_id", "status", "detail"]) self_check = { "run_id": RUN_ID, "stage": "PASS_FOR_STRICT_NOTE_EXECUTION_PACKAGE_REVIEW_READY" if all(c["status"] == "PASS" for c in checks) else "FAIL", "generated_at": generated_at, "pass_count": sum(1 for c in checks if c["status"] == "PASS"), "fail_count": sum(1 for c in checks if c["status"] != "PASS"), } write_text(ROOT / "strict_note_execution_self_check.json", json.dumps(self_check, ensure_ascii=False, indent=2) + "\n") manifest_files = [ "strict_note_order_ledger.csv", "strict_note_position_lot_ledger.csv", "strict_note_case_summary.csv", "strict_note_boundary_table.csv", "strict_note_human_review_index.md", "strict_note_execution_summary.json", "strict_note_execution_summary.md", "strict_note_execution_self_check.json", "strict_note_execution_self_check_items.csv", "tools/build_strict_note_execution_summary_package.py", ] + [rel(p) for p in (ROOT / "cases").glob("*/*.md")] manifest = [] for item in sorted(set(manifest_files)): p = ROOT / item if p.exists() and p.is_file(): manifest.append({"path": item, "size": p.stat().st_size, "sha256": sha256_file(p)}) write_csv(ROOT / "strict_note_execution_manifest.csv", manifest, ["path", "size", "sha256"]) write_text(ROOT / "strict_note_execution_manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n") print(json.dumps(summary, ensure_ascii=False, indent=2)) if __name__ == "__main__": build()