from __future__ import annotations import csv import hashlib import json import os import re from collections import Counter, defaultdict from datetime import datetime, timezone, timedelta from pathlib import Path import pandas as pd import pymysql RUN_ID = "RUN-ANA-WUJI-V1-STRICT-NOTE-FULL-RERUN-20260614-001" ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = ROOT.parents[2] LOCAL_DB_INDEX = Path(r"D:\strategy_project\s-system-doc\observer\天下模型沉淀\数据库索引数据.md") SOURCE_NOTE = PROJECT_ROOT / "ana-doc" / "wuji" / "profile" / "source_note" / "笔记精简版.md" FULL_SOURCE_RUN = PROJECT_ROOT / "ana-data" / "result" / "RUN-ANA-WUJI-FULL-2023-2026-20260608-001" V1_SOURCE_RUN = PROJECT_ROOT / "ana-data" / "result" / "RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001" TZ = timezone(timedelta(hours=8)) CONFIG = { "schema_version": "1.0", "run_id": RUN_ID, "stage": "STRICT_NOTE_FULL_RERUN_STRICT_BUY_POOL_REBUILT_EXECUTION_PREP_READY", "source_note": "ana-doc/wuji/profile/source_note/笔记精简版.md", "source_runs": { "old_full_run": "RUN-ANA-WUJI-FULL-2023-2026-20260608-001", "old_v1_sell_rolling_run": "RUN-ANA-WUJI-STRICT-SELL-ROLLING-REPAIR-20260609-001", }, "data_window": { "pull_daily_start": "2022-10-01", "minute_replay_entry_start": "2023-03-24", "minute_replay_entry_end": "2026-04-20", "calendar_start": "2022-10-01", "calendar_end": "2026-04-20", }, "data_sources": { "daily_price_table": "tianxia.a_share_daily_price", "market_breadth_table": "tianxia.ts_market_breadth_daily_cache", "calendar_table": "tianxia.a_share_trading_calendar", "price_adjustment": "source_table_as_is", "limitup_price_source": "high_price / prev_close - 1", "volume_source": "volume", "amount_source": "amount", }, "strict_buy_rules": { "prior_limitup_window_trading_days": 30, "prior_limitup_excludes_signal_day": True, "limitup_tolerance_rate": 0.0005, "limitup_rates": { "MAINBOARD_DEFAULT": 0.10, "STAR_688_SH": 0.20, "CHINEXT_300_301_SZ": 0.20, "BEIJING_BJ": 0.30, }, "volume_ratio_prev5_min": 2.0, "volume_history_required_trading_days": 5, "pullback_from_latest_prior_limitup_close_pct_max": -3.0, "upper_shadow_pct_proxy_min": 3.0, "upper_shadow_range_ratio_proxy_min": 0.4, "prev_high_window_trading_days": 60, "prev_high_min_history_trading_days": 20, "prev_high_ref_policy": "FIRST_PREVIOUS_HIGH_IN_60D_WINDOW", "prev_high_volume_filter": "if signal high touches previous 60-day high, signal volume must exceed reference high-day volume", "market_gate_policy": "SIGNAL_DAY_UP_3000", "market_gate_up_count_min": 3000, "selection_top_n_per_entry_date": 5, "sort_policy": "upper_shadow_pct desc -> volume_ratio desc -> amount desc -> symbol asc", }, "manual_review_boundary": { "bottom_support_strength": "SUPPORT_REVIEW_REQUIRED", "buy_point_confirmation": "MANUAL_OR_AI_MANUAL_REVIEW_REQUIRED", "strict_sell_trend_rolling_actions": "EXTERNAL_MANUAL_DECISION_SOURCE_REQUIRED", }, "citation_boundary": [ "This package rebuilds the strict BUY candidate pool only; it is not a completed V1 performance rerun.", "Do not cite success rate, return, win rate, drawdown, or strategy effectiveness from this package.", "Old V1 readouts remain down-read as based on the upstream wide/proxy BUY pool until strict replay passes review.", ], } def now_iso() -> str: return datetime.now(TZ).isoformat(timespec="seconds") def read_password() -> str: env = os.environ.get("TIANXIA_MYSQL_PASSWORD") or os.environ.get("MYSQL_PWD") if env: return env text = LOCAL_DB_INDEX.read_text(encoding="utf-8") match = re.search(r"^\s*-\s*密码:`([^`]+)`", text, re.MULTILINE) if not match: raise RuntimeError("Unable to read local MySQL credential from approved local index.") return match.group(1) def get_conn(): return pymysql.connect( host="127.0.0.1", port=3306, user="root", password=read_password(), database="tianxia", charset="utf8mb4", connect_timeout=5, read_timeout=180, ) 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 write_csv(df: pd.DataFrame, name: str) -> Path: path = ROOT / name path.parent.mkdir(parents=True, exist_ok=True) df.to_csv(path, index=False, encoding="utf-8-sig") return path def write_json(obj: dict, name: str) -> Path: path = ROOT / name path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8") return path def board_group(symbol: str) -> str: if symbol.endswith(".BJ"): return "BEIJING_BJ" if symbol.startswith("688") and symbol.endswith(".SH"): return "STAR_688_SH" if (symbol.startswith("300") or symbol.startswith("301")) and symbol.endswith(".SZ"): return "CHINEXT_300_301_SZ" return "MAINBOARD_DEFAULT" def limit_rate(symbol: str) -> float: return CONFIG["strict_buy_rules"]["limitup_rates"][board_group(symbol)] def load_source_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: w = CONFIG["data_window"] with get_conn() as conn: daily = pd.read_sql( """ SELECT trade_date, symbol, open_price, high_price, low_price, close_price, volume, amount FROM a_share_daily_price WHERE trade_date BETWEEN %(start)s AND %(end)s ORDER BY symbol, trade_date """, conn, params={"start": w["pull_daily_start"], "end": w["minute_replay_entry_end"]}, ) breadth = pd.read_sql( """ SELECT trade_date, stock_count, up_count, flat_count, down_count, run_id, price_source_table FROM ts_market_breadth_daily_cache WHERE trade_date BETWEEN %(start)s AND %(end)s AND scope_type='ALL_A_SHARE' AND scope_value='ALL' ORDER BY trade_date """, conn, params={"start": w["calendar_start"], "end": w["minute_replay_entry_end"]}, ) calendar = pd.read_sql( """ SELECT calendar_date, is_trading_day, trade_date_rank FROM a_share_trading_calendar WHERE calendar_date BETWEEN %(start)s AND %(end)s ORDER BY calendar_date """, conn, params={"start": w["calendar_start"], "end": w["calendar_end"]}, ) return daily, breadth, calendar def enrich_daily(daily: pd.DataFrame) -> pd.DataFrame: daily = daily.copy() daily["trade_date"] = pd.to_datetime(daily["trade_date"]) for col in ["open_price", "high_price", "low_price", "close_price", "volume", "amount"]: daily[col] = pd.to_numeric(daily[col], errors="coerce") daily = daily.sort_values(["symbol", "trade_date"]).reset_index(drop=True) grouped = daily.groupby("symbol", group_keys=False) daily["prev_close"] = grouped["close_price"].shift(1) daily["prev5_avg_volume"] = grouped["volume"].transform( lambda s: s.shift(1).rolling(5, min_periods=5).mean() ) daily["volume_ratio"] = daily["volume"] / daily["prev5_avg_volume"] daily["upper_shadow_abs"] = daily["high_price"] - daily[["open_price", "close_price"]].max(axis=1) daily["day_range_abs"] = daily["high_price"] - daily["low_price"] daily["upper_shadow_pct"] = daily["upper_shadow_abs"] / daily["prev_close"] * 100.0 daily["upper_shadow_range_ratio"] = daily["upper_shadow_abs"] / daily["day_range_abs"] daily["daily_return_pct"] = (daily["close_price"] / daily["prev_close"] - 1.0) * 100.0 daily["market_group"] = daily["symbol"].map(board_group) daily["limit_rate"] = daily["symbol"].map(limit_rate) tol = CONFIG["strict_buy_rules"]["limitup_tolerance_rate"] daily["strict_limitup_return_rate"] = daily["high_price"] / daily["prev_close"] - 1.0 daily["strict_limitup_event_flag"] = ( daily["prev_close"].gt(0) & daily["strict_limitup_return_rate"].ge(daily["limit_rate"] - tol) ) prev60_high = [] prev60_high_volume = [] prev60_high_ref_date = [] prior_limitup_flag = [] latest_prior_limitup_date = [] latest_prior_limitup_close = [] pullback_low = [] pullback_low_date = [] prior30_count = [] prev_high_policy = [] reason_counter = Counter() for _symbol, g in daily.groupby("symbol", sort=False): highs = g["high_price"].to_numpy() lows = g["low_price"].to_numpy() closes = g["close_price"].to_numpy() vols = g["volume"].to_numpy() dates = pd.to_datetime(g["trade_date"]).tolist() limit_flags = g["strict_limitup_event_flag"].to_numpy() for i in range(len(g)): p60_start = max(0, i - CONFIG["strict_buy_rules"]["prev_high_window_trading_days"]) if i - p60_start >= CONFIG["strict_buy_rules"]["prev_high_min_history_trading_days"]: wh = highs[p60_start:i] max_pos = int(wh.argmax()) ref_idx = p60_start + max_pos prev60_high.append(highs[ref_idx]) prev60_high_volume.append(vols[ref_idx]) prev60_high_ref_date.append(dates[ref_idx]) prev_high_policy.append(CONFIG["strict_buy_rules"]["prev_high_ref_policy"]) else: prev60_high.append(float("nan")) prev60_high_volume.append(float("nan")) prev60_high_ref_date.append(pd.NaT) prev_high_policy.append("") p30_start = max(0, i - CONFIG["strict_buy_rules"]["prior_limitup_window_trading_days"]) prior30_count.append(i - p30_start) limit_indices = [idx for idx in range(p30_start, i) if bool(limit_flags[idx])] if not limit_indices: prior_limitup_flag.append(False) latest_prior_limitup_date.append(pd.NaT) latest_prior_limitup_close.append(float("nan")) pullback_low.append(float("nan")) pullback_low_date.append(pd.NaT) if i - p30_start < CONFIG["strict_buy_rules"]["prior_limitup_window_trading_days"]: reason_counter["PRIOR_30_TRADING_DAY_WINDOW_INCOMPLETE_HELD"] += 1 else: reason_counter["STRICT_PRIOR_LIMITUP_FAIL"] += 1 continue latest_idx = limit_indices[-1] lo_slice = lows[latest_idx + 1 : i + 1] if len(lo_slice) == 0: lo_slice = lows[latest_idx : i + 1] lo_offset = 0 else: lo_offset = latest_idx + 1 min_pos = int(lo_slice.argmin()) min_idx = lo_offset + min_pos prior_limitup_flag.append(True) latest_prior_limitup_date.append(dates[latest_idx]) latest_prior_limitup_close.append(closes[latest_idx]) pullback_low.append(lows[min_idx]) pullback_low_date.append(dates[min_idx]) daily["prev60_high"] = prev60_high daily["prev60_high_volume"] = prev60_high_volume daily["prev60_high_ref_date"] = prev60_high_ref_date daily["prev60_high_ref_policy"] = prev_high_policy daily["prior30_trading_day_count"] = prior30_count daily["prior_strict_limitup_30_flag"] = prior_limitup_flag daily["latest_prior_strict_limitup_date"] = latest_prior_limitup_date daily["latest_prior_strict_limitup_close"] = latest_prior_limitup_close daily["pullback_low_since_latest_limitup"] = pullback_low daily["pullback_low_since_latest_limitup_date"] = pullback_low_date daily["pullback_from_latest_limitup_close_pct"] = ( daily["pullback_low_since_latest_limitup"] / daily["latest_prior_strict_limitup_close"] - 1.0 ) * 100.0 daily["touch_prev_high_flag"] = daily["prev60_high"].gt(0) & daily["high_price"].ge(daily["prev60_high"] * 0.995) daily["prev_high_volume_pass_flag"] = (~daily["touch_prev_high_flag"]) | daily["volume"].gt(daily["prev60_high_volume"]) return daily def build_entry_links(daily: pd.DataFrame, breadth: pd.DataFrame) -> pd.DataFrame: breadth = breadth.copy() breadth["signal_trade_date"] = pd.to_datetime(breadth["trade_date"]) breadth = breadth.drop(columns=["trade_date"]) trade_dates = sorted(pd.to_datetime(daily["trade_date"].drop_duplicates()).tolist()) links = pd.DataFrame({"signal_trade_date": trade_dates[:-1], "entry_trade_date": trade_dates[1:]}) w = CONFIG["data_window"] links = links[ (links["entry_trade_date"] >= pd.Timestamp(w["minute_replay_entry_start"])) & (links["entry_trade_date"] <= pd.Timestamp(w["minute_replay_entry_end"])) ].copy() links = links.merge(breadth, on="signal_trade_date", how="left") links["market_gate_open_flag"] = links["up_count"].fillna(-1).ge(CONFIG["strict_buy_rules"]["market_gate_up_count_min"]) links["market_gate_status"] = links["market_gate_open_flag"].map( { True: "MKT_GATE_OPEN_SIGNAL_DAY_UP_3000", False: "NO_TRADE_MARKET_GATE_CLOSED_SIGNAL_DAY", } ) return links def build_candidates(daily: pd.DataFrame, links: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: signals = daily.merge( links, left_on="trade_date", right_on="signal_trade_date", how="inner", suffixes=("", "_breadth"), ) r = CONFIG["strict_buy_rules"] checks = { "prev_close_ok": signals["prev_close"].gt(0), "prev5_volume_ok": signals["prev5_avg_volume"].gt(0), "prior_limitup_ok": signals["prior_strict_limitup_30_flag"], "volume_ratio_ok": signals["volume_ratio"].ge(r["volume_ratio_prev5_min"]), "pullback_ok": signals["pullback_from_latest_limitup_close_pct"].le( r["pullback_from_latest_prior_limitup_close_pct_max"] ), "upper_shadow_pct_ok": signals["upper_shadow_pct"].ge(r["upper_shadow_pct_proxy_min"]), "upper_shadow_range_ok": signals["upper_shadow_range_ratio"].ge(r["upper_shadow_range_ratio_proxy_min"]), "prev_high_volume_ok": signals["prev_high_volume_pass_flag"], } for col, value in checks.items(): signals[col] = value.fillna(False) failure_reasons = [] for row in signals.itertuples(index=False): reasons = [] for name in checks: if not bool(getattr(row, name)): reasons.append(name.replace("_ok", "").upper() + "_FAIL") if row.prior30_trading_day_count < r["prior_limitup_window_trading_days"]: reasons.append("PRIOR_30_TRADING_DAY_WINDOW_INCOMPLETE_HELD") if not reasons: reasons.append("STRICT_CODE_PASS_SUPPORT_REVIEW_REQUIRED") failure_reasons.append(";".join(dict.fromkeys(reasons))) signals["strict_code_status"] = [ "STRICT_CODE_PASS_SUPPORT_REVIEW_REQUIRED" if x == "STRICT_CODE_PASS_SUPPORT_REVIEW_REQUIRED" else "STRICT_CODE_FAIL_OR_HELD" for x in failure_reasons ] signals["strict_code_reason"] = failure_reasons pass_mask = signals["strict_code_status"].eq("STRICT_CODE_PASS_SUPPORT_REVIEW_REQUIRED") candidates = signals[pass_mask].copy() candidates = candidates.sort_values( ["entry_trade_date", "upper_shadow_pct", "volume_ratio", "amount", "symbol"], ascending=[True, False, False, False, True], ) candidates["candidate_rank"] = candidates.groupby("entry_trade_date").cumcount() + 1 candidates["candidate_id"] = [ f"STRICT-NOTE-{d:%Y%m%d}-{rank:02d}-{sym.replace('.', '_')}" for d, rank, sym in zip(candidates["entry_trade_date"], candidates["candidate_rank"], candidates["symbol"]) ] candidates["support_manual_decision"] = "SUPPORT_REVIEW_REQUIRED" candidates["buy_point_manual_decision"] = "BUY_POINT_REVIEW_REQUIRED" candidates["source_note_semantics"] = "prior_limitup_30_ex_signal;volume_ratio_ge_2;pullback_ge_3pct;long_upper_shadow_proxy;bottom_support_manual" reason_counter = Counter() for reason in signals["strict_code_reason"].astype(str): for item in reason.split(";"): reason_counter[item] += 1 reason_summary = pd.DataFrame( [{"reason": k, "rows": v} for k, v in sorted(reason_counter.items())] ) failed = signals[~pass_mask].copy() review_sample = ( failed.sort_values(["strict_code_reason", "entry_trade_date", "symbol"]) .groupby("strict_code_reason", dropna=False) .head(50) .reset_index(drop=True) ) review_sample["review_row_id"] = [f"STRICT-NOTE-REVIEW-SAMPLE-{i+1:06d}" for i in range(len(review_sample))] return candidates.reset_index(drop=True), review_sample, reason_summary def build_case_index(candidates: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: top_n = CONFIG["strict_buy_rules"]["selection_top_n_per_entry_date"] selected = candidates[candidates["candidate_rank"].le(top_n)].copy() selected["case_id"] = [f"WUJI-STRICT-{d:%Y%m%d}" for d in selected["entry_trade_date"]] case_index = ( selected.groupby(["case_id", "entry_trade_date", "signal_trade_date", "market_gate_status", "market_gate_open_flag"], dropna=False) .agg( selected_candidate_count=("candidate_id", "count"), symbols=("symbol", lambda s: ";".join(s)), candidate_ids=("candidate_id", lambda s: ";".join(s)), ) .reset_index() .sort_values("entry_trade_date") ) return selected.reset_index(drop=True), case_index.reset_index(drop=True) def format_dates(df: pd.DataFrame) -> pd.DataFrame: out = df.copy() for col in out.columns: if pd.api.types.is_datetime64_any_dtype(out[col]): out[col] = out[col].dt.strftime("%Y-%m-%d") return out def write_readme(summary: dict) -> Path: path = ROOT / "README.md" text = f"""# {RUN_ID} ## Purpose This package is the execution-prep strict BUY candidate rebuild for the Wuji V1 strict-note full rerun. It rebuilds the BUY candidate pool from source daily data using `笔记精简版.md` strict limit-up and volume conditions. ## What This Package Is - strict BUY candidate evidence package - frozen run configuration - selected top-5-per-entry-date candidate ledger for the next replay stage - self-check, source artifact manifest, and package manifest ## What This Package Is Not - not a completed V1 strict performance rerun - not a buy recommendation - not a success-rate, return-rate, win-rate, drawdown, or strategy-effectiveness conclusion ## Key Counts - strict code-pass candidates: {summary['counts']['strict_code_pass_candidates']} - selected candidates for replay: {summary['counts']['selected_candidates']} - case dates prepared: {summary['counts']['case_dates_prepared']} - market gate open case dates: {summary['counts']['market_gate_open_case_dates']} - market gate closed case dates: {summary['counts']['market_gate_closed_case_dates']} ## Next Required Step Submit this package to `case_analysis.reviewer` for execution-prep review. If it passes, the next stage is minute buy-point review, external manual/AI-manual decision source, V1 sell/trend/rolling replay, lifecycle/readability package generation, and final citation review. """ path.write_text(text, encoding="utf-8") return path def build_source_manifest() -> pd.DataFrame: rows = [] for source_name, path in [ ("source_note", SOURCE_NOTE), ("old_full_summary", FULL_SOURCE_RUN / "summary.json"), ("old_full_candidate_ledger", FULL_SOURCE_RUN / "candidate_ledger.csv"), ("old_v1_summary", V1_SOURCE_RUN / "summary.json"), ]: exists = path.exists() rows.append( { "source_name": source_name, "path": str(path), "exists": exists, "size": path.stat().st_size if exists else "", "sha256": sha256_file(path) if exists and path.is_file() else "", } ) return pd.DataFrame(rows) def build_manifest() -> pd.DataFrame: rows = [] for path in sorted(ROOT.rglob("*")): if path.is_file() and path.name not in {"manifest.csv", "manifest.json"}: rows.append( { "path": rel(path), "size": path.stat().st_size, "sha256": sha256_file(path), } ) return pd.DataFrame(rows) def main() -> None: ROOT.mkdir(parents=True, exist_ok=True) daily_raw, breadth, calendar = load_source_data() daily = enrich_daily(daily_raw) links = build_entry_links(daily, breadth) candidates, review_pool, reason_summary = build_candidates(daily, links) selected, case_index = build_case_index(candidates) counts = { "source_daily_rows": int(len(daily_raw)), "source_symbols": int(daily_raw["symbol"].nunique()), "entry_links": int(len(links)), "strict_code_pass_candidates": int(len(candidates)), "selected_candidates": int(len(selected)), "case_dates_prepared": int(case_index["case_id"].nunique()), "market_gate_open_case_dates": int(case_index["market_gate_open_flag"].sum()), "market_gate_closed_case_dates": int((~case_index["market_gate_open_flag"]).sum()), "support_review_required_candidates": int(candidates["support_manual_decision"].eq("SUPPORT_REVIEW_REQUIRED").sum()), } reason_counts = dict(zip(reason_summary["reason"], reason_summary["rows"])) generated_at = now_iso() config = dict(CONFIG) config["generated_at"] = generated_at write_json(config, "strict_note_run_config.json") (ROOT / "strict_note_run_config.md").write_text( "# strict_note_run_config\n\n" f"- run_id: {RUN_ID}\n" f"- generated_at: {generated_at}\n" "- direct_blueprint: ana-doc/wuji/profile/source_note/笔记精简版.md\n" "- strict_limitup: prior 30 trading days, exclude signal day, board-specific 10%/20%/30%, high_price / prev_close - 1, tolerance 0.05 percentage point\n" "- volume_ratio: signal volume / previous 5 trading-day average >= 2.0\n" "- pullback: latest prior strict limit-up close to signal-window low <= -3.0%\n" "- long_upper_shadow: execution proxy upper_shadow_pct >= 3.0 and upper_shadow_range_ratio >= 0.4\n" "- bottom_support: SUPPORT_REVIEW_REQUIRED, not auto-passed by code\n" "- market_gate: signal-day ALL_A_SHARE up_count >= 3000\n" "- selection: top 5 per entry_trade_date after strict code pass; no BUY is generated in this package\n", encoding="utf-8", ) candidate_cols = [ "candidate_id", "symbol", "market_group", "signal_trade_date", "entry_trade_date", "candidate_rank", "market_gate_status", "market_gate_open_flag", "up_count", "open_price", "high_price", "low_price", "close_price", "prev_close", "volume", "amount", "prev5_avg_volume", "volume_ratio", "limit_rate", "strict_limitup_return_rate", "prior_strict_limitup_30_flag", "latest_prior_strict_limitup_date", "latest_prior_strict_limitup_close", "pullback_low_since_latest_limitup", "pullback_low_since_latest_limitup_date", "pullback_from_latest_limitup_close_pct", "upper_shadow_pct", "upper_shadow_range_ratio", "prev60_high", "prev60_high_volume", "prev60_high_ref_date", "prev60_high_ref_policy", "touch_prev_high_flag", "prev_high_volume_pass_flag", "support_manual_decision", "buy_point_manual_decision", "source_note_semantics", ] selected_cols = ["case_id"] + candidate_cols review_cols = [ "review_row_id", "symbol", "market_group", "signal_trade_date", "entry_trade_date", "strict_code_status", "strict_code_reason", "market_gate_status", "up_count", "prev_close", "high_price", "low_price", "close_price", "volume", "prev5_avg_volume", "volume_ratio", "limit_rate", "strict_limitup_return_rate", "prior_strict_limitup_30_flag", "latest_prior_strict_limitup_date", "pullback_from_latest_limitup_close_pct", "upper_shadow_pct", "upper_shadow_range_ratio", "prev_high_volume_pass_flag", ] write_csv(format_dates(candidates[candidate_cols]), "strict_note_candidate_ledger.csv") write_csv(format_dates(selected[selected_cols]), "strict_note_selected_candidate_ledger.csv") write_csv(format_dates(case_index), "strict_note_case_index.csv") write_csv(format_dates(review_pool[review_cols]), "strict_note_review_pool_ledger.csv") write_csv(reason_summary, "strict_note_review_pool_reason_summary.csv") source_manifest = build_source_manifest() write_csv(source_manifest, "source_artifact_manifest.csv") self_items = [ ("SOURCE_NOTE_EXISTS", SOURCE_NOTE.exists(), str(SOURCE_NOTE)), ("STRICT_CONFIG_WRITTEN", (ROOT / "strict_note_run_config.json").exists(), "strict_note_run_config.json"), ("PRIOR_LIMITUP_EXCLUDES_SIGNAL_DAY", True, "computed from rows [i-30:i], signal row excluded"), ("VOLUME_RATIO_MIN_2_FROZEN", CONFIG["strict_buy_rules"]["volume_ratio_prev5_min"] == 2.0, "volume_ratio_prev5_min=2.0"), ("BOTTOM_SUPPORT_NOT_AUTO_PASSED", candidates["support_manual_decision"].eq("SUPPORT_REVIEW_REQUIRED").all(), "all candidates require support review"), ("SELECTED_TOP_N_PER_ENTRY", selected.groupby("entry_trade_date")["candidate_id"].count().le(CONFIG["strict_buy_rules"]["selection_top_n_per_entry_date"]).all(), "top_n<=5"), ("NO_BUY_ORDERS_GENERATED", not (ROOT / "strict_order_ledger.csv").exists(), "candidate package only"), ("SOURCE_ARTIFACTS_EXIST", source_manifest["exists"].all(), "source_artifact_manifest.csv"), ] self_df = pd.DataFrame( [ {"item": item, "status": "PASS" if ok else "FAIL", "detail": detail} for item, ok, detail in self_items ] ) write_csv(self_df, "self_check_items.csv") self_json = { "run_id": RUN_ID, "generated_at": generated_at, "stage": CONFIG["stage"], "overall_status": "PASS_FOR_STRICT_BUY_POOL_EXECUTION_PREP_REVIEW_READY" if self_df["status"].eq("PASS").all() else "FAIL", "pass_count": int(self_df["status"].eq("PASS").sum()), "fail_count": int(self_df["status"].eq("FAIL").sum()), } write_json(self_json, "self_check.json") summary = { "run_id": RUN_ID, "generated_at": generated_at, "stage": CONFIG["stage"], "counts": counts, "review_pool_reason_counts": reason_counts, "citation_boundary": CONFIG["citation_boundary"], "next_step": "submit strict BUY pool execution-prep package to case_analysis.reviewer", } write_json(summary, "summary.json") (ROOT / "summary.md").write_text( "# Strict Note BUY Pool Rebuild Summary\n\n" f"- run_id: {RUN_ID}\n" f"- generated_at: {generated_at}\n" f"- strict code-pass candidates: {counts['strict_code_pass_candidates']}\n" f"- selected candidates: {counts['selected_candidates']}\n" f"- case dates prepared: {counts['case_dates_prepared']}\n" f"- market gate open case dates: {counts['market_gate_open_case_dates']}\n" f"- market gate closed case dates: {counts['market_gate_closed_case_dates']}\n\n" "Boundary: this package rebuilds the strict BUY pool only. It does not contain BUY orders, SELL orders, returns, or strategy conclusions.\n", encoding="utf-8", ) write_readme(summary) manifest = build_manifest() write_csv(manifest, "manifest.csv") write_json( { "run_id": RUN_ID, "generated_at": generated_at, "file_count": int(len(manifest)), "files": manifest.to_dict(orient="records"), }, "manifest.json", ) if __name__ == "__main__": main()