from __future__ import annotations
|
|
import hashlib
|
import json
|
from datetime import datetime
|
from pathlib import Path
|
|
import pandas as pd
|
|
|
RUN_ID = "RUN-ANA-WUJI-FULL-2023-2026-20260608-001"
|
TASK_ID = "ANA-WUJI-BASELINE-2023-2026"
|
DESIGN_ID = "DESIGN-WUJI-FULL-2023-2026-20260608"
|
DESIGN_AUDIT_ID = "AUDIT-ANA-WUJI-FULL-2023-2026-20260608-DESIGN-001"
|
SOURCE_RUN_ID = "RUN-ANA-WUJI-EXPAND-30-20260608-001"
|
ROOT = Path(__file__).resolve().parents[1]
|
BATCH_SIZE = 50
|
|
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",
|
"AUDIT-ANA-WUJI-EXPAND-30-20260608-EXEC-REREVIEW-001",
|
]
|
|
|
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, name: str) -> None:
|
df.to_csv(ROOT / name, index=False, encoding="utf-8-sig")
|
|
|
def write_json(data: dict, name: str) -> None:
|
(ROOT / name).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 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["market_gate_open_bool"] = bool_series(df["market_gate_open_flag"])
|
df["strict_bool"] = bool_series(df["strict_candidate_flag"])
|
for col in [
|
"candidate_rank",
|
"up_count",
|
"down_count",
|
"stock_count",
|
"volume_ratio",
|
"upper_shadow_pct",
|
"amount",
|
]:
|
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
rows = (
|
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", "min"),
|
max_candidate_rank=("candidate_rank", "max"),
|
)
|
.sort_values("entry_trade_date")
|
.reset_index(drop=True)
|
)
|
rows["market_gate_open_flag"] = rows["market_gate_open_flag"].astype(bool)
|
return rows
|
|
|
def bucket_for(row: pd.Series) -> str:
|
if not bool(row["market_gate_open_flag"]):
|
return "FULL_MARKET_GATE_CLOSED"
|
if int(row["risk_count"]) > 0:
|
return "FULL_OPEN_WITH_RISK_CANDIDATES"
|
return "FULL_OPEN_TOP5_CANDIDATES"
|
|
|
def main() -> None:
|
candidate_ledger = pd.read_csv(ROOT / "candidate_ledger.csv", encoding="utf-8-sig")
|
candidate_ledger["entry_trade_date"] = candidate_ledger["entry_trade_date"].astype(str)
|
candidate_ledger["signal_trade_date"] = candidate_ledger["signal_trade_date"].astype(str)
|
candidate_ledger["candidate_rank"] = pd.to_numeric(candidate_ledger["candidate_rank"], errors="coerce")
|
date_summary = build_date_summary(candidate_ledger)
|
|
case_rows: list[dict] = []
|
batch_rows: list[dict] = []
|
for idx, row in date_summary.iterrows():
|
entry_date = str(row["entry_trade_date"])
|
batch_no = idx // BATCH_SIZE + 1
|
batch_id = f"B{batch_no:03d}"
|
case_id = f"WUJI-FULL-{entry_date.replace('-', '')}"
|
gate_open = bool(row["market_gate_open_flag"])
|
case_rows.append(
|
{
|
"case_id": case_id,
|
"batch_id": batch_id,
|
"full_case_role": "FULL_2023_2026_ENTRY_DATE",
|
"expand_case_role": "FULL_2023_2026_ENTRY_DATE",
|
"entry_trade_date": entry_date,
|
"signal_trade_date": str(row["signal_trade_date"]),
|
"selection_bucket": bucket_for(row),
|
"case_status": "FULL_SELECTED_FOR_REPLAY" if gate_open else "FULL_NO_TRADE_MARKET_GATE_CLOSED",
|
"market_gate_status": str(row["market_gate_status"]),
|
"market_gate_open_flag": gate_open,
|
"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": "Full-sample deterministic replay: use audited candidate ledger top 5 for this entry date.",
|
"selection_basis": "candidate_rank already freezes upper_shadow_pct desc -> volume_ratio desc -> amount desc.",
|
"substitution_flag": False,
|
"substitution_reason": "",
|
"no_future_selection_statement": (
|
"Full-sample selection uses candidate_ledger signal/entry fields only. "
|
"It does not read buy/sell logs, future prices, returns, lot outcomes, or drawdown."
|
),
|
}
|
)
|
|
case_index = pd.DataFrame(case_rows)
|
selected_rows: list[pd.DataFrame] = []
|
for _, case in case_index.iterrows():
|
top = (
|
candidate_ledger[candidate_ledger.entry_trade_date == case.entry_trade_date]
|
.sort_values("candidate_rank")
|
.head(5)
|
.copy()
|
)
|
if len(top) < 5:
|
raise RuntimeError(f"entry_trade_date {case.entry_trade_date} has fewer than 5 candidates")
|
top["case_id"] = case.case_id
|
top["batch_id"] = case.batch_id
|
top["case_status"] = case.case_status
|
top["selection_bucket"] = case.selection_bucket
|
selected_rows.append(top)
|
selected = pd.concat(selected_rows, ignore_index=True)
|
|
for batch_id, group in case_index.groupby("batch_id", sort=True):
|
selected_count = int((selected.batch_id == batch_id).sum())
|
batch_rows.append(
|
{
|
"batch_id": batch_id,
|
"batch_order": int(batch_id[1:]),
|
"case_count": int(len(group)),
|
"selected_candidate_rows": selected_count,
|
"entry_date_start": str(group.entry_trade_date.min()),
|
"entry_date_end": str(group.entry_trade_date.max()),
|
"batch_dir": f"batches/{batch_id}",
|
"batch_status": "FULL_BATCH_CONFIG_FROZEN",
|
"stop_on_self_check_fail": True,
|
}
|
)
|
|
batch_index = pd.DataFrame(batch_rows)
|
diff = pd.DataFrame(
|
[
|
{"metric": "candidate_rows", "expected": 37116, "actual": len(candidate_ledger), "status": "PASS" if len(candidate_ledger) == 37116 else "DIFF"},
|
{"metric": "entry_trade_dates", "expected": 743, "actual": case_index.entry_trade_date.nunique(), "status": "PASS" if case_index.entry_trade_date.nunique() == 743 else "DIFF"},
|
{"metric": "selected_candidate_rows", "expected": 3715, "actual": len(selected), "status": "PASS" if len(selected) == 3715 else "DIFF"},
|
{"metric": "min_candidates_per_entry_date", "expected": 5, "actual": int(date_summary.candidate_count.min()), "status": "PASS" if int(date_summary.candidate_count.min()) >= 5 else "DIFF"},
|
]
|
)
|
|
write_csv(date_summary, "full_candidate_date_summary.csv")
|
write_csv(case_index, "full_case_index.csv")
|
write_csv(case_index, "case_index.csv")
|
write_csv(batch_index, "full_batch_index.csv")
|
write_csv(selected, "full_selected_candidate_ledger.csv")
|
write_csv(selected, "selected_candidate_ledger.csv")
|
write_csv(diff, "full_candidate_pool_diff.csv")
|
|
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": "FULL_RUN_CONFIG_FROZEN",
|
"scope": {
|
"candidate_rows": int(len(candidate_ledger)),
|
"entry_trade_dates": int(case_index.entry_trade_date.nunique()),
|
"market_gate_open_entry_dates": int((case_index.market_gate_status == "MKT_GATE_OPEN_PREV_DAY_UP_3000").sum()),
|
"market_gate_closed_entry_dates": int((case_index.market_gate_status == "NO_TRADE_MARKET_GATE_CLOSED").sum()),
|
"selected_candidate_rows": int(len(selected)),
|
"candidate_per_entry_date": 5,
|
"batch_size": BATCH_SIZE,
|
"batch_count": int(len(batch_index)),
|
"batch_order": "entry_trade_date ascending",
|
},
|
"selection_policy": {
|
"sort_policy": "candidate_rank from audited candidate_ledger; rank freezes upper_shadow_pct desc -> volume_ratio desc -> amount desc",
|
"top_n_per_entry_date": 5,
|
"no_future_selection_statement": (
|
"Selection uses only audited candidate ledger signal/entry fields; it does not read future prices, returns, buy/sell outcomes, drawdown, or return statistics."
|
),
|
},
|
"return_policy": {
|
"primary": "PRIMARY_STRICT_CLOSED_CASE",
|
"coverage": "ALL_ENTRY_DATE_COVERAGE",
|
"lot_recalc": "STRICT_CLOSED_LOT_RECALC_ONLY",
|
"return_stat_ready": False,
|
},
|
"stop_conditions": [
|
"any batch self_check FAIL",
|
"candidate pool count or entry date count differs without full_candidate_pool_diff and review",
|
"selected candidate ledger row count != 3715",
|
"T+1 or lookahead violation",
|
"ledger recompute failure",
|
"image board link failure",
|
],
|
"artifacts": {
|
"full_run_config.md": {"path": "full_run_config.md"},
|
"full_run_config.json": {"path": "full_run_config.json"},
|
"full_case_index.csv": {"path": "full_case_index.csv"},
|
"full_batch_index.csv": {"path": "full_batch_index.csv"},
|
"full_selected_candidate_ledger.csv": {"path": "full_selected_candidate_ledger.csv"},
|
"full_candidate_pool_diff.csv": {"path": "full_candidate_pool_diff.csv"},
|
},
|
}
|
write_json(config, "full_run_config.json")
|
(ROOT / "full_run_config.md").write_text(
|
"\n".join(
|
[
|
f"# {RUN_ID} full_run_config",
|
"",
|
f"- 设计 ID:`{DESIGN_ID}`",
|
f"- 设计审核 ID:`{DESIGN_AUDIT_ID}`",
|
"- 阶段:`FULL_RUN_CONFIG_FROZEN`",
|
f"- 全量 entry date:{case_index.entry_trade_date.nunique()}",
|
f"- 候选池行数:{len(candidate_ledger)}",
|
f"- 选中候选:{len(selected)}(每个 entry date 前 5)",
|
f"- 批次:{len(batch_index)} 批,每批最多 {BATCH_SIZE} 个案例日",
|
f"- 市场闸门打开 entry date:{config['scope']['market_gate_open_entry_dates']}",
|
f"- 市场闸门关闭 entry date:{config['scope']['market_gate_closed_entry_dates']}",
|
"",
|
"## 收益口径",
|
"",
|
"- 主口径:`PRIMARY_STRICT_CLOSED_CASE`",
|
"- 覆盖口径:`ALL_ENTRY_DATE_COVERAGE`",
|
"- 辅助 lot 口径:`STRICT_CLOSED_LOT_RECALC_ONLY`",
|
"- `RETURN_STAT_READY=false`,执行审核通过且审核员明确允许前不得引用完整结论。",
|
"",
|
"## 停止条件",
|
"",
|
"- 任一批自检 FAIL 必须停止。",
|
"- `full_selected_candidate_ledger.csv` 不等于 3715 行必须停止。",
|
"- T+1、未来函数、账本复算、图片链接或 manifest 出现阻断错误必须停止。",
|
"",
|
]
|
),
|
encoding="utf-8",
|
)
|
|
summary = {
|
"schema_version": "1.0",
|
"run_id": RUN_ID,
|
"generated_at": now_iso(),
|
"stage": "FULL_RUN_CONFIG_FROZEN",
|
"case_count": int(len(case_index)),
|
"selected_candidate_rows": int(len(selected)),
|
"batch_count": int(len(batch_index)),
|
"artifacts": {
|
name: {
|
"size": (ROOT / name).stat().st_size,
|
"sha256": sha256_file(ROOT / name),
|
}
|
for name in [
|
"full_run_config.md",
|
"full_run_config.json",
|
"full_case_index.csv",
|
"full_batch_index.csv",
|
"full_selected_candidate_ledger.csv",
|
"full_candidate_pool_diff.csv",
|
"case_index.csv",
|
"selected_candidate_ledger.csv",
|
]
|
},
|
}
|
write_json(summary, "full_config_freeze_summary.json")
|
(ROOT / "full_config_freeze_summary.md").write_text(
|
"\n".join(
|
[
|
"# full_config_freeze_summary",
|
"",
|
f"- run_id:`{RUN_ID}`",
|
f"- case_count:{len(case_index)}",
|
f"- selected_candidate_rows:{len(selected)}",
|
f"- batch_count:{len(batch_index)}",
|
"- 当前只冻结配置,不产生交易结论。",
|
"",
|
]
|
),
|
encoding="utf-8",
|
)
|
|
|
if __name__ == "__main__":
|
main()
|