#!/usr/bin/env python3 from __future__ import annotations import argparse import json import subprocess import sys from pathlib import Path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run train/valid/test backtests for multiple manifests.") parser.add_argument("--manifest", action="append", required=True, help="Experiment manifest path. Repeat for multiple manifests.") parser.add_argument("--snapshot-dir", default="data/datasets/snapshots", help="Snapshot root directory.") parser.add_argument("--output-root", required=True, help="Root directory for run artifacts.") parser.add_argument("--summary-path", required=True, help="Path to write summary JSON.") return parser.parse_args() def run_split(root: Path, manifest: str, split: str, output_root: Path, snapshot_dir: str) -> dict[str, object]: cmd = [ sys.executable, "apps/backtester/run.py", "--manifest", manifest, "--snapshot-dir", snapshot_dir, "--split", split, "--output-root", str(output_root), ] result = subprocess.run( cmd, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, ) tail = result.stdout.splitlines()[-5:] if result.stdout else [] for line in tail: print(line) if result.returncode != 0: raise RuntimeError(f"{Path(manifest).stem} {split} failed:\n{result.stdout}") latest = max(output_root.glob("bt_*"), key=lambda path: path.stat().st_mtime) summary = json.loads((latest / "metrics" / "metrics_summary.json").read_text()) return { "run_id": latest.name, "ret": summary["total_return_pct"], "dd": summary["max_drawdown_pct"], "n": summary["trade_count"], "pf": summary["profit_factor"], } def main() -> None: args = parse_args() root = Path(__file__).resolve().parents[2] output_root = Path(args.output_root) output_root.mkdir(parents=True, exist_ok=True) rows: list[dict[str, object]] = [] for manifest in args.manifest: exp_name = Path(manifest).stem print(f"RUN {exp_name}", flush=True) exp_root = output_root / exp_name exp_root.mkdir(parents=True, exist_ok=True) metrics = { split: run_split(root, manifest, split, exp_root, args.snapshot_dir) for split in ("train", "valid", "test") } rows.append({"experiment": exp_name, "metrics": metrics}) summary_path = Path(args.summary_path) summary_path.parent.mkdir(parents=True, exist_ok=True) summary_path.write_text(json.dumps(rows, indent=2) + "\n") print(f"WROTE {summary_path}") for row in rows: metrics = row["metrics"] print( row["experiment"], f"tr={metrics['train']['ret']:.2f} v={metrics['valid']['ret']:.2f} t={metrics['test']['ret']:.2f}", ) if __name__ == "__main__": main()