#!/usr/bin/env python3 """DeepEval Phase 1: Run WFV comparison matrix (clean lineage). Usage: python -m apps.tools.run_deepeval_phase1 [--output-root runs/deepeval_phase1] Runs walk-forward validation for: 1.1 Clean lineage baseline 1.2 Clean lineage replay of legacy v0.326 Then prints a comparison matrix. """ from __future__ import annotations import argparse import json import subprocess import sys from pathlib import Path from concurrent.futures import ProcessPoolExecutor, as_completed CONFIGS = { "v1.1_clean_baseline": "configs/experiments/return_max_long_v1.1.json", "v1.2_clean_replay": "configs/experiments/return_max_long_v1.2.json", } WF_TRAIN_DAYS = 504 WF_TEST_DAYS = 63 def run_single_wfv(label: str, manifest_path: str, output_root: str) -> dict: """Run a single WFV and return the summary dict.""" out_dir = str(Path(output_root) / label) cmd = [ sys.executable, "-m", "apps.backtester.run", "--manifest", manifest_path, "--walk-forward", "--wf-train-days", str(WF_TRAIN_DAYS), "--wf-test-days", str(WF_TEST_DAYS), "--output-root", out_dir, "--initial-equity", "100000", "--snapshot-dir", "data/datasets/snapshots", ] print(f"[{label}] Starting WFV...") result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"[{label}] FAILED:\n{result.stderr[-500:]}") return {"label": label, "error": result.stderr[-500:]} summary_path = Path(out_dir) / "walk_forward" / "walk_forward_summary.json" if not summary_path.exists(): print(f"[{label}] WARNING: summary not found at {summary_path}") return {"label": label, "error": "summary not found"} with open(summary_path) as f: summary = json.load(f) print(f"[{label}] Done. Test mean return: {summary['test_aggregate']['mean_return_pct']:.1f}%") return {"label": label, "summary": summary} def print_comparison_matrix(results: list[dict]) -> None: """Print a formatted comparison matrix.""" print("\n" + "=" * 90) print("DeepEval Phase 1 — WFV Comparison Matrix") print("=" * 90) header = f"{'Config':<25} {'Test Mean%':>10} {'Test Med%':>10} {'Test Worst%':>11} {'Train-Test Gap':>14} {'Test Sharpe':>11}" print(header) print("-" * 90) for r in results: label = r["label"] if "error" in r: print(f"{label:<25} {'ERROR':>10}") continue s = r["summary"] ta = s["test_aggregate"] gap = s["gap_stats"] print( f"{label:<25} " f"{ta['mean_return_pct']:>10.1f} " f"{ta['median_return_pct']:>10.1f} " f"{ta['worst_return_pct']:>11.1f} " f"{gap['mean_train_test_return_gap_pct']:>14.1f} " f"{ta.get('mean_profit_factor', 0):>11.1f}" ) print("=" * 90) print("\nKey interpretation:") print(" - v1.1 is the clean-lineage baseline") print(" - v1.2 is the clean-lineage replay of legacy v0.326 non-exact core") print(" - Train-Test Gap: lower is better (less overfitting)") def main() -> None: parser = argparse.ArgumentParser(description="DeepEval Phase 1: WFV Comparison Matrix") parser.add_argument("--output-root", default="runs/deepeval_phase1", help="Output directory") parser.add_argument("--parallel", type=int, default=2, help="Max parallel WFV runs") args = parser.parse_args() results = [] if args.parallel > 1: with ProcessPoolExecutor(max_workers=args.parallel) as executor: futures = { executor.submit(run_single_wfv, label, path, args.output_root): label for label, path in CONFIGS.items() } for future in as_completed(futures): results.append(future.result()) else: for label, path in CONFIGS.items(): results.append(run_single_wfv(label, path, args.output_root)) # Sort by label for consistent output results.sort(key=lambda r: r["label"]) print_comparison_matrix(results) # Save raw results out_path = Path(args.output_root) / "phase1_comparison.json" out_path.parent.mkdir(parents=True, exist_ok=True) with open(out_path, "w") as f: json.dump(results, f, indent=2, default=str) print(f"\nRaw results saved to {out_path}") if __name__ == "__main__": main()