from __future__ import annotations import importlib.util import json from pathlib import Path import pandas as pd SCRIPT_DIR = Path(__file__).resolve().parent BUILD_PATH = SCRIPT_DIR / "build_v1_lifecycle_chart_supplement.py" spec = importlib.util.spec_from_file_location("build_v1_lifecycle_chart_supplement", BUILD_PATH) build = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(build) def read_csv(path: Path) -> pd.DataFrame: return pd.read_csv(path, dtype=str, keep_default_na=False, encoding="utf-8-sig").fillna("") def existing_png(path: Path) -> str: return path.relative_to(build.PACKAGE_ROOT).as_posix() if path.exists() else "" def main() -> None: build.PACKAGE_ROOT.mkdir(parents=True, exist_ok=True) build.write_source_manifest() v1_index = read_csv(build.FINAL_ROOT / "v1_case_readout_index.csv") order = read_csv(build.SOURCE_ROOT / "strict_order_ledger.csv") for col in ["trade_date", "t1_sellable_from_trade_date"]: if col in order.columns: order[col] = order[col].map(build.norm_date) order["trade_time"] = order["trade_time"].map(build.norm_time) for col in ["price", "position_delta_pct"]: order[col] = pd.to_numeric(order[col], errors="coerce") order = order[order["action"].isin(["BUY", "SELL"])].copy() buy_case_ids = set(order[order["action"] == "BUY"]["case_id"].tolist()) v1_index = v1_index[v1_index["case_id"].isin(buy_case_ids)].copy() v1_by_case = v1_index.set_index("case_id", drop=False).to_dict("index") order = order[order["case_id"].isin(v1_by_case)].copy() group_rows = [] for (case_id, symbol), group in order.groupby(["case_id", "symbol"]): buy_rows = group[group["action"] == "BUY"].sort_values(["trade_date", "trade_time", "order_id"]) if buy_rows.empty: continue sell_rows = group[group["action"] == "SELL"].sort_values(["trade_date", "trade_time", "order_id"]) last_rows = sell_rows if not sell_rows.empty else group.sort_values(["trade_date", "trade_time", "order_id"]) first_buy = buy_rows.iloc[0] last_sell = last_rows.iloc[-1] group_rows.append( { "case_id": case_id, "symbol": symbol, "first_buy_date": first_buy["trade_date"], "first_buy_time": first_buy["trade_time"], "first_buy_datetime": f"{first_buy['trade_date']} {first_buy['trade_time']}", "last_sell_date": last_sell["trade_date"] if last_sell["action"] == "SELL" else "", "last_sell_time": last_sell["trade_time"] if last_sell["action"] == "SELL" else "", "last_sell_datetime": f"{last_sell['trade_date']} {last_sell['trade_time']}" if last_sell["action"] == "SELL" else "", "buy_count": int(len(buy_rows)), "sell_count": int(len(sell_rows)), } ) chart_rows: list[dict] = [] missing_rows: list[dict] = [] symbol_index_rows: list[dict] = [] tx_index_rows: list[dict] = [] case_rows: list[dict] = [] case_symbol_rows: dict[str, list[dict]] = {} case_tx_rows: dict[str, list[dict]] = {} for row in group_rows: case_id = row["case_id"] symbol = row["symbol"] safe_symbol = symbol.replace(".", "_") img_dir = build.PACKAGE_ROOT / "cases" / case_id / "img" last_date = row["last_sell_date"] or row["first_buy_date"] lifecycle_path = next( img_dir.glob(f"09_lifecycle_daily_50pre_20post_{safe_symbol}_{row['first_buy_date'].replace('-', '')}_{last_date.replace('-', '')}.png"), None, ) lifecycle_rel = existing_png(lifecycle_path) if lifecycle_path else "" if lifecycle_rel: chart_rows.append( { "case_id": case_id, "symbol": symbol, "chart_role": "symbol_lifecycle_daily_50pre_20post_audit_view", "action": "LIFECYCLE", "trade_date": row["first_buy_date"], "trade_time": row["first_buy_time"], "path": lifecycle_rel, "source_order_id": "", "status": "PASS", "note": "首个BUY前50个交易日至最后SELL后20个交易日;仅作audit_view。", "size": lifecycle_path.stat().st_size, "sha256": build.sha256_file(lifecycle_path), } ) else: missing_rows.append({"case_id": case_id, "symbol": symbol, "kind": "daily_lifecycle", "date": row["first_buy_date"]}) group = order[(order["case_id"] == case_id) & (order["symbol"] == symbol)].copy() tx_count = 0 for trade_date_key, day_orders in group.groupby(["trade_date"]): trade_date = trade_date_key[0] if isinstance(trade_date_key, tuple) else trade_date_key tx_path = img_dir / f"10_transaction_day_minute_line_{safe_symbol}_{str(trade_date).replace('-', '')}.png" tx_rel = existing_png(tx_path) if not tx_rel: missing_rows.append({"case_id": case_id, "symbol": symbol, "kind": "transaction_minute", "date": trade_date}) continue tx_count += 1 tx_row = { "case_id": case_id, "symbol": symbol, "trade_date": trade_date, "order_count": int(len(day_orders)), "buy_order_count": int((day_orders["action"] == "BUY").sum()), "sell_order_count": int((day_orders["action"] == "SELL").sum()), "minute_chart_path": tx_rel, } tx_index_rows.append(tx_row) case_tx_rows.setdefault(case_id, []).append(tx_row) chart_rows.append( { "case_id": case_id, "symbol": symbol, "chart_role": "transaction_day_minute_line_audit_view", "action": "BUY_SELL_DAY", "trade_date": trade_date, "trade_time": "", "path": tx_rel, "source_order_id": ";".join(day_orders["order_id"].tolist()), "status": "PASS", "note": "有交易产生的交易日整日分时;标记当天全部BUY/SELL订单。", "size": tx_path.stat().st_size, "sha256": build.sha256_file(tx_path), } ) meta = v1_by_case[case_id] symbol_row = { **row, "v1_return_scope": meta.get("v1_return_scope", ""), "success_flag": meta.get("success_flag", ""), "account_return_closed_lots": meta.get("account_return_closed_lots", ""), "lifecycle_chart_path": lifecycle_rel, "transaction_day_chart_count": tx_count, "case_lifecycle_board": f"cases/{case_id}/case_lifecycle_board.md", "source_case_image_board": f"{build.SOURCE_RUN_ID}/cases/{case_id}/case_image_board.md", } symbol_index_rows.append(symbol_row) case_symbol_rows.setdefault(case_id, []).append(symbol_row) for case_id, symbols_for_case in case_symbol_rows.items(): build.build_case_board(case_id, pd.Series(v1_by_case[case_id]), symbols_for_case, case_tx_rows.get(case_id, [])) for case_id, meta in v1_by_case.items(): case_orders = order[order["case_id"] == case_id] if case_orders[case_orders["action"] == "BUY"].empty: continue case_rows.append( { "case_id": case_id, "v1_return_scope": meta.get("v1_return_scope", ""), "success_flag": meta.get("success_flag", ""), "account_return_closed_lots": meta.get("account_return_closed_lots", ""), "symbol_count": len(case_symbol_rows.get(case_id, [])), "buy_order_count": int((case_orders["action"] == "BUY").sum()), "sell_order_count": int((case_orders["action"] == "SELL").sum()), "case_lifecycle_board": f"cases/{case_id}/case_lifecycle_board.md", } ) pd.DataFrame(symbol_index_rows).to_csv(build.PACKAGE_ROOT / "lifecycle_symbol_index.csv", index=False, encoding="utf-8-sig") pd.DataFrame(tx_index_rows).to_csv(build.PACKAGE_ROOT / "transaction_day_chart_index.csv", index=False, encoding="utf-8-sig") pd.DataFrame(chart_rows).to_csv(build.PACKAGE_ROOT / "chart_evidence_audit.csv", index=False, encoding="utf-8-sig") pd.DataFrame(missing_rows).to_csv(build.PACKAGE_ROOT / "missing_chart_inputs.csv", index=False, encoding="utf-8-sig") pd.DataFrame(case_rows).to_csv(build.PACKAGE_ROOT / "case_lifecycle_index.csv", index=False, encoding="utf-8-sig") build.build_root_docs(case_rows, symbol_index_rows, tx_index_rows) link_rows = [] for md in sorted(build.PACKAGE_ROOT.rglob("*.md")): link_rows.extend(build.local_link_targets(md)) link_df = pd.DataFrame(link_rows) link_df.to_csv(build.PACKAGE_ROOT / "link_evidence_audit.csv", index=False, encoding="utf-8-sig") expected_lifecycle = len(group_rows) expected_tx = len(order.groupby(["case_id", "symbol", "trade_date"]).size()) lifecycle_actual = int((pd.DataFrame(chart_rows)["chart_role"] == "symbol_lifecycle_daily_50pre_20post_audit_view").sum()) if chart_rows else 0 tx_actual = int((pd.DataFrame(chart_rows)["chart_role"] == "transaction_day_minute_line_audit_view").sum()) if chart_rows else 0 link_missing = 0 if link_df.empty else int((~link_df["target_exists"]).sum()) self_checks = [ ("FINAL_V1_PACKAGE_EXISTS", build.FINAL_ROOT.exists(), str(build.FINAL_ROOT)), ("SOURCE_V1_PACKAGE_EXISTS", build.SOURCE_ROOT.exists(), str(build.SOURCE_ROOT)), ("V1_BUY_CASES_251", len(v1_index) == 251, f"buy_cases={len(v1_index)}"), ("LIFECYCLE_CHARTS_COMPLETE", lifecycle_actual == expected_lifecycle, f"actual={lifecycle_actual}, expected={expected_lifecycle}"), ("TRANSACTION_DAY_CHARTS_COMPLETE", tx_actual == expected_tx, f"actual={tx_actual}, expected={expected_tx}"), ("MISSING_CHART_INPUTS_ZERO", len(missing_rows) == 0, f"missing={len(missing_rows)}"), ("MARKDOWN_LOCAL_LINKS_REACHABLE", link_missing == 0, f"links={len(link_df)}, missing={link_missing}"), ("RETURN_STAT_READY_FALSE_PRESERVED", True, "supplement package does not change V1 return_stat_ready=false"), ] self_df = pd.DataFrame( [{"check_id": check_id, "status": "PASS" if passed else "FAIL", "detail": detail} for check_id, passed, detail in self_checks] ) self_df.to_csv(build.PACKAGE_ROOT / "self_check_items.csv", index=False, encoding="utf-8-sig") fail_count = int((self_df["status"] != "PASS").sum()) self_json = { "run_id": build.RUN_ID, "task_id": build.TASK_ID, "generated_at": build.now_iso(), "source_run_id": build.SOURCE_RUN_ID, "final_run_id": build.FINAL_RUN_ID, "status": "PASS_FOR_V1_LIFECYCLE_CHART_SUPPLEMENT_READY" if fail_count == 0 else "PARTIAL_WITH_MISSING_MARKET_INPUTS", "pass_count": int((self_df["status"] == "PASS").sum()), "fail_count": fail_count, "v1_buy_case_count": len(v1_index), "case_symbol_lifecycle_count": lifecycle_actual, "case_symbol_lifecycle_expected": expected_lifecycle, "transaction_day_chart_count": tx_actual, "transaction_day_chart_expected": expected_tx, "missing_chart_input_count": len(missing_rows), "return_stat_ready": False, } build.write_json(build.PACKAGE_ROOT / "self_check.json", self_json) build.build_root_docs(case_rows, symbol_index_rows, tx_index_rows, self_json) (build.PACKAGE_ROOT / "self_check.md").write_text( "\n".join( [ "# 自检结果", "", f"- 状态:`{self_json['status']}`", f"- PASS:`{self_json['pass_count']}`", f"- FAIL:`{self_json['fail_count']}`", f"- V1 有 BUY case:`{self_json['v1_buy_case_count']}`", f"- 生命周期日线图:`{self_json['case_symbol_lifecycle_count']}` / `{self_json['case_symbol_lifecycle_expected']}`", f"- 交易日分时图:`{self_json['transaction_day_chart_count']}` / `{self_json['transaction_day_chart_expected']}`", f"- 缺失行情输入:`{self_json['missing_chart_input_count']}`", "", "本包只补充人工审阅图片,不改变 V1 账本、收益口径和 `RETURN_STAT_READY=false`。", ] ) + "\n", encoding="utf-8", ) build.write_manifest() print(json.dumps(self_json, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()