Raw-intercept alphas absorbed the T-bill yield on uninvested/levered portions (582 well-fitted funds >2%/yr off; sum-of-betas polluted by level-matching). Now fund AND sleeves are netted against BIL daily total return before every regression; a cash position contributes exactly zero. - decompose: rf_series()/excess(); shv+bil dropped from regressors (~0 columns in excess space); FULL_WINDOW -> 2007-06-01 (BIL inception; mixing raw pre-2007 with excess breaks the fit). - factors: same excess treatment; shv out of DRIVERS. - CASH axis redefined: alpha/cash_yield -> net cash position = 1 - sum(betas) (label 'cash (net posn)'). - CANDIDATE list 250 -> 11: the old list was mostly under-invested funds whose 'alpha' was cash yield, not skill. - refback.py: per-fund fitted reference (forward-selected sleeves) stored as ref_5y/ref_full in search_all.json; app alpha-search table gains a 'reference (5y)' column - the answer to 'what is alpha computed against' (the fund's OWN fitted sleeve mix, not one index). - App captions updated; raw-alpha-era results backed up as *_rawalpha.json (not deleted).
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""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()
|