"""Concurrent Position Analysis: Position clustering and correlation. Are simultaneous positions correlated? Analyzes trade blotter to find overlapping position groups and their sector distribution. Usage: python -m dev.analysis.concurrent_position_analysis \ --run-dir runs/run_20260313_xyz [--output-dir ./data/analysis] """ from __future__ import annotations import argparse import csv import datetime as dt from collections import Counter from pathlib import Path from typing import Any import pyarrow.parquet as pq def _load_blotter(run_dir: Path) -> list[dict[str, Any]]: """Load trade blotter from parquet.""" blotter_path = run_dir / "trade_blotter.parquet" if not blotter_path.exists(): # Try CSV fallback csv_path = run_dir / "trade_blotter.csv" if csv_path.exists(): rows = [] with open(csv_path) as f: reader = csv.DictReader(f) for row in reader: rows.append(row) return rows raise FileNotFoundError(f"No trade blotter found in {run_dir}") table = pq.read_table(blotter_path) return table.to_pylist() def _parse_date(val: Any) -> dt.date | None: """Parse a date value from various formats.""" if val is None: return None if isinstance(val, dt.date): return val if isinstance(val, dt.datetime): return val.date() try: return dt.date.fromisoformat(str(val)[:10]) except (ValueError, TypeError): return None def analyze(trades: list[dict[str, Any]], output_dir: Path) -> None: output_dir.mkdir(parents=True, exist_ok=True) # Parse entry/exit dates positions: list[dict[str, Any]] = [] for t in trades: entry = _parse_date(t.get("entry_date")) exit_ = _parse_date(t.get("exit_date")) if entry and exit_: positions.append({ "trade_id": t.get("trade_id", ""), "symbol": t.get("symbol", ""), "sector": t.get("sector", "UNKNOWN"), "entry_date": entry, "exit_date": exit_, }) if not positions: print("No valid trades found.") return # Build daily position timeline all_dates = set() for p in positions: d = p["entry_date"] while d <= p["exit_date"]: all_dates.add(d) d += dt.timedelta(days=1) daily_positions: dict[dt.date, list[dict[str, Any]]] = {} for d in sorted(all_dates): active = [p for p in positions if p["entry_date"] <= d <= p["exit_date"]] if active: daily_positions[d] = active # Compute concurrent position stats concurrent_counts = [len(v) for v in daily_positions.values()] max_concurrent = max(concurrent_counts) if concurrent_counts else 0 avg_concurrent = sum(concurrent_counts) / len(concurrent_counts) if concurrent_counts else 0 # Find overlap groups (days with >1 position) overlap_days = {d: ps for d, ps in daily_positions.items() if len(ps) > 1} # Sector distribution in overlap groups overlap_sector_counts: Counter[str] = Counter() overlap_symbol_counts: Counter[str] = Counter() for d, ps in overlap_days.items(): for p in ps: overlap_sector_counts[p["sector"]] += 1 overlap_symbol_counts[p["symbol"]] += 1 # Peak overlap days peak_days = sorted(overlap_days.items(), key=lambda x: len(x[1]), reverse=True)[:10] # CSV: daily concurrent position counts csv_rows: list[dict[str, Any]] = [] for d in sorted(daily_positions): ps = daily_positions[d] sectors = Counter(p["sector"] for p in ps) csv_rows.append({ "date": str(d), "concurrent_positions": len(ps), "unique_sectors": len(sectors), "sectors": "|".join(f"{s}:{c}" for s, c in sectors.most_common()), "symbols": "|".join(p["symbol"] for p in ps), }) csv_path = output_dir / "concurrent_position_analysis.csv" if csv_rows: fieldnames = list(csv_rows[0].keys()) with open(csv_path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(csv_rows) # Console output print(f"\n{'='*70}") print(f"Concurrent Position Analysis — {len(positions)} trades") print(f"{'='*70}") print(f"\n Total trading days with positions: {len(daily_positions)}") print(f" Days with >1 concurrent position: {len(overlap_days)}") print(f" Max concurrent positions: {max_concurrent}") print(f" Avg concurrent positions: {avg_concurrent:.1f}") if overlap_sector_counts: print(f"\n Sector distribution in overlap periods:") for sector, count in overlap_sector_counts.most_common(): print(f" {sector:<20} {count:>5} position-days") if peak_days: print(f"\n Top 10 peak overlap days:") for d, ps in peak_days: symbols = ", ".join(p["symbol"] for p in ps) sectors = set(p["sector"] for p in ps) print(f" {d} {len(ps)} positions sectors={len(sectors)} [{symbols}]") print(f"\nCSV written to: {csv_path}") def main() -> None: parser = argparse.ArgumentParser(description="Concurrent Position Analysis") parser.add_argument("--run-dir", required=True, help="Path to backtest run directory") parser.add_argument("--output-dir", default="./data/analysis", help="Output directory") args = parser.parse_args() trades = _load_blotter(Path(args.run_dir)) if not trades: print("No trades found in blotter.") return analyze(trades, Path(args.output_dir)) if __name__ == "__main__": main()