from __future__ import annotations import hashlib import json import shutil from datetime import datetime from pathlib import Path from typing import Iterable import pandas as pd RUN_ID = "RUN-ANA-WUJI-EXPAND-30-20260608-001" SOURCE_RUN_ID = "RUN-ANA-WUJI-BASELINE-PILOT-20260607-001" TASK_ID = "ANA-WUJI-BASELINE-2023-2026" DESIGN_ID = "DESIGN-WUJI-EXPAND-30-20260608" DESIGN_AUDIT_ID = "AUDIT-ANA-WUJI-EXPAND-30-20260608-DESIGN-001" SOURCE_AUDIT_IDS = [ "AUDIT-ANA-WUJI-BASELINE-FLOW-20260607-001", "AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-EXEC-REREVIEW-001", "AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-EXIT-REVIEW-001", "AUDIT-ANA-WUJI-RETURN-STAT-PILOT-20260608-EXEC-REREVIEW-001", "AUDIT-ANA-WUJI-BASELINE-PILOT-20260608-CANDIDATE-POOL-EVIDENCE-REREVIEW-001", ] ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = ROOT.parents[2] SOURCE_ROOT = PROJECT_ROOT / "ana-data" / "result" / SOURCE_RUN_ID NO_FUTURE_SELECTION_STATEMENT = ( "Selection uses candidate_ledger signal/entry fields only. It did not read " "buy/sell decision logs, future prices after the entry date, returns, lot " "outcomes, drawdown, or return statistics." ) def now_iso() -> str: return datetime.now().astimezone().isoformat(timespec="seconds") 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 write_csv(df: pd.DataFrame, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) df.to_csv(path, index=False, encoding="utf-8-sig") def write_json(data: dict, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def bool_series(series: pd.Series) -> pd.Series: return series.astype(str).str.lower().isin(["true", "1", "yes"]) def copy_source_inputs() -> None: ROOT.mkdir(parents=True, exist_ok=True) for name in [ "candidate_ledger.csv", "candidate_generation_summary.json", "candidate_generation_summary.md", "baseline_rule_mapping.csv", "baseline_excluded_rule_table.csv", "candidate_pool_evidence_repair_summary.json", "candidate_pool_evidence_repair_summary.md", "candidate_pool_evidence_source_sample_50.csv", ]: src = SOURCE_ROOT / name if src.exists(): shutil.copy2(src, ROOT / name) target_tools = ROOT / "tools" target_tools.mkdir(parents=True, exist_ok=True) for src in sorted((SOURCE_ROOT / "tools").glob("*.py")): text = src.read_text(encoding="utf-8") text = text.replace(SOURCE_RUN_ID, RUN_ID) (target_tools / src.name).write_text(text, encoding="utf-8") def load_inputs() -> tuple[pd.DataFrame, pd.DataFrame, dict]: candidate_ledger = pd.read_csv(SOURCE_ROOT / "candidate_ledger.csv", encoding="utf-8-sig") source_case_index = pd.read_csv(SOURCE_ROOT / "case_index.csv", encoding="utf-8-sig") summary = json.loads((SOURCE_ROOT / "candidate_generation_summary.json").read_text(encoding="utf-8")) return candidate_ledger, source_case_index, summary def build_date_summary(candidate_ledger: pd.DataFrame) -> pd.DataFrame: df = candidate_ledger.copy() df["entry_trade_date"] = df["entry_trade_date"].astype(str) df["signal_trade_date"] = df["signal_trade_date"].astype(str) df["candidate_rank_num"] = pd.to_numeric(df["candidate_rank"], errors="coerce") df["strict_bool"] = bool_series(df["strict_candidate_flag"]) df["market_gate_open_bool"] = bool_series(df["market_gate_open_flag"]) for col in [ "up_count", "down_count", "stock_count", "volume_ratio", "upper_shadow_pct", "amount", ]: df[col] = pd.to_numeric(df[col], errors="coerce") grouped = ( df.groupby("entry_trade_date", as_index=False) .agg( signal_trade_date=("signal_trade_date", "first"), market_gate_status=("market_gate_status", "first"), market_gate_open_flag=("market_gate_open_bool", "max"), stock_count=("stock_count", "first"), up_count=("up_count", "first"), down_count=("down_count", "first"), candidate_count=("candidate_id", "count"), strict_candidate_count=("strict_bool", "sum"), pass_count=("candidate_status", lambda s: int((s == "PASS").sum())), risk_count=("candidate_status", lambda s: int((s == "FAKE_BREAKOUT_RISK_REVIEW").sum())), review_candidate_count=("candidate_status", lambda s: int((s != "PASS").sum())), max_volume_ratio=("volume_ratio", "max"), max_upper_shadow_pct=("upper_shadow_pct", "max"), max_amount=("amount", "max"), min_candidate_rank=("candidate_rank_num", "min"), ) .sort_values("entry_trade_date") .reset_index(drop=True) ) grouped["year"] = grouped["entry_trade_date"].str.slice(0, 4) grouped["market_gate_open_flag"] = grouped["market_gate_open_flag"].astype(bool) return grouped def sort_open_pass(pool: pd.DataFrame) -> pd.DataFrame: return pool.sort_values( ["pass_count", "max_volume_ratio", "max_upper_shadow_pct", "entry_trade_date"], ascending=[False, False, False, True], ) def sort_closed(pool: pd.DataFrame) -> pd.DataFrame: return pool.sort_values( ["candidate_count", "strict_candidate_count", "entry_trade_date"], ascending=[False, False, True], ) def sort_risk(pool: pd.DataFrame) -> pd.DataFrame: p = pool.copy() p["gate_score"] = p["market_gate_open_flag"].astype(int) return p.sort_values( ["gate_score", "risk_count", "max_upper_shadow_pct", "max_volume_ratio", "entry_trade_date"], ascending=[False, False, False, False, True], ) def make_case_row( row: pd.Series, case_id: str, role: str, bucket: str, source_case_id: str, reason: str, substitution_flag: bool = False, substitution_reason: str = "", ) -> dict: gate_open = bool(row["market_gate_open_flag"]) case_status = "SELECTED_FOR_EXPAND_REPLAY" if gate_open else "SELECTED_NO_TRADE_MARKET_GATE_CLOSED" return { "case_id": case_id, "source_case_id": source_case_id, "source_run_id": SOURCE_RUN_ID if source_case_id else "", "expand_case_role": role, "entry_trade_date": str(row["entry_trade_date"]), "signal_trade_date": str(row["signal_trade_date"]), "selection_bucket": bucket, "case_status": case_status, "market_gate_status": str(row["market_gate_status"]), "candidate_count": int(row["candidate_count"]), "strict_candidate_count": int(row["strict_candidate_count"]), "review_candidate_count": int(row["review_candidate_count"]), "pass_count": int(row["pass_count"]), "risk_count": int(row["risk_count"]), "up_count": int(row["up_count"]), "down_count": int(row["down_count"]), "stock_count": int(row["stock_count"]), "max_volume_ratio": float(row["max_volume_ratio"]), "max_upper_shadow_pct": float(row["max_upper_shadow_pct"]), "max_amount": float(row["max_amount"]), "selection_reason": reason, "selection_basis": ( "Deterministic controlled expansion sampling from candidate_ledger.csv, " "using only signal-day and entry-day candidate fields." ), "substitution_flag": bool(substitution_flag), "substitution_reason": substitution_reason, "no_future_selection_statement": NO_FUTURE_SELECTION_STATEMENT, } def add_anchor_rows( date_summary: pd.DataFrame, source_case_index: pd.DataFrame, case_rows: list[dict], selection_log: list[dict], selected_dates: set[str], ) -> None: date_lookup = date_summary.set_index("entry_trade_date") for _, src in source_case_index.sort_values("case_id").iterrows(): entry_date = str(src["entry_trade_date"]) row = date_lookup.loc[entry_date].copy() row["entry_trade_date"] = entry_date case_row = make_case_row( row=row, case_id=str(src["case_id"]), role="ANCHOR_REUSED_FROM_AUDITED_7_CASE_PILOT", bucket=str(src["selection_bucket"]), source_case_id=str(src["case_id"]), reason=( "Reuse audited 7-case structural pilot anchor exactly; this anchor was " "already checked in the prior structure pilot and return-stat prep flow." ), ) case_row["case_status"] = str(src["case_status"]) case_rows.append(case_row) selected_dates.add(entry_date) selection_log.append({**case_row, "selection_action": "ANCHOR_REUSED", "selection_order": len(case_rows)}) def choose_rows( pool: pd.DataFrame, count: int, selected_dates: set[str], sort_fn, per_year: int | None = None, years: Iterable[str] = ("2023", "2024", "2025", "2026"), ) -> list[pd.Series]: chosen: list[pd.Series] = [] available = pool[~pool["entry_trade_date"].isin(selected_dates)].copy() if per_year is not None: for year in years: yp = sort_fn(available[available["year"] == year]) for _, row in yp.head(per_year).iterrows(): if len(chosen) >= count: break date = str(row["entry_trade_date"]) if date not in selected_dates: chosen.append(row) selected_dates.add(date) if len(chosen) >= count: break if len(chosen) < count: remaining = pool[~pool["entry_trade_date"].isin(selected_dates)].copy() for _, row in sort_fn(remaining).iterrows(): if len(chosen) >= count: break date = str(row["entry_trade_date"]) chosen.append(row) selected_dates.add(date) return chosen def choose_boundary_rows(date_summary: pd.DataFrame, selected_dates: set[str]) -> list[pd.Series]: available = date_summary[~date_summary["entry_trade_date"].isin(selected_dates)].copy() ordered = available.sort_values("entry_trade_date") start_pool = ordered.head(17) end_pool = ordered.tail(12).sort_values("entry_trade_date", ascending=False) chosen: list[pd.Series] = [] for pool in [start_pool, end_pool]: for _, row in pool.iterrows(): date = str(row["entry_trade_date"]) if date not in selected_dates: chosen.append(row) selected_dates.add(date) break if len(chosen) < 3: min_date = pd.to_datetime(ordered["entry_trade_date"].min()) max_date = pd.to_datetime(ordered["entry_trade_date"].max()) remaining = date_summary[~date_summary["entry_trade_date"].isin(selected_dates)].copy() remaining["entry_dt"] = pd.to_datetime(remaining["entry_trade_date"]) remaining["boundary_distance"] = remaining["entry_dt"].map( lambda d: min(abs((d - min_date).days), abs((max_date - d).days)) ) remaining = remaining.sort_values( ["boundary_distance", "market_gate_open_flag", "candidate_count", "entry_trade_date"], ascending=[True, False, False, True], ) for _, row in remaining.iterrows(): if len(chosen) >= 3: break date = str(row["entry_trade_date"]) chosen.append(row) selected_dates.add(date) return chosen def build_case_selection(candidate_ledger: pd.DataFrame, source_case_index: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: date_summary = build_date_summary(candidate_ledger) case_rows: list[dict] = [] selection_log: list[dict] = [] selected_dates: set[str] = set() add_anchor_rows(date_summary, source_case_index, case_rows, selection_log, selected_dates) new_specs = [ ( "OPEN_STRONG_CANDIDATE", choose_rows( date_summary[(date_summary["market_gate_open_flag"]) & (date_summary["pass_count"] > 0)], 12, selected_dates, sort_open_pass, per_year=3, ), "Market gate open and PASS candidates are abundant; chosen by pass_count, volume_ratio, upper_shadow and date.", ), ( "MARKET_GATE_CLOSED_WITH_CANDIDATES", choose_rows( date_summary[~date_summary["market_gate_open_flag"]], 3, selected_dates, sort_closed, per_year=1, years=("2023", "2024", "2025"), ), "Market gate closed boundary; retained to prove no-trade status is explicit, not silently dropped.", ), ( "PREV_HIGH_REVIEW_RISK", choose_rows( date_summary[date_summary["risk_count"] > 0], 3, selected_dates, sort_risk, ), "FAKE_BREAKOUT_RISK_REVIEW / previous-high risk coverage; chosen by gate availability and risk evidence.", ), ( "MINUTE_COVERAGE_BOUNDARY", choose_boundary_rows(date_summary, selected_dates), "Near minute replay coverage boundary; chosen by entry date proximity to replay start/end.", ), ( "LOW_STRICT_OR_NO_TRADE_BOUNDARY", choose_rows( date_summary, 2, selected_dates, lambda p: p.sort_values( ["market_gate_open_flag", "strict_candidate_count", "pass_count", "candidate_count", "entry_trade_date"], ascending=[False, True, True, False, True], ), ), "Low strict-candidate / no-trade boundary coverage; chosen by low strict/pass count without outcome data.", ), ] seq = len(case_rows) + 1 for bucket, rows, reason in new_specs: for row in rows: entry_date = str(row["entry_trade_date"]) case_id = f"WUJI-EXPAND-CASE-{seq:02d}-{entry_date.replace('-', '')}" case_row = make_case_row( row=row, case_id=case_id, role="NEW_EXPAND_30_CASE", bucket=bucket, source_case_id="", reason=reason, ) case_rows.append(case_row) selection_log.append({**case_row, "selection_action": "NEW_SELECTED", "selection_order": len(case_rows)}) seq += 1 if len(case_rows) != 30: raise RuntimeError(f"Expected 30 case days, got {len(case_rows)}.") if len({r["entry_trade_date"] for r in case_rows}) != 30: raise RuntimeError("Duplicate entry_trade_date detected in selected expansion cases.") return pd.DataFrame(case_rows), pd.DataFrame(selection_log), date_summary def selected_candidates_for_cases(case_index: pd.DataFrame, candidate_ledger: pd.DataFrame) -> pd.DataFrame: rows: list[pd.DataFrame] = [] for _, case in case_index.iterrows(): day = candidate_ledger[candidate_ledger["entry_trade_date"].astype(str) == str(case["entry_trade_date"])].copy() day["candidate_rank"] = pd.to_numeric(day["candidate_rank"], errors="coerce") if str(case["selection_bucket"]) == "PREV_HIGH_REVIEW_RISK": review = day[day["candidate_status"] != "PASS"].sort_values("candidate_rank").head(2) strict = day[day["candidate_status"] == "PASS"].sort_values("candidate_rank").head(3) chosen = pd.concat([strict, review], ignore_index=True).sort_values("candidate_rank").head(5) else: strict = day[day["candidate_status"] == "PASS"].sort_values("candidate_rank").head(5) chosen = strict if len(strict) >= 5 else day.sort_values("candidate_rank").head(5) chosen = chosen.copy() chosen["case_id"] = case["case_id"] chosen["source_case_id"] = case.get("source_case_id", "") chosen["expand_case_role"] = case["expand_case_role"] chosen["case_status"] = case["case_status"] chosen["selection_bucket"] = case["selection_bucket"] chosen["case_candidate_slot"] = range(1, len(chosen) + 1) chosen["selection_reason"] = case["selection_reason"] chosen["no_future_selection_statement"] = NO_FUTURE_SELECTION_STATEMENT rows.append(chosen) return pd.concat(rows, ignore_index=True) if rows else pd.DataFrame() def build_manifest() -> dict: files = [] for path in sorted(ROOT.rglob("*")): if not path.is_file(): continue if "__pycache__" in path.parts: continue rel = path.relative_to(ROOT).as_posix() if rel == "manifest.json": continue files.append({"path": rel, "size": path.stat().st_size, "sha256": sha256_file(path)}) return { "schema_version": "1.0", "run_id": RUN_ID, "generated_at": now_iso(), "stage": "EXPAND_RUN_CONFIG_FROZEN", "overall_status": "READY_FOR_EXPAND_EXECUTION", "files": files, } def markdown_table(df: pd.DataFrame, cols: list[str]) -> str: head = "| " + " | ".join(cols) + " |" sep = "| " + " | ".join(["---"] * len(cols)) + " |" lines = [head, sep] for _, row in df[cols].iterrows(): values = [str(row[c]).replace("\n", " ") for c in cols] lines.append("| " + " | ".join(values) + " |") return "\n".join(lines) def write_config_docs(case_index: pd.DataFrame, selection_log: pd.DataFrame, date_summary: pd.DataFrame, source_summary: dict) -> None: bucket_counts = case_index["selection_bucket"].value_counts().sort_index().to_dict() role_counts = case_index["expand_case_role"].value_counts().sort_index().to_dict() config = { "schema_version": "1.0", "task_id": TASK_ID, "run_id": RUN_ID, "source_run_id": SOURCE_RUN_ID, "design_id": DESIGN_ID, "design_audit_id": DESIGN_AUDIT_ID, "source_audit_ids": SOURCE_AUDIT_IDS, "generated_at": now_iso(), "stage": "EXPAND_RUN_CONFIG_FROZEN", "execution_scope": { "case_days_total": int(len(case_index)), "anchor_case_days": int((case_index["expand_case_role"] == "ANCHOR_REUSED_FROM_AUDITED_7_CASE_PILOT").sum()), "new_case_days": int((case_index["expand_case_role"] == "NEW_EXPAND_30_CASE").sum()), "allowed_scope": "30 controlled case days only", "not_allowed": [ "2023-2026 full-sample execution", "complete baseline return/success/win/drawdown/effectiveness conclusion", "silent sample substitution", ], }, "selection_policy": { "no_future_selection_statement": NO_FUTURE_SELECTION_STATEMENT, "source_fields": [ "candidate_ledger.csv", "candidate_generation_summary.json", "case_index.csv from audited 7-case pilot for anchors", ], "sort_policy": source_summary.get("candidate_sort_policy", {}), "bucket_counts": bucket_counts, "role_counts": role_counts, }, "source_candidate_pool_snapshot": { "candidate_rows": int(len(pd.read_csv(ROOT / "candidate_ledger.csv", encoding="utf-8-sig"))), "candidate_entry_dates": int(date_summary["entry_trade_date"].nunique()), "non_anchor_entry_dates": int(date_summary["entry_trade_date"].nunique() - 7), "market_gate_open_entry_dates": int(date_summary["market_gate_open_flag"].sum()), "market_gate_closed_entry_dates": int((~date_summary["market_gate_open_flag"]).sum()), "source_date_constraints": source_summary.get("date_constraints", {}), }, "artifacts": { "expand_run_config.md": "Human-readable frozen expansion config.", "expand_run_config.json": "Machine-readable frozen expansion config.", "case_index.csv": "Tool-compatible 30-case index.", "expand_case_index.csv": "Expanded case index with audit fields.", "expand_sample_selection_log.csv": "One row per selected case day with role, bucket, reason, and substitution status.", "expand_candidate_selection_ledger.csv": "Frozen selected candidate evidence, independent of image script rewrites.", "candidate_date_summary.csv": "Entry-date summary used for deterministic selection.", }, } write_json(config, ROOT / "expand_run_config.json") write_json(config, ROOT / "run_config.json") md_lines = [ "# 无忌交易系统 30 案例日扩样执行配置冻结", "", f"- task_id:`{TASK_ID}`", f"- run_id:`{RUN_ID}`", f"- source_run_id:`{SOURCE_RUN_ID}`", f"- design_id:`{DESIGN_ID}`", f"- design_audit_id:`{DESIGN_AUDIT_ID}`", f"- generated_at:`{config['generated_at']}`", "- stage:`EXPAND_RUN_CONFIG_FROZEN`", "", "## 选样边界", "", "- 本轮只允许执行 30 个受控案例日:7 个已审核锚点 + 23 个新增分层案例日。", "- 不直接进入 2023-2026 全量执行。", "- 不得把本轮读数写成完整 baseline 成功率、收益率、胜率、回撤或策略有效性结论。", f"- 后验排除声明:{NO_FUTURE_SELECTION_STATEMENT}", "", "## 分层计数", "", markdown_table( pd.DataFrame( [ {"item": "anchor_case_days", "count": config["execution_scope"]["anchor_case_days"]}, {"item": "new_case_days", "count": config["execution_scope"]["new_case_days"]}, *[{"item": k, "count": v} for k, v in bucket_counts.items()], ] ), ["item", "count"], ), "", "## 30 个案例日冻结表", "", markdown_table( case_index[ [ "case_id", "expand_case_role", "entry_trade_date", "signal_trade_date", "selection_bucket", "market_gate_status", "candidate_count", "pass_count", "risk_count", "substitution_flag", ] ], [ "case_id", "expand_case_role", "entry_trade_date", "signal_trade_date", "selection_bucket", "market_gate_status", "candidate_count", "pass_count", "risk_count", "substitution_flag", ], ), "", "## 审核关注点", "", "- 执行审核应检查本配置、候选账本、选样日志、选股图片、买卖裁决图片、订单账本、lot 账本、账户账本、自检和 manifest 是否互相追溯。", "- 替代样本不得静默发生;如执行阶段出现数据缺口,必须写入 `expand_sample_selection_log.csv` 或问题记录并送审。", "- 所有图片仍按无忌专项流程要求作为人工审核第一入口。", "", ] text = "\n".join(md_lines) (ROOT / "expand_run_config.md").write_text(text, encoding="utf-8") (ROOT / "run_config.md").write_text(text, encoding="utf-8") def main() -> None: copy_source_inputs() candidate_ledger, source_case_index, source_summary = load_inputs() case_index, selection_log, date_summary = build_case_selection(candidate_ledger, source_case_index) selected = selected_candidates_for_cases(case_index, candidate_ledger) write_csv(case_index, ROOT / "case_index.csv") write_csv(case_index, ROOT / "expand_case_index.csv") write_csv(selection_log, ROOT / "expand_sample_selection_log.csv") write_csv(date_summary, ROOT / "candidate_date_summary.csv") write_csv(selected, ROOT / "expand_candidate_selection_ledger.csv") write_csv(selected, ROOT / "selected_candidate_ledger.csv") snapshot = { "schema_version": "1.0", "run_id": RUN_ID, "generated_at": now_iso(), "source_run_id": SOURCE_RUN_ID, "source_candidate_rows": int(len(candidate_ledger)), "source_entry_dates": int(date_summary["entry_trade_date"].nunique()), "selected_case_days": int(case_index["entry_trade_date"].nunique()), "selected_candidate_rows": int(len(selected)), "no_future_selection_statement": NO_FUTURE_SELECTION_STATEMENT, } write_json(snapshot, ROOT / "candidate_source_audit_snapshot.json") write_config_docs(case_index, selection_log, date_summary, source_summary) write_json(build_manifest(), ROOT / "manifest.json") print(json.dumps({ "run_id": RUN_ID, "stage": "EXPAND_RUN_CONFIG_FROZEN", "case_days": int(len(case_index)), "selected_candidate_rows": int(len(selected)), "manifest_files": len(build_manifest()["files"]), }, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()