#!/usr/bin/env python3 """Engine ablation analysis: measure each primary engine's marginal contribution to composite performance (primary + IA + parking). Runs N+1 backtests (baseline + leave-one-out for each enabled engine) with presets attached and reports which engines help vs harm composite returns. Usage: # Leave-one-out analysis python -m apps.tools.run_engine_ablation \\ --manifest configs/experiments/return_max_long_v7.70.json \\ --parking qqqm_low_dd \\ --idle-alpha micro_event_alpha_plus_event_plus \\ --output-root /tmp/ablation_v7.70 # Greedy pruning (iteratively removes worst composite-contributor) python -m apps.tools.run_engine_ablation \\ --manifest configs/experiments/return_max_long_v7.70.json \\ --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\ --greedy-prune --output-root /tmp/ablation_v7.70 # Risk budget sweep (test 0.5x/1.0x/1.5x for each engine's risk budget) python -m apps.tools.run_engine_ablation \\ --manifest configs/experiments/return_max_long_v7.70.json \\ --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\ --budget-sweep --output-root /tmp/ablation_v7.70 """ from __future__ import annotations import argparse import copy import json import subprocess import sys import tempfile from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] DEFAULT_SNAPSHOT_DIR = "" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Engine ablation analysis with composite scoring." ) parser.add_argument("--manifest", required=True, help="Base experiment manifest path.") parser.add_argument("--parking", default="qqqm_low_dd", help="Cash parking preset.") parser.add_argument("--idle-alpha", default="micro_event_alpha_plus_event_plus", help="IA sleeve preset.") parser.add_argument("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR) parser.add_argument("--output-root", required=True, help="Root directory for run artifacts.") parser.add_argument("--split", default="all", help="Backtest split (default: 'all' = full period merging train+valid+test).") parser.add_argument( "--greedy-prune", action="store_true", help="After LOO, iteratively remove worst engine until no improvement possible.", ) parser.add_argument( "--budget-sweep", action="store_true", help="Instead of removing engines, test 0.5x/1.5x risk budget for each engine.", ) parser.add_argument( "--no-presets", action="store_true", help="Run without IA/parking presets (primary-only ablation).", ) parser.add_argument( "--idle-alpha-dedup", default=None, choices=["skip", "rename"], help="IA dedup mode: 'rename' injects IA engines even when engine_id conflicts with primary (adds __ia_sleeve suffix).", ) parser.add_argument("--form4-sleeve", default=None, help="Form4 insider sleeve preset name.") parser.add_argument("--ownership-sleeve", default=None, help="Ownership 13D sleeve preset name.") return parser.parse_args() def _load_manifest(manifest_path: str) -> dict: return json.loads(Path(manifest_path).read_text()) def _get_enabled_engines(manifest: dict) -> list[dict]: return [e for e in manifest.get("strategy_engines", []) if e.get("enabled", True) is not False] def _run_backtest( manifest_dict: dict, parking: str | None, idle_alpha: str | None, split: str | None, output_root: Path, run_label: str, snapshot_dir: str, idle_alpha_dedup: str | None = None, form4_sleeve: str | None = None, ownership_sleeve: str | None = None, ) -> dict[str, object]: """Write a temp manifest JSON and run the backtest, returning key metrics.""" run_dir = output_root / run_label run_dir.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( mode="w", suffix=".json", dir=run_dir, delete=False ) as f: json.dump(manifest_dict, f, indent=2) temp_manifest = f.name cmd = [ sys.executable, "-m", "apps.backtester.run", "--manifest", temp_manifest, "--output-root", str(run_dir), ] if snapshot_dir: cmd += ["--snapshot-dir", snapshot_dir] if split: cmd += ["--split", split] if parking: cmd += ["--parking", parking] if idle_alpha: cmd += ["--idle-alpha", idle_alpha] if idle_alpha_dedup: cmd += ["--idle-alpha-dedup", idle_alpha_dedup] if form4_sleeve: cmd += ["--form4-sleeve", form4_sleeve] if ownership_sleeve: cmd += ["--ownership-sleeve", ownership_sleeve] print(f" [{run_label}]...", end=" ", flush=True) result = subprocess.run( cmd, cwd=str(REPO_ROOT), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, ) if result.returncode != 0: print("FAILED") lines = (result.stdout or "").splitlines() for line in lines[-4:]: print(f" {line}") return {"error": True, "run_label": run_label, "total_return_pct": None, "composite_amplification": None} # Find latest run dir bt_dirs = sorted(run_dir.glob("bt_*"), key=lambda p: p.stat().st_mtime) if not bt_dirs: print("NO_RUN_DIR") return {"error": True, "run_label": run_label, "total_return_pct": None} latest = bt_dirs[-1] metrics_path = latest / "metrics" / "metrics_summary.json" sleeve_path = latest / "metrics" / "sleeve_decomposition.json" summary = json.loads(metrics_path.read_text()) if metrics_path.exists() else {} sleeve = json.loads(sleeve_path.read_text()) if sleeve_path.exists() else {} ret = summary.get("total_return_pct") sharpe = summary.get("sharpe_ratio") dd = summary.get("max_drawdown_pct") amp = sleeve.get("composite_amplification") idle = sleeve.get("avg_idle_fraction_pct") print(f"CW={ret:.2f}% Sharpe={sharpe:.2f} DD={dd:.2f}% Idle={idle:.1f}%" if ret is not None else "no metrics") return { "run_label": run_label, "total_return_pct": ret, "sharpe_ratio": sharpe, "max_drawdown_pct": dd, "composite_amplification": amp, "avg_idle_fraction_pct": idle, "trade_count": summary.get("trade_count", 0), "error": False, } def run_loo_ablation( base_manifest: dict, parking: str | None, idle_alpha: str | None, split: str | None, output_root: Path, snapshot_dir: str, idle_alpha_dedup: str | None = None, form4_sleeve: str | None = None, ownership_sleeve: str | None = None, ) -> tuple[dict, list[dict]]: """Run baseline + leave-one-out for each enabled engine. Returns (baseline, loo_results).""" engines = _get_enabled_engines(base_manifest) print(f"\n=== Leave-One-Out Ablation: {len(engines)} enabled engines ===") print(f"Total runs: {len(engines) + 1}\n") print("Running baseline...") baseline = _run_backtest( base_manifest, parking, idle_alpha, split, output_root / "loo", "baseline", snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) loo_results: list[dict] = [] for i, engine in enumerate(engines): engine_id = engine.get("engine_id", f"engine_{i}") # Build manifest with this engine removed mod = copy.deepcopy(base_manifest) mod["strategy_engines"] = [ e for e in mod["strategy_engines"] if e.get("engine_id") != engine_id ] mod["experiment_name"] = f"{base_manifest.get('experiment_name', 'ablation')}_loo_{engine_id}" result = _run_backtest( mod, parking, idle_alpha, split, output_root / "loo", f"loo_{engine_id}", snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) base_ret = baseline.get("total_return_pct") loo_ret = result.get("total_return_pct") delta = (loo_ret - base_ret) if (loo_ret is not None and base_ret is not None) else None result["engine_id"] = engine_id result["delta_return_pct"] = delta result["engine_risk_budget_pct"] = engine.get("engine_risk_budget_pct") loo_results.append(result) return baseline, loo_results def run_budget_sweep( base_manifest: dict, parking: str | None, idle_alpha: str | None, split: str | None, output_root: Path, snapshot_dir: str, idle_alpha_dedup: str | None = None, form4_sleeve: str | None = None, ownership_sleeve: str | None = None, ) -> tuple[dict, list[dict]]: """Test 0.5x and 1.5x risk budget for each engine vs baseline.""" engines = _get_enabled_engines(base_manifest) print(f"\n=== Budget Sweep: {len(engines)} engines × 2 levels ===") print(f"Total additional runs: {len(engines) * 2}\n") print("Running baseline...") baseline = _run_backtest( base_manifest, parking, idle_alpha, split, output_root / "budget_sweep", "baseline", snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) sweep_results: list[dict] = [] for i, engine in enumerate(engines): engine_id = engine.get("engine_id", f"engine_{i}") orig_budget = engine.get("engine_risk_budget_pct", 1.0) for scale, label in [(0.5, "half"), (1.5, "boost")]: mod = copy.deepcopy(base_manifest) for e in mod["strategy_engines"]: if e.get("engine_id") == engine_id: e["engine_risk_budget_pct"] = orig_budget * scale mod["experiment_name"] = f"sweep_{engine_id}_{label}" result = _run_backtest( mod, parking, idle_alpha, split, output_root / "budget_sweep", f"{engine_id}_{label}", snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) base_ret = baseline.get("total_return_pct") sweep_ret = result.get("total_return_pct") delta = (sweep_ret - base_ret) if (sweep_ret is not None and base_ret is not None) else None result["engine_id"] = engine_id result["scale"] = scale result["scale_label"] = label result["delta_return_pct"] = delta result["orig_budget"] = orig_budget result["new_budget"] = orig_budget * scale sweep_results.append(result) return baseline, sweep_results def run_greedy_prune( base_manifest: dict, parking: str | None, idle_alpha: str | None, split: str | None, output_root: Path, snapshot_dir: str, idle_alpha_dedup: str | None = None, form4_sleeve: str | None = None, ownership_sleeve: str | None = None, ) -> list[str]: """Iteratively remove the engine with highest positive delta (least hurts or improves composite). Returns list of removed engine_ids in removal order.""" current_manifest = copy.deepcopy(base_manifest) removed: list[str] = [] round_num = 0 print("\n=== Greedy Pruning ===") while True: round_num += 1 print(f"\n--- Round {round_num} ---") baseline, loo_results = run_loo_ablation( current_manifest, parking, idle_alpha, split, output_root / f"greedy_round_{round_num}", snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) valid = [r for r in loo_results if not r.get("error") and r.get("delta_return_pct") is not None] if not valid: print("No valid LOO results, stopping.") break # Find engine whose removal gives highest delta (most beneficial or least harmful to remove) best = max(valid, key=lambda r: r["delta_return_pct"]) if best["delta_return_pct"] <= 0: print(f"Best removal delta={best['delta_return_pct']:.2f}pp (non-positive). Stopping.") break engine_id = best["engine_id"] delta = best["delta_return_pct"] print(f"Removing '{engine_id}' (delta={delta:+.2f}pp)") removed.append(engine_id) current_manifest["strategy_engines"] = [ e for e in current_manifest["strategy_engines"] if e.get("engine_id") != engine_id ] print(f"\nGreedy pruning complete. Removed {len(removed)} engines: {removed}") # Save the pruned manifest pruned_name = base_manifest.get("experiment_name", "ablation") + "_pruned" current_manifest["experiment_name"] = pruned_name pruned_path = output_root / f"{pruned_name}.json" pruned_path.write_text(json.dumps(current_manifest, indent=2)) print(f"Pruned manifest saved to: {pruned_path}") return removed def _fmt(v: object, decimals: int = 2) -> str: if v is None: return "N/A" try: return f"{float(v):.{decimals}f}" # type: ignore[arg-type] except (TypeError, ValueError): return str(v) def print_loo_table(baseline: dict, loo_results: list[dict]) -> None: base_ret = baseline.get("total_return_pct") print(f"\n{'Engine':<55} {'Without%':>9} {'Baseline%':>10} {'Δpp':>7} {'Action':>8}") print("-" * 95) sorted_loo = sorted( loo_results, key=lambda r: (r.get("delta_return_pct") is None, r.get("delta_return_pct") or 0.0), reverse=True, ) for r in sorted_loo: engine_id = str(r.get("engine_id", "?"))[:54] without = _fmt(r.get("total_return_pct")) delta = r.get("delta_return_pct") delta_str = f"{delta:+.2f}" if delta is not None else "N/A" action = "" if delta is not None: action = "REMOVE!" if delta > 0 else "KEEP" print(f"{engine_id:<55} {without:>9} {_fmt(base_ret):>10} {delta_str:>7} {action:>8}") print("-" * 95) print(f"{'BASELINE':<55} {_fmt(base_ret):>9}") def print_budget_sweep_table(baseline: dict, sweep_results: list[dict]) -> None: base_ret = baseline.get("total_return_pct") print(f"\n{'Engine':<45} {'Scale':>6} {'Budget':>7} {'CW%':>8} {'Δpp':>7}") print("-" * 80) for r in sweep_results: engine_id = str(r.get("engine_id", "?"))[:44] scale = r.get("scale_label", "?") budget = _fmt(r.get("new_budget"), decimals=4) cw = _fmt(r.get("total_return_pct")) delta = r.get("delta_return_pct") delta_str = f"{delta:+.2f}" if delta is not None else "N/A" print(f"{engine_id:<45} {scale:>6} {budget:>7} {cw:>8} {delta_str:>7}") print("-" * 80) print(f"{'BASELINE':>58} {_fmt(base_ret):>8}") def main() -> None: args = parse_args() output_root = Path(args.output_root) output_root.mkdir(parents=True, exist_ok=True) base_manifest = _load_manifest(args.manifest) exp_name = base_manifest.get("experiment_name", Path(args.manifest).stem) parking = None if args.no_presets else args.parking idle_alpha = None if args.no_presets else getattr(args, "idle_alpha") print(f"Engine Ablation: {exp_name}") print(f"Presets: parking={parking}, idle_alpha={idle_alpha}") engines = _get_enabled_engines(base_manifest) print(f"Enabled engines: {len(engines)}") idle_alpha_dedup = None if args.no_presets else getattr(args, "idle_alpha_dedup", None) form4_sleeve = None if args.no_presets else getattr(args, "form4_sleeve", None) ownership_sleeve = None if args.no_presets else getattr(args, "ownership_sleeve", None) if args.greedy_prune: removed = run_greedy_prune( base_manifest, parking, idle_alpha, args.split, output_root, args.snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) print(f"\nFinal removed engines: {removed}") elif args.budget_sweep: baseline, sweep_results = run_budget_sweep( base_manifest, parking, idle_alpha, args.split, output_root, args.snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) print_budget_sweep_table(baseline, sweep_results) # Save results out = output_root / "budget_sweep_results.json" out.write_text(json.dumps({"baseline": baseline, "sweep": sweep_results}, indent=2)) print(f"\nResults saved to: {out}") else: # Default: LOO ablation baseline, loo_results = run_loo_ablation( base_manifest, parking, idle_alpha, args.split, output_root, args.snapshot_dir, idle_alpha_dedup, form4_sleeve, ownership_sleeve ) print_loo_table(baseline, loo_results) # Save results out = output_root / "loo_results.json" out.write_text(json.dumps({"baseline": baseline, "loo": loo_results}, indent=2)) print(f"\nResults saved to: {out}") if __name__ == "__main__": main()