from __future__ import annotations
|
|
import csv
|
import hashlib
|
import json
|
from datetime import datetime, timezone, timedelta
|
from pathlib import Path
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
RUN_ID = "RUN-ANA-WUJI-STRICT-CLOSED-234-REVIEW-GUIDE-20260608-001"
|
TASK_ID = "ANA-WUJI-STRICT-CLOSED-234-REVIEW-GUIDE-20260608"
|
SOURCE_RUN_ID = "RUN-ANA-WUJI-FULL-2023-2026-20260608-001"
|
SOURCE_AUDIT_ID = "AUDIT-ANA-WUJI-FULL-2023-2026-20260608-EXEC-REREVIEW-002"
|
FINAL_AUDIT_ID = "AUDIT-ANA-WUJI-FINAL-CONCLUSION-20260608-EXEC-001"
|
|
PACKAGE_DIR = PROJECT_ROOT / "ana-data" / "result" / RUN_ID
|
SOURCE_DIR = PROJECT_ROOT / "ana-data" / "result" / SOURCE_RUN_ID
|
TZ = timezone(timedelta(hours=8))
|
|
|
def rel(path: Path) -> str:
|
return path.resolve().relative_to(PROJECT_ROOT).as_posix()
|
|
|
def read_csv(path: Path) -> list[dict[str, str]]:
|
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]], fieldnames: list[str]) -> None:
|
with path.open("w", encoding="utf-8", newline="") as f:
|
writer = csv.DictWriter(f, fieldnames=fieldnames, lineterminator="\n")
|
writer.writeheader()
|
for row in rows:
|
writer.writerow({k: row.get(k, "") for k in fieldnames})
|
|
|
def sha256(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 read_json(path: Path) -> dict:
|
with path.open("r", encoding="utf-8") as f:
|
return json.load(f)
|
|
|
def write_json(path: Path, data: dict | list) -> None:
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
def md_link(path: Path, label: str | None = None) -> str:
|
target = rel(path)
|
return f"[{label or target}](../../../{target})"
|
|
|
def build() -> None:
|
generated_at = datetime.now(TZ).isoformat(timespec="seconds")
|
PACKAGE_DIR.mkdir(parents=True, exist_ok=True)
|
|
case_scope = read_csv(SOURCE_DIR / "full_return_stat_case_scope.csv")
|
case_summary = {r["case_id"]: r for r in read_csv(SOURCE_DIR / "case_summary.csv")}
|
selected = read_csv(SOURCE_DIR / "full_selected_candidate_ledger.csv")
|
lots = read_csv(SOURCE_DIR / "full_return_stat_lot_scope.csv")
|
orders = read_csv(SOURCE_DIR / "order_ledger.csv")
|
summary = read_json(SOURCE_DIR / "full_return_stat_summary.json")
|
|
selected_by_case: dict[str, list[dict[str, str]]] = {}
|
for row in selected:
|
selected_by_case.setdefault(row["case_id"], []).append(row)
|
|
lot_counts: dict[str, dict[str, int]] = {}
|
for row in lots:
|
case_id = row["case_id"]
|
item = lot_counts.setdefault(case_id, {"lots": 0, "closed": 0, "boundary": 0})
|
item["lots"] += 1
|
if row.get("lot_scope") == "STRICT_CLOSED_LOT_RECALC_ONLY":
|
item["closed"] += 1
|
else:
|
item["boundary"] += 1
|
|
order_counts: dict[str, dict[str, int]] = {}
|
for row in orders:
|
case_id = row["case_id"]
|
item = order_counts.setdefault(case_id, {"orders": 0, "buy": 0, "sell": 0})
|
item["orders"] += 1
|
action = row.get("action", "")
|
if action == "BUY":
|
item["buy"] += 1
|
elif action == "SELL":
|
item["sell"] += 1
|
|
strict_cases = [
|
row
|
for row in case_scope
|
if row.get("case_scope_status") == "PRIMARY_STRICT_CLOSED_CASE"
|
and row.get("primary_strict_closed_case_flag") == "1"
|
]
|
strict_cases.sort(key=lambda r: (r["entry_trade_date"], r["case_id"]))
|
|
index_rows: list[dict[str, object]] = []
|
file_rows: list[dict[str, object]] = []
|
for row in strict_cases:
|
case_id = row["case_id"]
|
case_dir = SOURCE_DIR / "cases" / case_id
|
cs = case_summary.get(case_id, {})
|
candidates = selected_by_case.get(case_id, [])
|
symbols = ";".join(c.get("symbol", "") for c in candidates[:5])
|
candidate_ids = ";".join(c.get("candidate_id", "") for c in candidates[:5])
|
lc = lot_counts.get(case_id, {"lots": 0, "closed": 0, "boundary": 0})
|
oc = order_counts.get(case_id, {"orders": 0, "buy": 0, "sell": 0})
|
account_return = row.get("account_return_closed_lots") or cs.get("account_return_closed_lots", "")
|
success_flag = row.get("primary_case_success_flag", "")
|
|
index_rows.append(
|
{
|
"case_id": case_id,
|
"batch_id": row.get("batch_id", ""),
|
"entry_trade_date": row.get("entry_trade_date", ""),
|
"signal_trade_date": row.get("signal_trade_date", ""),
|
"market_gate_status": row.get("market_gate_status", ""),
|
"primary_scope": row.get("case_scope_status", ""),
|
"success_flag": success_flag,
|
"account_return_closed_lots": account_return,
|
"buy_lot_count": row.get("buy_lot_count", ""),
|
"closed_lot_count": row.get("closed_lot_count", ""),
|
"order_buy_count": oc["buy"],
|
"order_sell_count": oc["sell"],
|
"top5_symbols": symbols,
|
"top5_candidate_ids": candidate_ids,
|
"case_image_board": rel(case_dir / "case_image_board.md"),
|
"case_story_board": rel(case_dir / "case_story_board.md"),
|
"case_candidate_ledger": rel(case_dir / "candidate_ledger.csv"),
|
}
|
)
|
file_rows.append(
|
{
|
"case_id": case_id,
|
"case_dir": rel(case_dir),
|
"case_image_board": rel(case_dir / "case_image_board.md"),
|
"case_story_board": rel(case_dir / "case_story_board.md"),
|
"case_candidate_ledger": rel(case_dir / "candidate_ledger.csv"),
|
"case_image_manifest": rel(case_dir / "image_manifest.csv"),
|
"source_case_scope": rel(SOURCE_DIR / "full_return_stat_case_scope.csv"),
|
"source_lot_scope": rel(SOURCE_DIR / "full_return_stat_lot_scope.csv"),
|
"source_case_summary": rel(SOURCE_DIR / "case_summary.csv"),
|
"source_decision_log": rel(SOURCE_DIR / "decision_log.csv"),
|
"source_order_ledger": rel(SOURCE_DIR / "order_ledger.csv"),
|
"source_position_lot_ledger": rel(SOURCE_DIR / "position_lot_ledger.csv"),
|
"source_daily_account_ledger": rel(SOURCE_DIR / "daily_account_ledger.csv"),
|
}
|
)
|
|
readme_path = PACKAGE_DIR / "README.md"
|
navigation_path = PACKAGE_DIR / "strict_closed_234_navigation.md"
|
index_path = PACKAGE_DIR / "strict_closed_234_case_index.csv"
|
file_map_path = PACKAGE_DIR / "strict_closed_234_file_map.csv"
|
self_check_path = PACKAGE_DIR / "self_check.json"
|
self_check_items_path = PACKAGE_DIR / "self_check_items.csv"
|
manifest_path = PACKAGE_DIR / "manifest.json"
|
|
write_csv(
|
index_path,
|
index_rows,
|
[
|
"case_id",
|
"batch_id",
|
"entry_trade_date",
|
"signal_trade_date",
|
"market_gate_status",
|
"primary_scope",
|
"success_flag",
|
"account_return_closed_lots",
|
"buy_lot_count",
|
"closed_lot_count",
|
"order_buy_count",
|
"order_sell_count",
|
"top5_symbols",
|
"top5_candidate_ids",
|
"case_image_board",
|
"case_story_board",
|
"case_candidate_ledger",
|
],
|
)
|
write_csv(
|
file_map_path,
|
file_rows,
|
[
|
"case_id",
|
"case_dir",
|
"case_image_board",
|
"case_story_board",
|
"case_candidate_ledger",
|
"case_image_manifest",
|
"source_case_scope",
|
"source_lot_scope",
|
"source_case_summary",
|
"source_decision_log",
|
"source_order_ledger",
|
"source_position_lot_ledger",
|
"source_daily_account_ledger",
|
],
|
)
|
|
primary = summary["primary_scope"]
|
coverage = summary["coverage_scope"]
|
lot_scope = summary["lot_recalc_scope"]
|
boundary = summary["boundary"]
|
readme = f"""# 234 个严格闭合案例阅读包
|
|
本包只负责把已审核通过的无忌全量执行包中 `PRIMARY_STRICT_CLOSED_CASE` 的 234 个案例串起来,方便同事拿到 git 仓库后按图、按表、按账本复核。它不新增交易结论,不重跑候选池、买卖裁决或账户账本。
|
|
## 当前可引用边界
|
|
- 来源 run:`{SOURCE_RUN_ID}`
|
- 来源执行复审:`{SOURCE_AUDIT_ID}`
|
- 最终结论引用审核:`{FINAL_AUDIT_ID}`
|
- `RETURN_STAT_READY=false`
|
- 主口径:`PRIMARY_STRICT_CLOSED_CASE`,{primary["case_count"]} 个严格闭合 case,正收益 {primary["positive_case_count"]} 个,成功率读数 {primary["candidate_success_rate_for_audit_only"]:.10f},账户贡献合计 {primary["account_return_sum_for_audit_only"]:.8f}
|
- 覆盖口径:`ALL_ENTRY_DATE_COVERAGE`,{coverage["entry_date_count"]} 个 entry date,市场闸门打开 {coverage["market_gate_open_entry_dates"]},关闭 {coverage["market_gate_closed_entry_dates"]}
|
- 辅助 lot 口径:`STRICT_CLOSED_LOT_RECALC_ONLY`,{lot_scope["total_lot_count"]} 个 lot,闭合 {lot_scope["closed_lot_count"]},边界 {lot_scope["unresolved_lot_count"]}
|
- 边界:{boundary["case_boundary_count"]} 个 case 边界和 {boundary["lot_boundary_count"]} 个 lot 边界不得混入主口径
|
|
## 先看哪些文件
|
|
1. `ana-doc/wuji/无忌交易系统234个严格闭合案例阅读说明.md`:给同事看的总说明,解释怎么看、每类文件干什么。
|
2. `strict_closed_234_case_index.csv`:234 个主口径案例索引,一行一个 case,含日期、批次、收益读数、top5 股票和图片入口。
|
3. `strict_closed_234_file_map.csv`:逐 case 文件地图,说明这个 case 的图片板、故事板、候选账本、全局账本在哪里。
|
4. `ana-data/result/{SOURCE_RUN_ID}/case_image_board.md`:全量包根图片入口,可跳到所有 case。
|
5. `ana-data/result/{SOURCE_RUN_ID}/full_return_stat_case_scope.csv`:正式判断某个 case 是否属于主口径的 scope 表。
|
|
## 单个案例怎么读
|
|
1. 在 `strict_closed_234_case_index.csv` 选一个 `case_id`。
|
2. 打开该行的 `case_image_board`。先看日 K 选股图,再看 1 分钟买点图,再看卖点裁决图。
|
3. 打开同目录 `case_story_board.md`,按文字串起选股、买入、卖出、lot 状态和边界。
|
4. 回到全局表核对:`full_return_stat_case_scope.csv` 看主口径,`full_return_stat_lot_scope.csv` 看 lot,`order_ledger.csv` 看订单,`position_lot_ledger.csv` 看每笔 lot,`daily_account_ledger.csv` 看账户资金流。
|
5. 只把 `PRIMARY_STRICT_CLOSED_CASE` 行用于主口径读数;不要把市场闸门关闭、无 BUY、未真实 SELL 或数据缺口样本混进来。
|
"""
|
readme_path.write_text(readme, encoding="utf-8")
|
|
navigation = f"""# 234 个严格闭合案例导航
|
|
生成时间:{generated_at}
|
|
## 读图顺序
|
|
每个 case 的 `case_image_board.md` 都是人工审核第一入口:
|
|
1. 选股日 K 图:验证信号日以前的形态、放量、长上影、近期涨停记忆和候选排序。
|
2. 买点 1 分钟图:验证入场日早盘是否触发买点,且不读取未来数据。
|
3. 卖点 1 分钟图:验证 T+1 之后的卖点确认和卖出原因。
|
4. 案例读数区:确认当前收益口径为 `PRIMARY_STRICT_CLOSED_CASE`,边界分类为无。
|
|
## 表格串联方式
|
|
| 你要核对什么 | 看哪个文件 | 用什么键串起来 |
|
|---|---|---|
|
| 234 个主口径案例名单 | `strict_closed_234_case_index.csv` | `case_id` |
|
| 某个 case 是否真的进主口径 | `../{SOURCE_RUN_ID}/full_return_stat_case_scope.csv` | `case_id` |
|
| 入选前 5 候选股票 | `../{SOURCE_RUN_ID}/full_selected_candidate_ledger.csv` | `case_id` / `candidate_id` |
|
| 买入 / 卖出裁决 | `../{SOURCE_RUN_ID}/decision_log.csv` | `case_id` / `candidate_id` |
|
| 真实订单事件 | `../{SOURCE_RUN_ID}/order_ledger.csv` | `case_id` / `order_id` |
|
| 每笔仓位 lot | `../{SOURCE_RUN_ID}/position_lot_ledger.csv` | `case_id` / `trade_lot_id` |
|
| lot 口径归属 | `../{SOURCE_RUN_ID}/full_return_stat_lot_scope.csv` | `case_id` / `trade_lot_id` |
|
| 账户现金和持仓流 | `../{SOURCE_RUN_ID}/daily_account_ledger.csv` | `case_id` / 日期 |
|
| case 汇总收益 | `../{SOURCE_RUN_ID}/case_summary.csv` | `case_id` |
|
| 被排除边界 | `../{SOURCE_RUN_ID}/full_return_stat_boundary_table.csv` | `case_id` / `trade_lot_id` |
|
|
## 不要这样读
|
|
- 不要把 234 个主口径案例说成 743 个 entry date 的全体表现。
|
- 不要把辅助 lot 口径包装成 case 成功率。
|
- 不要忽略 `RETURN_STAT_READY=false`。
|
- 不要只看 CSV 数字而跳过图片入口;无忌专项流程要求图片是人工审核第一入口。
|
"""
|
navigation_path.write_text(navigation, encoding="utf-8")
|
|
checks = [
|
{
|
"check_id": "SOURCE_RUN_PRESENT",
|
"status": "PASS" if SOURCE_DIR.exists() else "FAIL",
|
"detail": rel(SOURCE_DIR),
|
},
|
{
|
"check_id": "STRICT_CLOSED_CASE_COUNT_234",
|
"status": "PASS" if len(strict_cases) == 234 else "FAIL",
|
"detail": f"strict_closed_cases={len(strict_cases)}",
|
},
|
{
|
"check_id": "ALL_STRICT_CASES_HAVE_IMAGE_BOARD",
|
"status": "PASS"
|
if all((SOURCE_DIR / "cases" / r["case_id"] / "case_image_board.md").exists() for r in strict_cases)
|
else "FAIL",
|
"detail": "case_image_board.md exists for every strict closed case",
|
},
|
{
|
"check_id": "ALL_STRICT_CASES_HAVE_STORY_BOARD",
|
"status": "PASS"
|
if all((SOURCE_DIR / "cases" / r["case_id"] / "case_story_board.md").exists() for r in strict_cases)
|
else "FAIL",
|
"detail": "case_story_board.md exists for every strict closed case",
|
},
|
{
|
"check_id": "SOURCE_SUMMARY_RETURN_STAT_HELD",
|
"status": "PASS"
|
if summary.get("return_stat_ready") is False
|
and summary.get("full_baseline_conclusion_allowed") is False
|
else "FAIL",
|
"detail": "return_stat_ready=false; full_baseline_conclusion_allowed=false",
|
},
|
]
|
write_csv(self_check_items_path, checks, ["check_id", "status", "detail"])
|
self_check = {
|
"schema_version": "1.0",
|
"task_id": TASK_ID,
|
"run_id": RUN_ID,
|
"source_run_id": SOURCE_RUN_ID,
|
"generated_at": generated_at,
|
"status": "PASS_FOR_STRICT_CLOSED_234_REVIEW_GUIDE_READY"
|
if all(c["status"] == "PASS" for c in checks)
|
else "FAIL",
|
"pass_count": sum(1 for c in checks if c["status"] == "PASS"),
|
"fail_count": sum(1 for c in checks if c["status"] == "FAIL"),
|
"strict_closed_case_count": len(strict_cases),
|
"return_stat_ready": False,
|
"notes": [
|
"本包是阅读说明和索引包,不新增收益结论。",
|
"所有主口径读数必须回到来源 full_return_stat_* 产物和审计边界。",
|
],
|
}
|
write_json(self_check_path, self_check)
|
|
manifest_files = [
|
readme_path,
|
navigation_path,
|
index_path,
|
file_map_path,
|
self_check_path,
|
self_check_items_path,
|
Path(__file__),
|
]
|
manifest = {
|
"schema_version": "1.0",
|
"task_id": TASK_ID,
|
"run_id": RUN_ID,
|
"source_run_id": SOURCE_RUN_ID,
|
"generated_at": generated_at,
|
"stage": "STRICT_CLOSED_234_REVIEW_GUIDE_READY",
|
"strict_closed_case_count": len(strict_cases),
|
"return_stat_ready": False,
|
"files": [
|
{
|
"path": rel(path),
|
"size": path.stat().st_size,
|
"sha256": sha256(path),
|
}
|
for path in manifest_files
|
],
|
}
|
write_json(manifest_path, manifest)
|
|
print(json.dumps(self_check, ensure_ascii=False, indent=2))
|
|
|
if __name__ == "__main__":
|
build()
|