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()