"""Backfill the per-fund alpha REFERENCE into search_all.json. The alpha search verdict (fundlab/search.py) computes alpha as the OLS intercept of the fund's daily total returns against the sleeves BIC forward-selection picked from the 21 broad sleeve axes (the same set for every fund). The picked sleeves ARE the fund's reference - a fitted sleeve mix, not a single index - but the screen never stored them, so the app couldn't show them. This script recomputes the forward selection (full + last-5y) for every screened fund and stores a compact, human-readable reference string on each row: ref_5y "ivv +0.62, tlt +0.31" (sleeves picked for the 5y model) ref_full "ivv +0.38, efa +0.15, ..." "pure alpha (no sleeve selected)" when the model has no components Resumable: rows that already carry ref_5y are skipped. Run: .venv/bin/python -m fundlab.refback (logs to fundlab/refback.log) """ from __future__ import annotations import json import time from fundlab import decompose from fundlab.searchlist import BROAD_SLEEVES HERE = __import__("pathlib").Path(__file__).parent SRC = HERE / "search_all.json" LOG = HERE / "refback.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 _fmt(m: dict) -> str: comps = m.get("components") or [] if not comps: return "pure alpha (no sleeve selected)" return ", ".join(f"{c['sym']} {c['beta']:+.2f}" for c in comps) def run() -> None: d = json.loads(SRC.read_text()) todo = [k for k, v in d.items() if isinstance(v, dict) and isinstance(v.get("r2_5y"), (int, float)) and "ref_5y" not in v] log(f"rows={len(d)} todo={len(todo)}") for i, k in enumerate(todo, 1): sym = d[k]["sym"] try: rec = decompose.decompose(sym, start=decompose.RECENT_WINDOW, candidates={sym: BROAD_SLEEVES}) full = decompose.decompose(sym, candidates={sym: BROAD_SLEEVES}) d[k]["ref_5y"] = _fmt(rec) d[k]["ref_full"] = _fmt(full) except Exception as e: # noqa: BLE001 - batch must not die d[k]["ref_5y"] = f"(backfill failed: {e})" if i % 25 == 0 or i == len(todo): SRC.write_text(json.dumps(d, indent=1)) log(f"{i}/{len(todo)} done (saved)") SRC.write_text(json.dumps(d, indent=1)) n = sum(1 for v in d.values() if isinstance(v, dict) and v.get("ref_5y")) log(f"done: {n} rows carry a reference") if __name__ == "__main__": run()