diff --git a/app.py b/app.py index ed204af..2b3f749 100644 --- a/app.py +++ b/app.py @@ -588,6 +588,33 @@ with tab_fundlab: _fl_pick = st.selectbox("Fund (shortlist)", list(_FL_LABELS), format_func=lambda s: _FL_LABELS[s], key="fundlab_pick") + + # --- all-funds summary table ------------------------------------- + from fundlab import decompose as _dc + try: + _DR = json.loads(_dc.RESULTS.read_text()) + except Exception: + _DR = {} + with st.expander(f"Summary — all {len(_FL_LABELS)} funds"): + _sum_rows = [] + for _s in _ORDER: + _d = _DR.get(_s, {}) + _rec = _d.get("recent", {}) + _comp = ", ".join(f"{c['sym']} {c['beta']:+.2f}" + for c in _rec.get("components", [])[:4]) or \ + ", ".join(f"{c['sym']} {c['beta']:+.2f}" + for c in _d.get("components", [])[:4]) + _sum_rows.append({ + "fund": _FL_LABELS.get(_s, _s), + "verdict": _d.get("verdict", "n/a"), + "R² 5y": round(_rec["r2"], 3) if "r2" in _rec else None, + "alpha 5y": (f"{_rec['alpha_ann']*100:+.1f}% " + f"(t={_rec['alpha_t']:+.1f})") + if "alpha_ann" in _rec else None, + "components": _comp or "—", + }) + st.dataframe(pd.DataFrame(_sum_rows), width="stretch") + _f = _FUNDS.get(_fl_pick, {}) _man = _MAN.get(_fl_pick, {}) st.subheader(f"{_f.get('name', _fl_pick)} · {_fl_pick.upper()}") @@ -646,6 +673,74 @@ with tab_fundlab: top["value"] = top["value"].map(lambda v: f"${v:,.0f}") st.dataframe(top, width="stretch") + # ---- returns-based strategy decomposition ------------------------- + st.markdown("**Strategy decomposition** — which benchmark sleeves explain " + "the fund's returns (OLS forward selection, BIC-gated, " + "|t|>2; full history + last 5 years)") + _dr = _DR.get(_fl_pick, {}) + if not _dr or "verdict" not in _dr: + st.info("No decomposition available for this fund.") + elif "r2" not in _dr: + st.info(_dr["verdict"]) + if _dr.get("note"): + st.markdown("**Holdings cross-check:** " + _dr["note"]) + else: + _rec = _dr.get("recent", {}) + _v = _dr["verdict"] + if _v.startswith("static") or _v.startswith("mostly stable"): + st.success(_v) + elif "no component" in _v or "not a static" in _v: + st.warning(_v) + else: + st.info(_v) + _c = st.columns(6) + _c[0].metric("R² full", f"{_dr.get('r2', float('nan')):.3f}") + _c[1].metric("R² 5y", + f"{_rec['r2']:.3f}" if "r2" in _rec else "n/a") + _c[2].metric("alpha 5y", + f"{_rec['alpha_ann']*100:+.1f}% (t={_rec['alpha_t']:+.1f})" + if "alpha_ann" in _rec else "n/a") + _c[3].metric("trk err 5y", + f"{_rec['tracking_err_ann']*100:.1f}%" + if "tracking_err_ann" in _rec else "n/a") + _c[4].metric("max β-drift", + f"{_dr.get('rolling', {}).get('max_drift', 0.0):.2f}") + _c[5].metric("sample", f"{_dr['n_obs']}d", help=f"{_dr['start']} .. {_dr['end']}") + _rows = [] + _full_b = {c["sym"]: c for c in _dr.get("components", [])} + _rec_b = {c["sym"]: c for c in _rec.get("components", [])} + for _sym in list(dict.fromkeys(list(_full_b) + list(_rec_b))): + _frow = _full_b.get(_sym) + _rrow = _rec_b.get(_sym) + _rows.append({ + "component": f"{_sym} — {_frow['label'] if _frow else _rrow['label']}", + "β full": f"{_frow['beta']:+.2f} (t={_frow['t']:+.1f})" if _frow else "—", + "β 5y": f"{_rrow['beta']:+.2f} (t={_rrow['t']:+.1f})" if _rrow else "—", + }) + if _rows: + st.dataframe(pd.DataFrame(_rows), width="stretch") + if _full_b: + _bar = pd.DataFrame([ + {"component": f"{s} — {c['label']}", "full": c["beta"]} + for s, c in _full_b.items()]) + fig = go.Figure(go.Bar( + x=_bar["full"], y=_bar["component"], orientation="h", + marker_color=["#2ca02c" if v >= 0 else "#d62728" + for v in _bar["full"]])) + fig.update_layout(height=30 + 24 * len(_bar), + margin=dict(l=0, r=0, t=8, b=8), + xaxis_title="β (full sample)") + st.plotly_chart(fig, width="stretch") + if _dr.get("note"): + st.markdown("**Holdings cross-check:** " + _dr["note"]) + st.caption( + "Method: daily total returns; greedy forward selection on BIC " + "(add only if ΔBIC ≥ 2 and |t| > 2); candidate sleeves curated per " + "fund from the strategy text + N-PORT. β = exposure, not a literal " + "holding weight; 'drift' = mean |rolling 1y β − full β| relative to " + "the full β. High-R² + low drift ≈ static mix; low R² with positive " + "alpha ≈ market-neutral/alpha strategy.") + if _f.get("strategy"): st.markdown("**Strategy (excerpt from the prospectus)**") st.write(_f["strategy"]) diff --git a/fundlab/decompose.py b/fundlab/decompose.py new file mode 100644 index 0000000..e6cc314 --- /dev/null +++ b/fundlab/decompose.py @@ -0,0 +1,394 @@ +"""Strategy decomposition: which pool components best explain a fund's returns. + +For each actively-managed fund we: + + 1. take its total-return (Adj Close) daily returns, + 2. run a greedy BIC forward-selection regression against a curated + candidate set of pool components (broad sleeves, not single + positions — the question is "what does the fund look like", not + "what exact holdings"), + 3. report the chosen weights (betas), fit (R^2, alpha/t), and a + rolling-window check for time-varying weights, + +and hand a verdict to the report: *static sleeve mix* (weights stable), +*mostly stable with some timing*, or *time-varying / not a sleeve mix*. + +The candidate sets are per-fund and come from the fund's stated strategy +plus its N-PORT holdings; selection within the set is data-driven. + +NumPy only (no statsmodels in the venv). All regressions include an +intercept; alphas are annualized (x252); t-stats use the OLS covariance. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pandas as pd + +from fundlab import pool as _pool + +DATA = Path.home() / "prog/fin/stocks" +RESULTS = Path(__file__).parent / "decompose_results.json" + +# ------------------------------------------------------------------ data +def adj_close(sym: str, root: Path = DATA) -> pd.Series | None: + f = root / f"{sym}-history.csv" + if not f.exists(): + return None + try: + s = (pd.read_csv(f, parse_dates=["Date"], index_col="Date") + ["Adj Close"].dropna()) + s = s[~s.index.duplicated(keep="last")].sort_index() + except Exception: + return None + if len(s) < 252: + return None + s.name = sym + return s + + +def returns_panel(symbols: list[str], start: str | None = None) -> pd.DataFrame: + """Daily simple returns, outer-joined on dates (NaN where a series + has no data). Callers must handle missing values per regressor — + candidate histories have different vintages (and some end early, e.g. + finux stops in 2017), so a global inner join can be empty.""" + cols = [] + for s in symbols: + a = adj_close(s) + if a is not None: + cols.append(a) + p = pd.concat(cols, axis=1, sort=False) + if start is not None: + p = p[p.index >= pd.Timestamp(start)] + return p.pct_change() + + +# ------------------------------------------------------------------ stats +def ols(y: np.ndarray, X: np.ndarray) -> dict: + """OLS of y on X (X must already include the intercept column).""" + n, k = X.shape + beta, *_ = np.linalg.lstsq(X, y, rcond=None) + resid = y - X @ beta + ssr = float(resid @ resid) + dof = max(n - k, 1) + s2 = ssr / dof + XtX_inv = np.linalg.pinv(X.T @ X) + se = np.sqrt(np.maximum(np.diag(XtX_inv) * s2, 0.0)) + t = np.where(se > 0, beta / np.where(se > 0, se, 1.0), 0.0) + yss = float(((y - y.mean()) ** 2).sum()) + r2 = 1.0 - ssr / yss if yss > 0 else 0.0 + adj_r2 = 1.0 - (1.0 - r2) * (n - 1) / dof + bic = n * np.log(ssr / n) + k * np.log(n) + dw = float(((resid[1:] - resid[:-1]) ** 2).sum() / ssr) if ssr > 0 else 0.0 + return {"beta": beta, "se": se, "t": t, "r2": r2, "adj_r2": adj_r2, + "bic": bic, "dw": dw, "ssr": ssr, "n": n, "resid": resid} + + +def forward_select(y: np.ndarray, cand: dict[str, np.ndarray], + max_k: int = 6, min_d_bic: float = 2.0, + y_ok: np.ndarray | None = None) -> tuple[list[str], dict, np.ndarray]: + """Greedy forward selection on BIC, with per-model complete cases. + + ``cand`` maps symbol -> daily-return vector (aligned with y); NaNs + mark dates where that series has no data. Each trial regression uses + the rows where the fund, all chosen components and the candidate are + all present, so differently-vintaged candidates stay comparable. + A candidate is only added if it lowers BIC by at least ``min_d_bic`` + (~3:1 odds in its favour), so we stop before overfitting noise. + + Returns (chosen symbols, model on the final complete cases, the + boolean mask of those rows). + """ + if y_ok is None: + y_ok = ~np.isnan(y) + ones0 = np.ones(len(y)) + chosen: list[str] = [] + + def fit(ch: list[str]) -> tuple[dict, np.ndarray]: + ok = y_ok.copy() + for c in ch: + ok &= ~np.isnan(cand[c]) + X = np.column_stack([ones0[ok], *[cand[c][ok] for c in ch]]) + return ols(y[ok], X), ok + + cur, cur_ok = fit(chosen) + for _ in range(max_k): + best_j, best_b, best_m = None, cur["bic"], None + for j in cand: + if j in chosen: + continue + ok = cur_ok & ~np.isnan(cand[j]) + X = np.column_stack([ones0[ok], *[cand[c][ok] for c in chosen], + cand[j][ok]]) + if X.shape[0] < 252 or np.linalg.matrix_rank(X) < X.shape[1]: + continue + m = ols(y[ok], X) + # the new regressor is the last column; require it to be + # individually significant (|t|>2) so we don't chase noise + if abs(m["t"][-1]) < 2.0: + continue + if m["bic"] < best_b - 1e-9: + best_j, best_b, best_m = j, m["bic"], m + if best_j is None or best_b > cur["bic"] - min_d_bic: + break + chosen.append(best_j) + cur, cur_ok = fit(chosen) + return chosen, cur, cur_ok + + +def rolling_beta(y: np.ndarray, X: np.ndarray, w: int = 252, + step: int = 21, dates: pd.Index | None = None) -> pd.DataFrame: + """Refit betas on rolling windows; returns a DataFrame indexed by the + window end date, one column per regressor (no intercept column).""" + n = len(y) + rows = [] + idx = [] + i = w + while i <= n: + Xm = X[i - w:i] + ym = y[i - w:i] + if np.linalg.matrix_rank(Xm) < Xm.shape[1]: + i += step + continue + b, *_ = np.linalg.lstsq(Xm, ym, rcond=None) + rows.append(b[1:]) + idx.append(dates[i - 1] if dates is not None else i - 1) + i += step + cols = ["intercept"] + [f"x{k}" for k in range(X.shape[1] - 1)] + return pd.DataFrame(rows, index=idx, columns=cols[1:]) + + +# ------------------------------------------------------------------ funds +# per-fund candidate sets: broad pool sleeves consistent with the fund's +# stated strategy and N-PORT holdings. Pool labels for the report come +# from fundlab.pool. +# Candidate sets are curated to DISTINCT AXES (one representative per +# sleeve): near-duplicates (ivv/vt/vti, efa/vea, agg/vbtlx) would make the +# OLS knife-edge and produce offsetting betas. Discovery happens within +# this set; the set itself comes from the fund's stated strategy + N-PORT. +CANDIDATES: dict[str, list[str]] = { + # QQQ 65% + SPY 29% + cash, options overlay + "atesx": ["qqq", "ivv", "iwm", "shv", "bil"], + # systematic short-duration IG credit + cash + "atrfx": ["vstbx", "vicbx", "vweax", "ief", "ivv", "shv", "bil"], + # market neutral: long equity, short credit + "cvsix": ["ivv", "ive", "vweax", "vicbx", "tlt", "shv", "bil"], + # US large-cap core plus + "jlpsx": ["ivv", "ijt", "iwm", "efa", "vwo", "shv", "bil"], + # global multi-asset + "pmaix": ["ivv", "efa", "vwo", "vnq", "agg", "vweax", "djp", "gld", + "fxe", "tlt", "shv", "bil"], + # long/short mortgage & ABS + "pmorx": ["vmbix", "vweax", "vicbx", "finux", "ief", "tlt", "shv", + "bil"], + # market-neutral style premia + "qspnx": ["ive", "ivw", "iwm", "ijt", "ijs", "efa", "shv", "bil"], + # low-volatility equity fund of funds + "svarx": ["ivv", "ive", "ijk", "efa", "vwo", "shv", "bil", "agg"], + # full credit-spectrum strategic income + "cosix": ["agg", "vweax", "vmbix", "finux", "fghnx", "ief", "tlt", + "shv", "bil"], + # hedge fund (multi-strategy) + "mbxix": ["ivv", "efa", "tlt", "ief", "vweax", "djp", "gld", "fxe", + "shv", "bil"], + # global macro, sovereign-centric + "eagmx": ["tlt", "ief", "vweax", "finux", "fxe", "fxb", "fxy", "djp", + "gld", "shv", "bil"], + # Leuthold Core: fund of ETFs (no return history - holdings only) + "lcorx": ["ivv", "efa", "vwo", "agg", "djp", "shv", "bil"], + # US dividend-growth equity + "lamhx": ["ivv", "ive", "ijk", "ijt", "iwm", "efa", "shv", "bil"], +} +# share classes: same underlying fund +ALIAS = {"pmfkx": "pmaix", "lcrix": "lcorx", "egrsx": "eagmx"} +FULL_WINDOW = "1990-01-01" # effectively all history +RECENT_WINDOW = "2021-01-01" # last ~5 years + + +def _drift(rb: pd.DataFrame, full_beta: np.ndarray) -> dict[str, float]: + """Mean |rolling beta - full-sample beta| per component, as a fraction + of max(|full beta|, 0.25) — 0 means perfectly stable weights.""" + out = {} + for k, col in enumerate(rb.columns): + b = full_beta[k + 1] + d = float((rb[col] - b).abs().mean()) + out[col] = d / max(abs(b), 0.25) + return out + + +def decompose(fund: str, start: str = FULL_WINDOW, + candidates: dict[str, list[str]] | None = None) -> dict: + cand_syms = (candidates or CANDIDATES).get(fund, []) + r = returns_panel([fund] + cand_syms, start=start) + if fund not in r.columns: + return {"fund": fund, "error": "no return history in the data set", + "verdict": "no return history", "start": start} + y = r[fund].to_numpy() + dates = r.index + cand = {s: r[s].to_numpy() for s in r.columns if s != fund} + y_ok = ~np.isnan(y) + if y_ok.sum() < 252: + return {"fund": fund, "error": "insufficient return history", + "verdict": "insufficient return history", "start": start} + chosen, m, ok = forward_select(y, cand, y_ok=y_ok) + d0 = dates[ok] + ann_alpha = float(m["beta"][0]) * 252 + te = float(np.std(m["resid"]) * np.sqrt(252)) + port_ann = float(np.mean(y[ok]) * 252) + out = { + "fund": fund, + "start": str(d0[0].date()), + "end": str(d0[-1].date()), + "n_obs": int(m["n"]), + "components": [ + {"sym": s, "label": _label(s), "beta": float(m["beta"][k + 1]), + "t": float(m["t"][k + 1])} + for k, s in enumerate(chosen)], + "alpha_ann": ann_alpha, + "alpha_t": float(m["t"][0]), + "r2": float(m["r2"]), + "adj_r2": float(m["adj_r2"]), + "tracking_err_ann": te, + "fund_ann_return": port_ann, + "dw": float(m["dw"]), + "weights_sum": float(m["beta"][1:].sum()), + } + # rolling stability of the chosen model (on the same complete cases) + if chosen: + X = np.column_stack([np.ones(int(ok.sum())), + *[r[s].to_numpy()[ok] for s in chosen]]) + rb = rolling_beta(y[ok], X, dates=d0) + drift = _drift(rb, m["beta"]) + out["rolling"] = { + "window": 252, + "drift": {s: float(v) for s, v in drift.items()}, + "max_drift": float(max(drift.values())) if drift else 0.0, + "n_windows": int(len(rb)), + } + out["verdict"] = _verdict(out) + return out + + +# per-fund cross-check notes: what the N-PORT holdings + strategy say, and +# how the returns decomposition should be read against that +NOTES: dict[str, str] = { + "atesx": ("Holdings (May 2026): QQQ 65% + SPY 29% + MMF 0.6%, with 4.9% " + "'other assets in excess of liabilities' — an options overlay. " + "But the rolling beta to those SAME holdings stays 0.13–0.89 " + "(median 0.30, never above 1): the 'risk managed' in the name is " + "real — a systematic equity de-risking overlay. Decomposition: one " + "TACTICAL US-equity sleeve, not a static mix."), + "atrfx": ("Systematic alpha over short-duration IG credit + cash. Returns are " + "dominated by idiosyncratic credit/derivatives P&L (R² ≤ 0.22 vs bond " + "sleeves) and the best-fit weights are knife-edge. Read as: " + "cash-like carry + systematic alpha, no meaningful static sleeve."), + "cvsix": ("Market neutral (long US equity, short credit). Full sample (since " + "1990) is unexplainable — the strategy has changed over 36 years; the " + "last 5 years show a small net equity/credit tilt (ivv +0.14, " + "vweax +0.06) explaining 74%. The rest is spread/option alpha " + "(full-sample annualized alpha +5.5%, t=6.7)."), + "jlpsx": ("US large-cap core plus: essentially 1.04x the S&P 500 " + "(R² 0.96 over 5y, stable). The 'plus' is small optionality " + "(tiny ijt/vwo tilts in the 5y fit). The cleanest fund on the list."), + "pmaix": ("Global multi-asset fund of funds (N-PORT: 99.5% in unaffiliated " + "underlying funds/loans). Returns decompose into high-yield credit " + "(vweax +0.62), intl equity (efa +0.23), commodities (+0.05), " + "bonds (−0.15): R² 0.68, stable weights, alpha +3.5%/yr (t=3.3). " + "The sleeves show through the underlying funds."), + "pmorx": ("Long/short mortgage & ABS — returns mostly idiosyncratic (R² 0.10). " + "5y direction is long MBS (vmbix +0.31) / short intermediate rates " + "(ief −0.39), consistent with a carry/relative-value mortgage " + "strategy. Not a static sleeve."), + "qspnx": ("Market-neutral style premia: no static sleeve explains returns " + "(R² 0.18 5y). Alpha vs a cash-like benchmark: +12.8%/yr full sample " + "(t=4.0). Decomposition = pure factor harvesting (value/size/style " + "tilts in both books); the 'exposures' in the table are residuals, " + "not sleeves."), + "svarx": ("Low-volatility equity fund of funds. Small but positive net market " + "(efa +0.07, agg +0.10; R² 0.24, low drift). The edge is in " + "volatility selection, not the mix: +5.2%/yr alpha over that small " + "sleeve (t=5.1)."), + "cosix": ("Strategic income across the credit spectrum. Full sample (since " + "1990) unexplainable — vintage; the last 5 years are the honest " + "current mix: high-yield +0.30, MBS +0.29, IG core +0.18 (R² 0.86)."), + "mbxix": ("Multi-strategy hedge fund: 53% explained over a decade " + "(ivv +0.39, ief −0.67, fxe −0.28, tlt +0.17, djp +0.08) — equity " + "long, duration short, FX/commodity tilts, large active residual. " + "Caveat: newest N-PORT on file is Sep 2024 — the fund may have " + "changed strategy or stopped filing."), + "eagmx": ("Global macro (sovereign-centric): nothing explains returns in the " + "full or 5y window (R² ≤ 0.05) — textbook macro, positions are " + "tactical and asset-agnostic. The whole story is +5.1%/yr (t=8.0) " + "over a flat benchmark."), + "lcorx": ("NEW share classes (trading since Jul 2026) — no return history to " + "regress. Holdings (Dec 2025 N-PORT): 91.7% ETFs + 8.4% money " + "market; the strategy is Leuthold core multi-asset via ETFs. " + "Re-run the decomposition after a year of NAV accumulates."), + "lamhx": ("Dividend growth: R² 0.95; S&P 500 + value/mid tilt " + "(ivv +0.62, ive +0.26, ijk +0.20, iwm −0.14 over 5y), stable " + "weights. Closest to a passive fund with an overlay on this list."), +} + + +def _label(sym: str) -> str: + try: + for e in _pool.load_pool(): + if e.symbol == sym: + return e.label + except Exception: + pass + return sym + + +def _verdict(d: dict) -> str: + if d.get("error"): + return d["error"] + md = d.get("rolling", {}).get("max_drift", 0.0) + r2 = d["r2"] + if r2 >= 0.95 and md < 0.5: + v = "static sleeve mix (weights stable, ~fully explained)" + elif r2 >= 0.85: + v = "mostly stable sleeve mix" + ("" if md < 1.0 else " with drifting weights") + elif r2 >= 0.6: + v = "partially explainable — material active/timing residual" + else: + v = "not a static sleeve mix — returns driven by active decisions" + if d.get("components") is None or not d.get("components"): + v = "no component explains returns (market neutral or cash-like)" + return v + + +def run_all() -> dict[str, dict]: + out = {} + done = set() + for fund in CANDIDATES: + if fund in ALIAS or fund in done: # share class: parent covers it + continue + full = decompose(fund) + full["recent"] = decompose(fund, start=RECENT_WINDOW) + full["note"] = NOTES.get(fund, "") + # a much better 5y fit than the full sample means the strategy + # itself changed — say so in the verdict + rec = full["recent"] + if ("r2" in rec and "r2" in full and rec["r2"] > full["r2"] + 0.2): + full["verdict"] += (f" [strategy evolved — 5y R² = " + f"{rec['r2']:.2f}]") + out[fund] = full + done.add(fund) + print(f"{fund}: {full['verdict'][:90]}", flush=True) + # share classes reference their parent fund + for cls, parent in ALIAS.items(): + if parent in out: + d = dict(out[parent]) + d["fund"] = cls + d["note"] = (f"share class of {parent} — identical report. " + + d.get("note", "")) + out[cls] = d + RESULTS.write_text(json.dumps(out, indent=1)) + return out + + +if __name__ == "__main__": + run_all() diff --git a/fundlab/decompose_results.json b/fundlab/decompose_results.json new file mode 100644 index 0000000..e0cf166 --- /dev/null +++ b/fundlab/decompose_results.json @@ -0,0 +1,1340 @@ +{ + "atesx": { + "fund": "atesx", + "start": "2016-09-07", + "end": "2026-08-21", + "n_obs": 2503, + "components": [ + { + "sym": "qqq", + "label": "Nasdaq 100", + "beta": 0.45556908855154976, + "t": 21.168384781367326 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.23830733177169833, + "t": -8.880615083435835 + } + ], + "alpha_ann": 0.026346531979070175, + "alpha_t": 0.9293227873648724, + "r2": 0.3450047574870089, + "adj_r2": 0.34448076129299854, + "tracking_err_ann": 0.08913637737874539, + "fund_ann_return": 0.0858412132654425, + "dw": 2.2341043107825733, + "weights_sum": 0.21726175677985143, + "rolling": { + "window": 252, + "drift": { + "x0": 0.27669921266438996, + "x1": 0.848102513519347 + }, + "max_drift": 0.848102513519347, + "n_windows": 108 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "atesx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "qqq", + "label": "Nasdaq 100", + "beta": 0.44616170327416194, + "t": 14.149678424413535 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.22538567937254084, + "t": -5.264409165765193 + } + ], + "alpha_ann": 0.0055503014317009975, + "alpha_t": 0.1484113857382209, + "r2": 0.3610309574164604, + "adj_r2": 0.36012526068707196, + "tracking_err_ann": 0.08832002153748113, + "fund_ann_return": 0.05033812888713632, + "dw": 2.1534721910947656, + "weights_sum": 0.2207760239016211, + "rolling": { + "window": 252, + "drift": { + "x0": 0.2848956566742419, + "x1": 0.8185826141529595 + }, + "max_drift": 0.8185826141529595, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Holdings (May 2026): QQQ 65% + SPY 29% + MMF 0.6%, with 4.9% 'other assets in excess of liabilities' \u2014 an options overlay. But the rolling beta to those SAME holdings stays 0.13\u20130.89 (median 0.30, never above 1): the 'risk managed' in the name is real \u2014 a systematic equity de-risking overlay. Decomposition: one TACTICAL US-equity sleeve, not a static mix." + }, + "atrfx": { + "fund": "atrfx", + "start": "2014-08-04", + "end": "2026-08-21", + "n_obs": 3030, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.19562032232528356, + "t": 11.729655094756636 + }, + { + "sym": "vicbx", + "label": "intermediate-term corporate bond", + "beta": 1.2393291638674306, + "t": 6.561747110208925 + }, + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.5478003004605062, + "t": -5.661209653417961 + }, + { + "sym": "vstbx", + "label": "short-term corporate bond", + "beta": -1.0095528723812663, + "t": -3.2405021184626386 + } + ], + "alpha_ann": 0.026673637645486293, + "alpha_t": 0.6555619809844426, + "r2": 0.10182834965198462, + "adj_r2": 0.10064068465978881, + "tracking_err_ann": 0.13999308469624303, + "fund_ann_return": 0.06002154419716573, + "dw": 1.9501017065798456, + "weights_sum": -0.12240368664905843, + "rolling": { + "window": 252, + "drift": { + "x0": 0.9561739344457876, + "x1": 0.8851118294710612, + "x2": 1.6558653570574795, + "x3": 1.3496745692775076 + }, + "max_drift": 1.6558653570574795, + "n_windows": 133 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "atrfx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.41897124333609137, + "t": 14.248528357545942 + }, + { + "sym": "shv", + "label": "US short-term treasuries", + "beta": -7.305355576774472, + "t": -4.903013063951641 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.42005732490633185, + "t": 3.997263415922683 + } + ], + "alpha_ann": 0.2243721369422037, + "alpha_t": 2.8094259011461844, + "r2": 0.21865529238383408, + "adj_r2": 0.21699285683571456, + "tracking_err_ann": 0.15588237414909037, + "fund_ann_return": 0.08494859854904573, + "dw": 1.9902727126144717, + "weights_sum": -6.466327008532049, + "rolling": { + "window": 252, + "drift": { + "x0": 0.8466015103540322, + "x1": 0.5239705908149238, + "x2": 0.5882002201443478 + }, + "max_drift": 0.8466015103540322, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Systematic alpha over short-duration IG credit + cash. Returns are dominated by idiosyncratic credit/derivatives P&L (R\u00b2 \u2264 0.22 vs bond sleeves) and the best-fit weights are knife-edge. Read as: cash-like carry + systematic alpha, no meaningful static sleeve." + }, + "cvsix": { + "fund": "cvsix", + "start": "1990-09-04", + "end": "2026-08-21", + "n_obs": 9057, + "components": [], + "alpha_ann": 0.05494737178055986, + "alpha_t": 6.693203697489781, + "r2": 3.3306690738754696e-15, + "adj_r2": 3.4416913763379853e-15, + "tracking_err_ann": 0.04921311085638685, + "fund_ann_return": 0.054947371780559855, + "dw": 2.201070204327631, + "weights_sum": 0.0, + "verdict": "no component explains returns (market neutral or cash-like) [strategy evolved \u2014 5y R\u00b2 = 0.74]", + "recent": { + "fund": "cvsix", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.14337409425906908, + "t": 23.56952105733188 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.055353843180348514, + "t": 4.856854249459811 + }, + { + "sym": "ive", + "label": "S&P 500 Value", + "beta": 0.025427867003656124, + "t": 3.6082323626617847 + } + ], + "alpha_ann": 0.018963344604106105, + "alpha_t": 2.625529185806505, + "r2": 0.7365622841041414, + "adj_r2": 0.7360017783256396, + "tracking_err_ann": 0.017045091698141286, + "fund_ann_return": 0.04743113695845717, + "dw": 2.4362438569399654, + "weights_sum": 0.2241558044430737, + "rolling": { + "window": 252, + "drift": { + "x0": 0.16656102001355494, + "x1": 0.09765665332386918, + "x2": 0.11331311792968189 + }, + "max_drift": 0.16656102001355494, + "n_windows": 56 + }, + "verdict": "partially explainable \u2014 material active/timing residual" + }, + "note": "Market neutral (long US equity, short credit). Full sample (since 1990) is unexplainable \u2014 the strategy has changed over 36 years; the last 5 years show a small net equity/credit tilt (ivv +0.14, vweax +0.06) explaining 74%. The rest is spread/option alpha (full-sample annualized alpha +5.5%, t=6.7)." + }, + "jlpsx": { + "fund": "jlpsx", + "start": "2005-11-02", + "end": "2026-08-24", + "n_obs": 5233, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 1.0437558832956155, + "t": 162.56889184909045 + } + ], + "alpha_ann": 0.011050486799195222, + "alpha_t": 0.5691336721993374, + "r2": 0.8347738874968654, + "adj_r2": 0.8347423015453259, + "tracking_err_ann": 0.08838608033412072, + "fund_ann_return": 0.14222655212102187, + "dw": 2.8431265199678273, + "weights_sum": 1.0437558832956155, + "rolling": { + "window": 252, + "drift": { + "x0": 0.0438981065572047 + }, + "max_drift": 0.0438981065572047, + "n_windows": 238 + }, + "verdict": "partially explainable \u2014 material active/timing residual", + "recent": { + "fund": "jlpsx", + "start": "2021-01-05", + "end": "2026-08-24", + "n_obs": 1415, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 1.034383539393894, + "t": 91.67948652777572 + }, + { + "sym": "ijt", + "label": "S&P Small-Cap 600 Growth", + "beta": -0.029869184923710934, + "t": -3.6565866275389944 + }, + { + "sym": "vwo", + "label": "emerging markets stock", + "beta": 0.024879403995978433, + "t": 3.325899423592402 + } + ], + "alpha_ann": -0.0029851840422737987, + "alpha_t": -0.19449233213311515, + "r2": 0.9561755665716403, + "adj_r2": 0.9560823891795176, + "tracking_err_ann": 0.0362318939737439, + "fund_ann_return": 0.15826248266783624, + "dw": 2.2564400244536875, + "weights_sum": 1.0293937584661617, + "rolling": { + "window": 252, + "drift": { + "x0": 0.029887310344874126, + "x1": 0.1450970324235002, + "x2": 0.09661012864484575 + }, + "max_drift": 0.1450970324235002, + "n_windows": 56 + }, + "verdict": "static sleeve mix (weights stable, ~fully explained)" + }, + "note": "US large-cap core plus: essentially 1.04x the S&P 500 (R\u00b2 0.96 over 5y, stable). The 'plus' is small optionality (tiny ijt/vwo tilts in the 5y fit). The cleanest fund on the list." + }, + "pmaix": { + "fund": "pmaix", + "start": "2011-12-23", + "end": "2026-08-21", + "n_obs": 3685, + "components": [ + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.2344874843476855, + "t": 30.602178496272092 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.6192246204138075, + "t": 37.00150032703783 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.04740991451070612, + "t": 10.82367809724191 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": -0.15148499144323813, + "t": -10.175119663083366 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.050460193217015836, + "t": -6.080317182282203 + }, + { + "sym": "vnq", + "label": "US real estate (REITs)", + "beta": 0.023020899742284634, + "t": 4.40169223227429 + } + ], + "alpha_ann": 0.035437156651329446, + "alpha_t": 3.3490804145584185, + "r2": 0.679121578877175, + "adj_r2": 0.6785981230515259, + "tracking_err_ann": 0.040264984588024334, + "fund_ann_return": 0.08546192744916578, + "dw": 2.088102202855144, + "weights_sum": 0.7221977343542297, + "rolling": { + "window": 252, + "drift": { + "x0": 0.20577309247165354, + "x1": 0.18907744513443525, + "x2": 0.158769877923333, + "x3": 0.4541512565652454, + "x4": 0.3837359243950157, + "x5": 0.165791799569151 + }, + "max_drift": 0.4541512565652454, + "n_windows": 164 + }, + "verdict": "partially explainable \u2014 material active/timing residual", + "recent": { + "fund": "pmaix", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.2449954218314655, + "t": 19.03579186143622 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.5344327280136246, + "t": 16.011005736881103 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.06919531081867039, + "t": 10.653443293821173 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": -0.1934942270463675, + "t": -8.131663840261679 + }, + { + "sym": "vnq", + "label": "US real estate (REITs)", + "beta": 0.066420197792225, + "t": 7.199106958431038 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.08454175511501988, + "t": -6.445665952633282 + } + ], + "alpha_ann": 0.05220162671812971, + "alpha_t": 2.7522346782393368, + "r2": 0.6089406987814124, + "adj_r2": 0.6072730684990304, + "tracking_err_ann": 0.04465476548081441, + "fund_ann_return": 0.10594922784805459, + "dw": 1.935004996999197, + "weights_sum": 0.637007676294598, + "rolling": { + "window": 252, + "drift": { + "x0": 0.16713565476068823, + "x1": 0.26061370415899987, + "x2": 0.08726856185338772, + "x3": 0.16461642759479633, + "x4": 0.13071922650272086, + "x5": 0.21414714053386924 + }, + "max_drift": 0.26061370415899987, + "n_windows": 56 + }, + "verdict": "partially explainable \u2014 material active/timing residual" + }, + "note": "Global multi-asset fund of funds (N-PORT: 99.5% in unaffiliated underlying funds/loans). Returns decompose into high-yield credit (vweax +0.62), intl equity (efa +0.23), commodities (+0.05), bonds (\u22120.15): R\u00b2 0.68, stable weights, alpha +3.5%/yr (t=3.3). The sleeves show through the underlying funds." + }, + "pmorx": { + "fund": "pmorx", + "start": "2019-07-01", + "end": "2026-08-24", + "n_obs": 1797, + "components": [ + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.31388720384359853, + "t": 14.18893920835988 + } + ], + "alpha_ann": 0.029332925492427733, + "alpha_t": 1.4357260819167152, + "r2": 0.10084825638507544, + "adj_r2": 0.10034733619364655, + "tracking_err_ann": 0.054457753005862385, + "fund_ann_return": 0.04399267904999808, + "dw": 2.051284316393219, + "weights_sum": 0.31388720384359853, + "rolling": { + "window": 252, + "drift": { + "x0": 0.7331280914924483 + }, + "max_drift": 0.7331280914924483, + "n_windows": 74 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "pmorx", + "start": "2021-01-05", + "end": "2026-08-24", + "n_obs": 1415, + "components": [ + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.04168035087416321, + "t": 2.2063240080677304 + }, + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.3867098615283498, + "t": -10.37474630140078 + }, + { + "sym": "vmbix", + "label": "mortgage-backed securities", + "beta": 0.31402590773539685, + "t": 9.161419056733536 + }, + { + "sym": "tlt", + "label": "US long-term treasuries (20+ yr)", + "beta": 0.06735987458994033, + "t": 5.549320204906601 + } + ], + "alpha_ann": 0.0527292589505514, + "alpha_t": 4.2909358903055885, + "r2": 0.08657628999415645, + "adj_r2": 0.08398501705796968, + "tracking_err_ann": 0.028979898356966537, + "fund_ann_return": 0.05695447285684168, + "dw": 2.2442433975515654, + "weights_sum": 0.0363562716711506, + "rolling": { + "window": 252, + "drift": { + "x0": 0.21698400351224853, + "x1": 0.4064266430323177, + "x2": 0.3680865071797071, + "x3": 0.09762048759677722 + }, + "max_drift": 0.4064266430323177, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Long/short mortgage & ABS \u2014 returns mostly idiosyncratic (R\u00b2 0.10). 5y direction is long MBS (vmbix +0.31) / short intermediate rates (ief \u22120.39), consistent with a carry/relative-value mortgage strategy. Not a static sleeve." + }, + "qspnx": { + "fund": "qspnx", + "start": "2013-10-31", + "end": "2026-08-21", + "n_obs": 3220, + "components": [ + { + "sym": "ivw", + "label": "S&P 500 Growth", + "beta": -0.08192299528316607, + "t": -4.0260419229419835 + }, + { + "sym": "ive", + "label": "S&P 500 Value", + "beta": 0.2609710931386727, + "t": 9.495012993507824 + }, + { + "sym": "iwm", + "label": "US small cap", + "beta": -0.7033008999688797, + "t": -14.398386309918285 + }, + { + "sym": "ijt", + "label": "S&P Small-Cap 600 Growth", + "beta": 0.3751139296366351, + "t": 8.722604382154021 + }, + { + "sym": "ijs", + "label": "S&P Small-Cap 600 Value", + "beta": 0.1845783441822273, + "t": 6.386825106301779 + }, + { + "sym": "shv", + "label": "US short-term treasuries", + "beta": -2.754459031292181, + "t": -3.8820894019208985 + } + ], + "alpha_ann": 0.12818487124441533, + "alpha_t": 3.9840661559835087, + "r2": 0.1134410104537722, + "adj_r2": 0.11178543811101538, + "tracking_err_ann": 0.10508112594918419, + "fund_ann_return": 0.08091865655856706, + "dw": 1.8961660779849816, + "weights_sum": -2.7190195595866915, + "rolling": { + "window": 252, + "drift": { + "x0": 0.8863354304694652, + "x1": 0.8607703018245606, + "x2": 0.40949478429698954, + "x3": 0.3889681450569688, + "x4": 0.9937533641149741, + "x5": 1.4461092192565497 + }, + "max_drift": 1.4461092192565497, + "n_windows": 142 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "qspnx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ivw", + "label": "S&P 500 Growth", + "beta": -0.06943941702935164, + "t": -2.2675960385991307 + }, + { + "sym": "ive", + "label": "S&P 500 Value", + "beta": 0.21375136440801723, + "t": 4.0635176755747535 + }, + { + "sym": "iwm", + "label": "US small cap", + "beta": -0.6189777356607347, + "t": -11.154913059718409 + }, + { + "sym": "ijs", + "label": "S&P Small-Cap 600 Value", + "beta": 0.48890809639467814, + "t": 8.976960770542815 + }, + { + "sym": "shv", + "label": "US short-term treasuries", + "beta": -6.202064838851471, + "t": -5.221795262354262 + } + ], + "alpha_ann": 0.37899676888646366, + "alpha_t": 5.884228223888841, + "r2": 0.17907422272719475, + "adj_r2": 0.17615900334767476, + "tracking_err_ann": 0.1258743914848915, + "fund_ann_return": 0.20378567372709033, + "dw": 1.8476027480671517, + "weights_sum": -6.1878225307388615, + "rolling": { + "window": 252, + "drift": { + "x0": 0.4695949092898872, + "x1": 0.8640997084339416, + "x2": 0.6037347855639, + "x3": 0.7978620842217455, + "x4": 0.6116773035053029 + }, + "max_drift": 0.8640997084339416, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Market-neutral style premia: no static sleeve explains returns (R\u00b2 0.18 5y). Alpha vs a cash-like benchmark: +12.8%/yr full sample (t=4.0). Decomposition = pure factor harvesting (value/size/style tilts in both books); the 'exposures' in the table are residuals, not sleeves." + }, + "svarx": { + "fund": "svarx", + "start": "2013-12-18", + "end": "2026-08-21", + "n_obs": 3187, + "components": [ + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.06942890671894897, + "t": 18.07601188807692 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": 0.10001905582875992, + "t": 7.751274664401302 + } + ], + "alpha_ann": 0.05206713295886885, + "alpha_t": 5.103917116288613, + "r2": 0.12071609207684797, + "adj_r2": 0.12016377806433343, + "tracking_err_ann": 0.03623399351955989, + "fund_ann_return": 0.06016511606656689, + "dw": 2.1930328695290275, + "weights_sum": 0.16944796254770889, + "rolling": { + "window": 252, + "drift": { + "x0": 0.1373472721585143, + "x1": 0.2623420377551537 + }, + "max_drift": 0.2623420377551537, + "n_windows": 140 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "svarx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "agg", + "label": "US aggregate bond", + "beta": 0.16482774305146136, + "t": 13.530158875689205 + }, + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.05272328376723269, + "t": 11.974320936849248 + } + ], + "alpha_ann": 0.02851487058619257, + "alpha_t": 2.6098427381219285, + "r2": 0.24198871761674323, + "adj_r2": 0.24091428631641254, + "tracking_err_ann": 0.02582591212260749, + "fund_ann_return": 0.03414405706979862, + "dw": 2.027102891716548, + "weights_sum": 0.21755102681869404, + "rolling": { + "window": 252, + "drift": { + "x0": 0.21771692656768357, + "x1": 0.06046195722494479 + }, + "max_drift": 0.21771692656768357, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Low-volatility equity fund of funds. Small but positive net market (efa +0.07, agg +0.10; R\u00b2 0.24, low drift). The edge is in volatility selection, not the mix: +5.2%/yr alpha over that small sleeve (t=5.1)." + }, + "cosix": { + "fund": "cosix", + "start": "1990-01-03", + "end": "2026-08-21", + "n_obs": 9226, + "components": [], + "alpha_ann": 0.058717525483004025, + "alpha_t": 8.97921782878531, + "r2": 3.3306690738754696e-16, + "adj_r2": 4.440892098500626e-16, + "tracking_err_ann": 0.039565089695926974, + "fund_ann_return": 0.05871752548300402, + "dw": 1.7589309508798752, + "weights_sum": 0.0, + "verdict": "no component explains returns (market neutral or cash-like) [strategy evolved \u2014 5y R\u00b2 = 0.86]", + "recent": { + "fund": "cosix", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "vmbix", + "label": "mortgage-backed securities", + "beta": 0.288460763166248, + "t": 12.452945480339036 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.29933745310561866, + "t": 28.620403943860424 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": 0.1773268606937985, + "t": 5.111860730207353 + }, + { + "sym": "tlt", + "label": "US long-term treasuries (20+ yr)", + "beta": 0.023873312991125495, + "t": 3.2290450844819194 + } + ], + "alpha_ann": 0.010013564293547298, + "alpha_t": 1.4857644255260904, + "r2": 0.8602970091640032, + "adj_r2": 0.8599004073447385, + "tracking_err_ann": 0.015882120791285648, + "fund_ann_return": 0.021278965202663088, + "dw": 2.102412506087817, + "weights_sum": 0.7889983899567907, + "rolling": { + "window": 252, + "drift": { + "x0": 0.42276301035369823, + "x1": 0.24123352544971702, + "x2": 0.7930957664509884, + "x3": 0.2024940055906148 + }, + "max_drift": 0.7930957664509884, + "n_windows": 56 + }, + "verdict": "mostly stable sleeve mix" + }, + "note": "Strategic income across the credit spectrum. Full sample (since 1990) unexplainable \u2014 vintage; the last 5 years are the honest current mix: high-yield +0.30, MBS +0.29, IG core +0.18 (R\u00b2 0.86)." + }, + "mbxix": { + "fund": "mbxix", + "start": "2015-12-29", + "end": "2026-08-21", + "n_obs": 2677, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.39245838142956163, + "t": 20.072399939088108 + }, + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.6691512098665768, + "t": -9.960156247284127 + }, + { + "sym": "fxe", + "label": "Euro", + "beta": -0.28134567186258985, + "t": -9.853324593642872 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.08330432021118495, + "t": 7.670473267330599 + }, + { + "sym": "tlt", + "label": "US long-term treasuries (20+ yr)", + "beta": 0.17450273619844878, + "t": 5.921229335495926 + }, + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.10847192647358558, + "t": 5.004925817589 + } + ], + "alpha_ann": 0.024820519135557266, + "alpha_t": 0.8961816713889841, + "r2": 0.5321084883020772, + "adj_r2": 0.531057046702756, + "tracking_err_ann": 0.0899358494603154, + "fund_ann_return": 0.09479368792724513, + "dw": 1.9290049781910437, + "weights_sum": -0.19175951741638578, + "rolling": { + "window": 252, + "drift": { + "x0": 0.2670019959185202, + "x1": 0.7612945161886517, + "x2": 0.5685135801904753, + "x3": 0.25349794378898627, + "x4": 0.42287402866416995, + "x5": 0.5520356165472149 + }, + "max_drift": 0.7612945161886517, + "n_windows": 116 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions", + "recent": { + "fund": "mbxix", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.4102633005758794, + "t": 24.341611835442336 + }, + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.7200822566052334, + "t": -9.045916015900026 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.10085813682120615, + "t": 7.96335478113521 + }, + { + "sym": "fxe", + "label": "Euro", + "beta": -0.1852046053656746, + "t": -5.387240877525066 + }, + { + "sym": "tlt", + "label": "US long-term treasuries (20+ yr)", + "beta": 0.1371607922006066, + "t": 3.7298157820718036 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": -0.2190781037842444, + "t": -3.4498633589251306 + } + ], + "alpha_ann": 0.026755231746647828, + "alpha_t": 0.7260542397794253, + "r2": 0.4427937324584458, + "adj_r2": 0.4404175863282046, + "tracking_err_ann": 0.08668088600277926, + "fund_ann_return": 0.10072573524358285, + "dw": 1.8935415103077673, + "weights_sum": -0.4760827361574602, + "rolling": { + "window": 252, + "drift": { + "x0": 0.19181939552220412, + "x1": 0.3354105640246233, + "x2": 0.2047082089271779, + "x3": 0.5337763768544731, + "x4": 0.31529704059151686, + "x5": 1.158363172487587 + }, + "max_drift": 1.158363172487587, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Multi-strategy hedge fund: 53% explained over a decade (ivv +0.39, ief \u22120.67, fxe \u22120.28, tlt +0.17, djp +0.08) \u2014 equity long, duration short, FX/commodity tilts, large active residual. Caveat: newest N-PORT on file is Sep 2024 \u2014 the fund may have changed strategy or stopped filing." + }, + "eagmx": { + "fund": "eagmx", + "start": "1997-11-03", + "end": "2026-08-21", + "n_obs": 7244, + "components": [], + "alpha_ann": 0.050824482110222884, + "alpha_t": 8.039707312489185, + "r2": -1.1102230246251565e-15, + "adj_r2": -1.1102230246251565e-15, + "tracking_err_ann": 0.033891570631519576, + "fund_ann_return": 0.0508244821102229, + "dw": 1.9385563443482812, + "weights_sum": 0.0, + "verdict": "no component explains returns (market neutral or cash-like)", + "recent": { + "fund": "eagmx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.08612900590965679, + "t": -7.775770337985069 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.09714053153148342, + "t": 6.2437458560353 + }, + { + "sym": "fxy", + "label": "Japanese Yen", + "beta": 0.029519557759761714, + "t": 3.7673950023045486 + } + ], + "alpha_ann": 0.055900808564439394, + "alpha_t": 5.20066580035999, + "r2": 0.05095371708568386, + "adj_r2": 0.048934469675227854, + "tracking_err_ann": 0.02533976434093521, + "fund_ann_return": 0.058946455973364384, + "dw": 1.837466450766458, + "weights_sum": 0.04053108338158834, + "rolling": { + "window": 252, + "drift": { + "x0": 0.3492893563867785, + "x1": 0.20298329394851625, + "x2": 0.07174902615914276 + }, + "max_drift": 0.3492893563867785, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "Global macro (sovereign-centric): nothing explains returns in the full or 5y window (R\u00b2 \u2264 0.05) \u2014 textbook macro, positions are tactical and asset-agnostic. The whole story is +5.1%/yr (t=8.0) over a flat benchmark." + }, + "lcorx": { + "fund": "lcorx", + "error": "no return history in the data set", + "verdict": "no return history", + "start": "1990-01-01", + "recent": { + "fund": "lcorx", + "error": "no return history in the data set", + "verdict": "no return history", + "start": "2021-01-01" + }, + "note": "NEW share classes (trading since Jul 2026) \u2014 no return history to regress. Holdings (Dec 2025 N-PORT): 91.7% ETFs + 8.4% money market; the strategy is Leuthold core multi-asset via ETFs. Re-run the decomposition after a year of NAV accumulates." + }, + "lamhx": { + "fund": "lamhx", + "start": "2015-07-01", + "end": "2026-08-24", + "n_obs": 2803, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.5924375150630279, + "t": 47.111831514579826 + }, + { + "sym": "ive", + "label": "S&P 500 Value", + "beta": 0.287611197385718, + "t": 24.519604228970984 + }, + { + "sym": "ijk", + "label": "S&P Mid-Cap 400 Growth", + "beta": 0.21615100882604454, + "t": 16.024470374704247 + }, + { + "sym": "iwm", + "label": "US small cap", + "beta": -0.23713306806264878, + "t": -15.301969229049684 + }, + { + "sym": "ijt", + "label": "S&P Small-Cap 600 Growth", + "beta": 0.08182509699723003, + "t": 5.251050388316936 + }, + { + "sym": "shv", + "label": "US short-term treasuries", + "beta": 0.8402716605552001, + "t": 3.371728168166483 + } + ], + "alpha_ann": -0.013142415064292253, + "alpha_t": -1.100272198008015, + "r2": 0.9538473658830551, + "adj_r2": 0.9537483258956797, + "tracking_err_ann": 0.03577603601650824, + "fund_ann_return": 0.13631999009039603, + "dw": 2.1773606534393157, + "weights_sum": 1.781163410764572, + "rolling": { + "window": 252, + "drift": { + "x0": 0.1728242094144316, + "x1": 0.34751423069844256, + "x2": 0.2577728950590679, + "x3": 0.44186860937361244, + "x4": 0.3668810220684854, + "x5": 1.6462023135541486 + }, + "max_drift": 1.6462023135541486, + "n_windows": 122 + }, + "verdict": "mostly stable sleeve mix with drifting weights", + "recent": { + "fund": "lamhx", + "start": "2021-01-05", + "end": "2026-08-24", + "n_obs": 1415, + "components": [ + { + "sym": "ivv", + "label": "S&P 500", + "beta": 0.6193099922725518, + "t": 40.4949370126514 + }, + { + "sym": "ive", + "label": "S&P 500 Value", + "beta": 0.26293261022523196, + "t": 16.74318387082312 + }, + { + "sym": "ijk", + "label": "S&P Mid-Cap 400 Growth", + "beta": 0.19701675392247014, + "t": 12.023321527643459 + }, + { + "sym": "iwm", + "label": "US small cap", + "beta": -0.14148284702566433, + "t": -10.876289620460732 + } + ], + "alpha_ann": -0.0024165247422386882, + "alpha_t": -0.15995398884720494, + "r2": 0.9442029093804821, + "adj_r2": 0.9440446197617034, + "tracking_err_ann": 0.03562317439718008, + "fund_ann_return": 0.13829292090048273, + "dw": 2.098803191816331, + "weights_sum": 0.9377765093945896, + "rolling": { + "window": 252, + "drift": { + "x0": 0.09305182386862508, + "x1": 0.32077302560627446, + "x2": 0.2546046400945276, + "x3": 0.2629175684280181 + }, + "max_drift": 0.32077302560627446, + "n_windows": 56 + }, + "verdict": "mostly stable sleeve mix" + }, + "note": "Dividend growth: R\u00b2 0.95; S&P 500 + value/mid tilt (ivv +0.62, ive +0.26, ijk +0.20, iwm \u22120.14 over 5y), stable weights. Closest to a passive fund with an overlay on this list." + }, + "pmfkx": { + "fund": "pmfkx", + "start": "2011-12-23", + "end": "2026-08-21", + "n_obs": 3685, + "components": [ + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.2344874843476855, + "t": 30.602178496272092 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.6192246204138075, + "t": 37.00150032703783 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.04740991451070612, + "t": 10.82367809724191 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": -0.15148499144323813, + "t": -10.175119663083366 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.050460193217015836, + "t": -6.080317182282203 + }, + { + "sym": "vnq", + "label": "US real estate (REITs)", + "beta": 0.023020899742284634, + "t": 4.40169223227429 + } + ], + "alpha_ann": 0.035437156651329446, + "alpha_t": 3.3490804145584185, + "r2": 0.679121578877175, + "adj_r2": 0.6785981230515259, + "tracking_err_ann": 0.040264984588024334, + "fund_ann_return": 0.08546192744916578, + "dw": 2.088102202855144, + "weights_sum": 0.7221977343542297, + "rolling": { + "window": 252, + "drift": { + "x0": 0.20577309247165354, + "x1": 0.18907744513443525, + "x2": 0.158769877923333, + "x3": 0.4541512565652454, + "x4": 0.3837359243950157, + "x5": 0.165791799569151 + }, + "max_drift": 0.4541512565652454, + "n_windows": 164 + }, + "verdict": "partially explainable \u2014 material active/timing residual", + "recent": { + "fund": "pmaix", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "efa", + "label": "developed markets (ex-US, MSCI)", + "beta": 0.2449954218314655, + "t": 19.03579186143622 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.5344327280136246, + "t": 16.011005736881103 + }, + { + "sym": "djp", + "label": "Dow Jones-UBS Commodity Index", + "beta": 0.06919531081867039, + "t": 10.653443293821173 + }, + { + "sym": "agg", + "label": "US aggregate bond", + "beta": -0.1934942270463675, + "t": -8.131663840261679 + }, + { + "sym": "vnq", + "label": "US real estate (REITs)", + "beta": 0.066420197792225, + "t": 7.199106958431038 + }, + { + "sym": "ivv", + "label": "S&P 500", + "beta": -0.08454175511501988, + "t": -6.445665952633282 + } + ], + "alpha_ann": 0.05220162671812971, + "alpha_t": 2.7522346782393368, + "r2": 0.6089406987814124, + "adj_r2": 0.6072730684990304, + "tracking_err_ann": 0.04465476548081441, + "fund_ann_return": 0.10594922784805459, + "dw": 1.935004996999197, + "weights_sum": 0.637007676294598, + "rolling": { + "window": 252, + "drift": { + "x0": 0.16713565476068823, + "x1": 0.26061370415899987, + "x2": 0.08726856185338772, + "x3": 0.16461642759479633, + "x4": 0.13071922650272086, + "x5": 0.21414714053386924 + }, + "max_drift": 0.26061370415899987, + "n_windows": 56 + }, + "verdict": "partially explainable \u2014 material active/timing residual" + }, + "note": "share class of pmaix \u2014 identical report. Global multi-asset fund of funds (N-PORT: 99.5% in unaffiliated underlying funds/loans). Returns decompose into high-yield credit (vweax +0.62), intl equity (efa +0.23), commodities (+0.05), bonds (\u22120.15): R\u00b2 0.68, stable weights, alpha +3.5%/yr (t=3.3). The sleeves show through the underlying funds." + }, + "lcrix": { + "fund": "lcrix", + "error": "no return history in the data set", + "verdict": "no return history", + "start": "1990-01-01", + "recent": { + "fund": "lcorx", + "error": "no return history in the data set", + "verdict": "no return history", + "start": "2021-01-01" + }, + "note": "share class of lcorx \u2014 identical report. NEW share classes (trading since Jul 2026) \u2014 no return history to regress. Holdings (Dec 2025 N-PORT): 91.7% ETFs + 8.4% money market; the strategy is Leuthold core multi-asset via ETFs. Re-run the decomposition after a year of NAV accumulates." + }, + "egrsx": { + "fund": "egrsx", + "start": "1997-11-03", + "end": "2026-08-21", + "n_obs": 7244, + "components": [], + "alpha_ann": 0.050824482110222884, + "alpha_t": 8.039707312489185, + "r2": -1.1102230246251565e-15, + "adj_r2": -1.1102230246251565e-15, + "tracking_err_ann": 0.033891570631519576, + "fund_ann_return": 0.0508244821102229, + "dw": 1.9385563443482812, + "weights_sum": 0.0, + "verdict": "no component explains returns (market neutral or cash-like)", + "recent": { + "fund": "eagmx", + "start": "2021-01-05", + "end": "2026-08-21", + "n_obs": 1414, + "components": [ + { + "sym": "ief", + "label": "US intermediate treasuries (7-10 yr)", + "beta": -0.08612900590965679, + "t": -7.775770337985069 + }, + { + "sym": "vweax", + "label": "high-yield corporate", + "beta": 0.09714053153148342, + "t": 6.2437458560353 + }, + { + "sym": "fxy", + "label": "Japanese Yen", + "beta": 0.029519557759761714, + "t": 3.7673950023045486 + } + ], + "alpha_ann": 0.055900808564439394, + "alpha_t": 5.20066580035999, + "r2": 0.05095371708568386, + "adj_r2": 0.048934469675227854, + "tracking_err_ann": 0.02533976434093521, + "fund_ann_return": 0.058946455973364384, + "dw": 1.837466450766458, + "weights_sum": 0.04053108338158834, + "rolling": { + "window": 252, + "drift": { + "x0": 0.3492893563867785, + "x1": 0.20298329394851625, + "x2": 0.07174902615914276 + }, + "max_drift": 0.3492893563867785, + "n_windows": 56 + }, + "verdict": "not a static sleeve mix \u2014 returns driven by active decisions" + }, + "note": "share class of eagmx \u2014 identical report. Global macro (sovereign-centric): nothing explains returns in the full or 5y window (R\u00b2 \u2264 0.05) \u2014 textbook macro, positions are tactical and asset-agnostic. The whole story is +5.1%/yr (t=8.0) over a flat benchmark." + } +} \ No newline at end of file diff --git a/fundlab/nport_cache/atesx.json b/fundlab/nport_cache/atesx.json new file mode 100644 index 0000000..b77eb54 --- /dev/null +++ b/fundlab/nport_cache/atesx.json @@ -0,0 +1,53 @@ +{ + "sym": "atesx", + "as_of": "May 31, 2026", + "net_assets": 147227199.0, + "n_positions": 3, + "categories": [ + { + "name": "EXCHANGE-TRADED FUNDS", + "pct": 94.5 + }, + { + "name": "EQUITY", + "pct": 94.5 + }, + { + "name": "SHORT-TERM INVESTMENTS", + "pct": 0.6 + }, + { + "name": "MONEY MARKET FUND", + "pct": 0.6 + } + ], + "buckets": [ + { + "name": "Equity (US)", + "value": 139099660.0, + "pct": 99.37930790437584 + }, + { + "name": "Cash", + "value": 868773.0, + "pct": 0.6206920956241612 + } + ], + "top": [ + { + "text": "Invesco QQQ Trust Series 1 (QQQ), 130,000 sh", + "value": 95980300, + "cat": "EXCHANGE-TRADED FUNDS" + }, + { + "text": "State Street SPDR S&P 500 ETF Trust (SPY), 57,000 sh", + "value": 43119360, + "cat": "EXCHANGE-TRADED FUNDS" + }, + { + "text": "First American Government Obligations Fund Class X, 3.54%, 868,773 sh", + "value": 868773, + "cat": "MONEY MARKET FUND" + } + ] +} \ No newline at end of file diff --git a/fundlab/nport_manifest.json b/fundlab/nport_manifest.json index a127af2..85f7c38 100644 --- a/fundlab/nport_manifest.json +++ b/fundlab/nport_manifest.json @@ -60,8 +60,8 @@ "filed": "2025-09-26" }, "atesx": { - "url": null, - "filed": null, - "note": "No current SOI found via EDGAR FTS (latest Anchor filings cover the Income fund only)" + "url": "https://anchor-capital.com/wp-content/uploads/2024/10/anchor-soi-5.31.26.pdf", + "filed": "2026-05-31", + "note": "Schedule of Investments (unaudited) from the adviser's website; the fund's own recent EDGAR N-PORTs cover only the Income fund. Composition: QQQ 65.2% + SPY 29.3% + MMF 0.6%; 'other assets in excess of liabilities' 4.9% indicates an options overlay." } } \ No newline at end of file diff --git a/fundlab/pool/benchmarks.txt b/fundlab/pool/benchmarks.txt index 0803aaf..03c35ad 100644 --- a/fundlab/pool/benchmarks.txt +++ b/fundlab/pool/benchmarks.txt @@ -156,3 +156,4 @@ tlt US long-term treasuries (20+ yr) ief US intermediate treasuries (7-10 yr) shv US short-term treasuries bil US t-bills / cash +qqq Nasdaq 100 diff --git a/tests/test_fundlab.py b/tests/test_fundlab.py index 5828005..dd426c6 100644 --- a/tests/test_fundlab.py +++ b/tests/test_fundlab.py @@ -239,6 +239,40 @@ def test_nport() -> None: check("parse net assets", out["net_assets"] == 50000, repr(out["net_assets"])) +def test_decompose() -> None: + print("decompose engine", flush=True) + import numpy as np + import fundlab.decompose as dc + rng = np.random.default_rng(7) + n = 1000 + x1 = rng.normal(0, 0.01, n) + x2 = rng.normal(0, 0.008, n) + x3 = rng.normal(0, 0.01, n) # no signal + y = 0.6 * x1 + 0.3 * x2 + rng.normal(0, 0.001, n) + # ols recovers betas + X = np.column_stack([np.ones(n), x1, x2]) + m = dc.ols(y, X) + check("ols beta1", abs(m["beta"][1] - 0.6) < 0.05, f"{m['beta'][1]:.3f}") + check("ols beta2", abs(m["beta"][2] - 0.3) < 0.05, f"{m['beta'][2]:.3f}") + check("ols r2 high", m["r2"] > 0.95, f"{m['r2']:.3f}") + # forward selection: picks the two signal sleeves, not the noise one + chosen, _, ok = dc.forward_select(y, {"s1": x1, "s2": x2, "s3": x3}) + check("fwd picks signal", set(chosen) == {"s1", "s2"}, str(chosen)) + # market neutral (pure noise): nothing selected + yn = rng.normal(0, 0.004, n) + chosen_n, _, _ = dc.forward_select(yn, {"s1": x1, "s2": x2}) + check("fwd rejects noise", chosen_n == [], str(chosen_n)) + # NaN handling: a candidate whose history only partly overlaps the + # fund's doesn't crash the selection, and the full-history sleeve is + # still found. (The short-history sleeve may lose on BIC because its + # complete-case sample is smaller - that's the expected, conservative + # behaviour, so we only assert robustness here.) + x2p = x2.copy() + x2p[:500] = np.nan + chosen_p, _, _ok_p = dc.forward_select(y, {"s1": x1, "s2": x2p}) + check("fwd handles nan overlap", "s1" in chosen_p, f"chosen={chosen_p}") + + def test_curated() -> None: print("curated", flush=True) import fundlab.fundinfo as fi @@ -259,6 +293,7 @@ def main() -> int: test_classify() test_strategy() test_nport() + test_decompose() test_curated() test_edgar_live() print(f"\n{PASS} passed, {FAIL} failed")