#!/usr/bin/env python3 """Bulk-apply 'dedup' corrections for the same-date/same-amount cross-file double-listings found by scripts/scan_double_listing.py. Mechanism (verified against official filings for JLPSX, GDEUX, GSOUX, FAEVX, CVSIX, FZAGX): Yahoo lists the (usually year-end) distribution in both the dividend and capitalGains event maps; one copy is spurious. The 'dedup' invariant keeps at most one copy, layout-agnostically. Skips symbols that already have a correction file (their pairs are handled by explicit, officially-verified ops). Read the scan report first (reports/double_listing/scan.md) — this applies the mechanism-inferred correction to every symbol with at least one same-amount pair. Usage: python3 scripts/apply_dedup_corrections.py [--dry-run] """ import argparse import json import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent CORR = ROOT / "overrides" / "corrections" SCAN = ROOT / "reports" / "double_listing" / "scan.json" NOTE = ("Bulk correction from the double-listing scan " "(scripts/scan_double_listing.py): Yahoo listed this distribution in " "both the dividend and capitalGains event files (same date, same " "amount); one copy is spurious. The 'dedup' invariant keeps at most " "one copy, layout-agnostically. Mechanism verified against official " "filings for JLPSX/GDEUX/GSOUX/FAEVX/CVSIX/FZAGX; mechanism-inferred " "for this symbol (no per-fund official check). Revert the entry if a " "future official cross-check shows both rows were real.") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() scan = json.loads(SCAN.read_text()) written = skipped = 0 for sym, r in sorted(scan["symbols"].items()): pairs = r.get("same_amt") or [] if not pairs: continue f = CORR / f"{sym.upper()}.json" if f.exists(): skipped += 1 continue entry = {"symbol": sym, "dedup": [[x["date"], x["amount"]] for x in pairs], "note": NOTE} if not args.dry_run: f.write_text(json.dumps(entry, indent=2) + "\n") written += 1 print(f"{'would write' if args.dry_run else 'wrote'} {written} correction files, " f"skipped {skipped} (existing corrections)") if not args.dry_run: subprocess.run([sys.executable, str(ROOT / "scripts" / "check_corrections.py")], check=False) return 0 if __name__ == "__main__": sys.exit(main())