1
2026-07-19 228d838fdb7f7dde7edc4993fdbb9654c9c31df7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
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()