#!/usr/bin/env python3 """DeepEval Phase 2.2: Effective Independent Sample Count calculator. Reads engine attribution and trade blotter data from a backtest run to compute per-engine effective_n — the number of truly independent trade observations, accounting for same-ticker repeated events. Usage: python -m apps.tools.deepeval_effective_n \ --run-dir runs/v1045_wfv/walk_forward/fold_07/train/bt_* Or for all folds: python -m apps.tools.deepeval_effective_n \ --wfv-dir runs/v1045_wfv """ from __future__ import annotations import argparse import csv import json from collections import defaultdict from datetime import date, timedelta from pathlib import Path MIN_GAP_DAYS = 126 # ~6 months between events for independence MAX_PER_TICKER = 3 # cap independent observations per ticker def load_trade_blotter(run_dir: Path) -> list[dict]: """Load trade blotter from parquet or CSV.""" parquet_path = run_dir / "artifacts" / "trade_blotter.parquet" if parquet_path.exists(): try: import pyarrow.parquet as pq table = pq.read_table(parquet_path) return table.to_pylist() except ImportError: pass csv_path = run_dir / "artifacts" / "trade_blotter.csv" if csv_path.exists(): with open(csv_path) as f: return list(csv.DictReader(f)) return [] def load_attribution(run_dir: Path) -> dict[str, dict]: """Load engine attribution CSV.""" csv_path = run_dir / "metrics" / "attribution_by_engine.csv" if not csv_path.exists(): return {} result = {} with open(csv_path) as f: for row in csv.DictReader(f): result[row["engine_id"]] = row return result def compute_effective_n(trades: list[dict], min_gap_days: int = MIN_GAP_DAYS) -> dict: """Compute effective independent sample count for trades grouped by engine. Returns dict of engine_id -> { total_trades, unique_tickers, effective_n, ticker_breakdown: {ticker: {total, independent}} } """ # Group trades by engine_id by_engine: dict[str, list[dict]] = defaultdict(list) for t in trades: eid = t.get("engine_id", "default") by_engine[eid].append(t) results = {} for engine_id, engine_trades in by_engine.items(): # Group by ticker by_ticker: dict[str, list[date]] = defaultdict(list) for t in engine_trades: symbol = t.get("symbol", "") entry_str = t.get("entry_date", "") if isinstance(entry_str, str) and entry_str: try: entry_date = date.fromisoformat(entry_str[:10]) except ValueError: continue elif isinstance(entry_str, date): entry_date = entry_str else: continue by_ticker[symbol].append(entry_date) ticker_breakdown = {} effective_n = 0 for ticker, dates in by_ticker.items(): sorted_dates = sorted(dates) independent = 1 # first event always counts last_counted = sorted_dates[0] for d in sorted_dates[1:]: if (d - last_counted).days >= min_gap_days: independent += 1 last_counted = d capped = min(independent, MAX_PER_TICKER) effective_n += capped ticker_breakdown[ticker] = { "total": len(dates), "independent": independent, "capped": capped, } results[engine_id] = { "total_trades": len(engine_trades), "unique_tickers": len(by_ticker), "effective_n": effective_n, "ticker_breakdown": ticker_breakdown, } return results def classify(effective_n: int) -> str: if effective_n < 3: return "shadow_only" elif effective_n < 8: return "reduced_budget" else: return "full_budget" def print_report(engine_stats: dict, attribution: dict) -> None: """Print formatted report.""" print(f"\n{'Engine':<50} {'Trades':>6} {'Tickers':>7} {'Eff-N':>5} {'Class':<15} {'PnL':>10}") print("-" * 100) # Sort by effective_n ascending (worst first) sorted_engines = sorted(engine_stats.items(), key=lambda x: x[1]["effective_n"]) shadow_count = 0 reduced_count = 0 full_count = 0 for eid, stats in sorted_engines: cls = classify(stats["effective_n"]) if cls == "shadow_only": shadow_count += 1 elif cls == "reduced_budget": reduced_count += 1 else: full_count += 1 pnl = "" if eid in attribution: pnl = f"${float(attribution[eid].get('net_pnl', 0)):>9,.0f}" is_exact = "exact" in eid marker = " *" if is_exact else "" print( f"{eid[:48] + marker:<50} " f"{stats['total_trades']:>6} " f"{stats['unique_tickers']:>7} " f"{stats['effective_n']:>5} " f"{cls:<15} " f"{pnl:>10}" ) print("-" * 100) print(f"Shadow-only (eff_n < 3): {shadow_count}") print(f"Reduced budget (3 <= eff_n < 8): {reduced_count}") print(f"Full budget (eff_n >= 8): {full_count}") print(f"Total engines with trades: {len(engine_stats)}") print("(* = exact pocket engine)") def process_run_dir(run_dir: Path) -> dict: """Process a single run directory.""" trades = load_trade_blotter(run_dir) if not trades: print(f"No trades found in {run_dir}") return {} attribution = load_attribution(run_dir) return compute_effective_n(trades) def main() -> None: parser = argparse.ArgumentParser(description="DeepEval Phase 2.2: Effective-N Calculator") parser.add_argument("--run-dir", help="Single backtest run directory") parser.add_argument("--wfv-dir", help="Walk-forward validation directory (processes all folds)") parser.add_argument("--output", help="Save JSON report to file") args = parser.parse_args() if args.wfv_dir: # Aggregate across all train folds wfv_dir = Path(args.wfv_dir) all_trades = [] for fold_dir in sorted(wfv_dir.glob("walk_forward/fold_*/train/bt_*")): trades = load_trade_blotter(fold_dir) all_trades.extend(trades) if not all_trades: print("No trades found in WFV folds") return print(f"Loaded {len(all_trades)} trades from WFV train folds") engine_stats = compute_effective_n(all_trades) # Load attribution from latest fold for PnL reference latest_fold = sorted(wfv_dir.glob("walk_forward/fold_*/train/bt_*"))[-1] if all_trades else None attribution = load_attribution(latest_fold) if latest_fold else {} elif args.run_dir: run_dir = Path(args.run_dir) trades = load_trade_blotter(run_dir) if not trades: print(f"No trades found in {run_dir}") return engine_stats = compute_effective_n(trades) attribution = load_attribution(run_dir) else: parser.error("Provide --run-dir or --wfv-dir") return print_report(engine_stats, attribution) if args.output: # Prepare serializable report report = {} for eid, stats in engine_stats.items(): report[eid] = { **stats, "classification": classify(stats["effective_n"]), "is_exact": "exact" in eid, } with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"\nJSON report saved to {args.output}") if __name__ == "__main__": main()