"""Extend per-share verification to the FULL CEF universe (295 tickers). The 50-fund shortlist run (fundlab.cef_annual) wrote fundlab/cef_annual.json. This batch runs the same analyze() over every ticker in fundlab/cef_universe.json and writes fundlab/cef_annual_all.json — incrementally, so a killed run resumes by re-running this script (cached funds are skipped, as are those already in the output file). Usage: .venv/bin/python -m fundlab.cef_universe_run # all, resumable .venv/bin/python -m fundlab.cef_universe_run --retry-err # redo failed ones """ from __future__ import annotations import json import sys import time from pathlib import Path from fundlab.cef_annual import (CACHE, analyze, character_score, current_nav_discount) HERE = Path(__file__).parent UNIVERSE = HERE / "cef_universe.json" OUT = HERE / "cef_annual_all.json" LOG = HERE / "cef_universe_run.log" def log(msg: str) -> None: line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}" print(line, flush=True) with LOG.open("a") as f: f.write(line + "\n") def run(retry_err: bool = False) -> dict: uni = json.loads(UNIVERSE.read_text()) prev: dict = {} if OUT.exists(): prev = json.loads(OUT.read_text()) todo = [] for s in sorted(uni): p = prev.get(s) if p is None or (retry_err and "error" in p): todo.append(s) log(f"universe={len(uni)} done={len(uni) - len(todo)} todo={len(todo)}" + (" [retry-err]" if retry_err else "")) res = dict(prev) for i, s in enumerate(todo, 1): u = uni[s] cik = int(u.get("cik", 0)) try: a = analyze(s, cik) except Exception as e: # noqa: BLE001 - batch must not die a = {"sym": s, "error": f"analyze exception: {e}"} a["char_actual"] = (round(character_score(a, u.get("name", "")), 2) if "share_div" in a else None) if "nav_end" in a: d = current_nav_discount(a) a["disc_now_approx"] = round(d, 4) if d is not None else None res[s] = a if "share_div" in a: log(f"{i:3}/{len(todo)} {s:7} div {a['share_div']:.0%} " f"gain {a['share_gains']:.0%} roc {a['share_roc']:.0%} " f"char={a['char_actual']}") else: log(f"{i:3}/{len(todo)} {s:7} -- {a.get('error', 'no data')}") OUT.write_text(json.dumps(res, indent=1, default=str)) log(f"done: parsed={sum(1 for v in res.values() if 'share_div' in v)} " f"of {len(res)}") return res if __name__ == "__main__": run(retry_err="--retry-err" in sys.argv)