1
2026-06-16 2d8cc2eb4b913c34d8317800458a85939de4da1e
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
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-BASELINE-PILOT-20260607-001"
ROOT = Path(__file__).resolve().parents[1]
 
 
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 check(name: str, passed: bool, detail: str, rows: list[dict]) -> None:
    rows.append(
        {
            "check_name": name,
            "status": "PASS" if passed else "FAIL",
            "detail": detail,
        }
    )
 
 
def main() -> None:
    checks: list[dict] = []
    order_ledger = pd.read_csv(ROOT / "order_ledger.csv", encoding="utf-8-sig")
    lots = pd.read_csv(ROOT / "position_lot_ledger.csv", encoding="utf-8-sig")
    decisions = pd.read_csv(ROOT / "decision_log.csv", encoding="utf-8-sig")
    sell_decisions = pd.read_csv(ROOT / "sell_decision_log.csv", encoding="utf-8-sig")
    exit_resolution = pd.read_csv(ROOT / "exit_resolution_log.csv", encoding="utf-8-sig")
    account = pd.read_csv(ROOT / "daily_account_ledger.csv", encoding="utf-8-sig")
    case_summary = pd.read_csv(ROOT / "case_summary.csv", encoding="utf-8-sig")
    image_manifest = pd.read_csv(ROOT / "image_manifest.csv", encoding="utf-8-sig")
 
    action_counts = order_ledger.action.value_counts().to_dict()
    closed = lots[lots.lot_status == "CLOSED_BY_AI_SELL"].copy()
    unresolved = lots[lots.lot_status != "CLOSED_BY_AI_SELL"].copy()
    check(
        "order_counts",
        action_counts.get("BUY", 0) == 14 and action_counts.get("SELL", 0) == len(closed),
        f"{action_counts}; closed_lots={len(closed)}",
        checks,
    )
 
    stage_counts = decisions.groupby(["decision_stage", "action_status"]).size().to_dict()
    check("decision_log_has_entry_and_exit", decisions.decision_stage.isin(["ENTRY_AI_REVIEW", "EXIT_AI_REVIEW"]).all(), str(stage_counts), checks)
 
    check("lot_status_counts", len(closed) + len(unresolved) == 14, f"closed={len(closed)}, unresolved={len(unresolved)}", checks)
    check(
        "unresolved_status_explicit",
        set(unresolved.lot_status).issubset({"WINDOW_END_VALUATION_ONLY", "EXIT_DATA_GAP_HELD", "EXIT_REVIEW_HELD"}),
        ",".join(sorted(set(unresolved.lot_status))),
        checks,
    )
    check(
        "exit_resolution_all_lots_explicit",
        len(exit_resolution) == len(lots) and not exit_resolution.final_action_status.isna().any(),
        f"resolution_rows={len(exit_resolution)}; statuses={exit_resolution.final_action_status.value_counts().to_dict()}",
        checks,
    )
 
    sell_orders = order_ledger[order_ledger.action == "SELL"].copy()
    merged = sell_orders.merge(lots, left_on="source_lot_id", right_on="trade_lot_id", suffixes=("_order", "_lot"))
    t1_ok = (
        pd.to_datetime(merged.trade_date)
        >= pd.to_datetime(merged.sellable_from_trade_date)
    ).all() and (
        pd.to_datetime(merged.trade_date)
        > pd.to_datetime(merged.entry_trade_date)
    ).all()
    check("t1_guard_for_sell_orders", bool(t1_ok), f"sell_orders={len(sell_orders)}", checks)
 
    recompute_ok = True
    for _, row in closed.iterrows():
        entry = float(row.entry_price)
        exit_price = float(row.exit_price)
        position = float(row.position_pct)
        lot_ret = exit_price / entry - 1
        contrib = lot_ret * position
        recompute_ok = recompute_ok and abs(lot_ret - float(row.lot_return_pct)) < 1e-6
        recompute_ok = recompute_ok and abs(contrib - float(row.account_return_contribution_pct)) < 1e-6
    check("lot_return_recompute", bool(recompute_ok), f"closed_lots={len(closed)}", checks)
 
    case_ok = True
    for _, row in case_summary.iterrows():
        group = lots[lots.case_id == row.case_id]
        contrib = pd.to_numeric(group.account_return_contribution_pct, errors="coerce").fillna(0).sum()
        case_ok = case_ok and abs(contrib - float(row.account_return_closed_lots)) < 1e-6
        case_ok = case_ok and str(row.strict_baseline_return_ready_flag) == "0"
    check("case_summary_recompute", bool(case_ok), f"cases={len(case_summary)}", checks)
 
    account_ok = True
    account_direction_ok = True
    account_balance_ok = True
    final_open_ok = True
    sell_open_position_ok = True
    for case_id, group in account.groupby("case_id"):
        sorted_group = group.sort_values(["event_date", "event_time", "symbol"]).copy()
        prev_cash = 1.0
        prev_open = 0.0
        for _, event in sorted_group.iterrows():
            cash = float(event.cash_pct_after_event)
            open_pos = float(event.open_position_pct_after_event)
            nav = float(event.account_nav_after_event)
            realized_delta = float(event.realized_return_delta)
            account_balance_ok = account_balance_ok and abs(nav - (cash + open_pos)) < 1e-6
            if event.action == "BUY":
                account_direction_ok = account_direction_ok and cash < prev_cash and open_pos > prev_open and abs(realized_delta) < 1e-9
            elif event.action == "SELL":
                account_direction_ok = account_direction_ok and cash > prev_cash and open_pos < prev_open
                sell_open_position_ok = sell_open_position_ok and open_pos >= -1e-9
            prev_cash = cash
            prev_open = open_pos
        last = sorted_group.iloc[-1]
        last_nav = float(last.account_nav_after_event)
        expected = 1.0 + pd.to_numeric(lots[lots.case_id == case_id].account_return_contribution_pct, errors="coerce").fillna(0).sum()
        account_ok = account_ok and abs(last_nav - expected) < 1e-6
        expected_open = pd.to_numeric(
            lots[
                (lots.case_id == case_id)
                & (lots.lot_status != "CLOSED_BY_AI_SELL")
            ].position_pct,
            errors="coerce",
        ).fillna(0).sum()
        final_open_ok = final_open_ok and abs(float(last.open_position_pct_after_event) - expected_open) < 1e-6
    check("daily_account_ledger_recompute", bool(account_ok), f"event_rows={len(account)}", checks)
    check("account_cash_position_direction", bool(account_direction_ok), "BUY cash down/open up; SELL cash up/open down", checks)
    check("account_nav_equals_cash_plus_open_position", bool(account_balance_ok), "nav equals cash plus open position after each event", checks)
    check("account_final_open_position_matches_unclosed_lots", bool(final_open_ok), "final open position equals unresolved lot position sum per case", checks)
    check("sell_reduces_open_position_without_negative_open", bool(sell_open_position_ok), "SELL events reduce open position and never leave negative open position", checks)
 
    lookahead_ok = True
    for df in [order_ledger, decisions, sell_decisions]:
        if "lookahead_violation_flag" in df.columns:
            lookahead_ok = lookahead_ok and not df.lookahead_violation_flag.astype(str).str.lower().isin(["true", "1"]).any()
    check("lookahead_flags_zero", bool(lookahead_ok), "order/decision/sell_decision flags checked", checks)
 
    chart_rows = []
    image_ok = True
    for _, row in image_manifest.iterrows():
        path = ROOT / str(row.path)
        exists = path.exists()
        actual_hash = sha256_file(path) if exists else ""
        hash_ok = exists and actual_hash == str(row.sha256)
        image_ok = image_ok and hash_ok
        chart_rows.append(
            {
                "case_id": row.case_id,
                "symbol": row.symbol,
                "chart_role": row.chart_role,
                "path": row.path,
                "exists": str(exists),
                "sha256_match": str(hash_ok),
                "decision_time": row.decision_time,
            }
        )
    chart_audit = pd.DataFrame(chart_rows)
    chart_audit.to_csv(ROOT / "chart_evidence_audit.csv", index=False, encoding="utf-8-sig")
    role_counts = image_manifest.chart_role.value_counts().to_dict()
    check("image_manifest_hash_match", bool(image_ok), f"images={len(image_manifest)}, roles={role_counts}", checks)
    expected_images = 35 + 60 + 14 + 14 + action_counts.get("SELL", 0)
    check("image_manifest_expected_count", len(image_manifest) == expected_images, f"images={len(image_manifest)}, expected={expected_images}", checks)
 
    strict_ready = False
    check("return_stat_not_ready_boundary", not strict_ready and (case_summary.strict_baseline_return_ready_flag.astype(str) == "0").all(), "RETURN_STAT_READY is intentionally false", checks)
 
    checks_df = pd.DataFrame(checks)
    checks_df.to_csv(ROOT / "self_check_items.csv", index=False, encoding="utf-8-sig")
    fail_count = int((checks_df.status == "FAIL").sum())
    summary = {
        "schema_version": "1.0",
        "run_id": RUN_ID,
        "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
        "stage": "STRUCTURE_PILOT_EXIT_REVIEW_RESOLVED_SELF_CHECK_DONE",
        "overall_status": "PASS_FOR_STRUCTURE_PILOT_RETURN_STAT_HELD" if fail_count == 0 else "FAIL",
        "fail_count": fail_count,
        "check_count": len(checks_df),
        "order_counts": action_counts,
        "lot_status_counts": lots.lot_status.value_counts().to_dict(),
        "image_role_counts": role_counts,
        "closed_lot_account_return_sum": float(pd.to_numeric(lots.account_return_contribution_pct, errors="coerce").fillna(0).sum()),
        "strict_baseline_return_ready_flag": False,
        "boundary": "Machine self-check passed only for the current structure pilot exit-review-resolved boundary. Execution audit is still required and return statistics are not ready.",
        "artifacts": {
            "self_check_items.csv": {
                "size": (ROOT / "self_check_items.csv").stat().st_size,
                "sha256": sha256_file(ROOT / "self_check_items.csv"),
            },
            "chart_evidence_audit.csv": {
                "size": (ROOT / "chart_evidence_audit.csv").stat().st_size,
                "sha256": sha256_file(ROOT / "chart_evidence_audit.csv"),
            },
        },
    }
    (ROOT / "self_check.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (ROOT / "self_check.md").write_text(
        "\n".join(
            [
                "# self_check",
                "",
                f"run_id:`{RUN_ID}`",
                f"状态:`{summary['overall_status']}`",
                f"检查项:{len(checks_df)},失败:{fail_count}",
                "",
                "关键读数:",
                f"- BUY:{action_counts.get('BUY', 0)}",
                f"- SELL:{action_counts.get('SELL', 0)}",
                f"- 图片:{len(image_manifest)}",
                f"- 已闭合 lot 账户贡献合计:{summary['closed_lot_account_return_sum']:.4%}",
                "",
                "边界:当前只通过结构试点自检,执行审核未通过且未到 RETURN_STAT_READY 前不得引用收益统计。",
                "",
            ]
        ),
        encoding="utf-8",
    )
 
 
if __name__ == "__main__":
    main()