Alpha search: mine + screen the local DB for idiosyncratic alpha complements

Answer to 'find other alpha-driven funds that complement the portfolio':

- fundlab/search.py: complementarity screen - each fund's daily total
  returns vs the same 21 broad sleeve axes (BIC forward selection,
  |t|>2), full + 5y; alpha (intercept t), R2, rolling 6m alpha
  persistence, correlation vs the current qspnx/pmaix portfolio and the
  spy/agg/tlt benchmark mix. Verdict tiers: CANDIDATE (alpha +
  persistent + portfolio-corr<0.3) / semi-alpha / alpha-but-correlated /
  sleeve mix / weak.
- fundlab/dbmine.py: the actual search universe - the local stocks DB
  already holds ~100 US open-end alternatives (AQR, PIMCO, JPM,
  Principal, Calamos, GMO, Franklin K2, ...). Name-pattern miner with
  share-class family dedupe (keeps the longest-history class).
- fundlab/tickers.py + searchlist.py: external longlist resolution
  (chart-API name gate + EDGAR 497 cover tickers). Finding: the famous
  multi-strategy/macro names (Millennium, Balyasny, Two Sigma, Winton,
  Marshall Wace, Brevan Howard, AQR Event-Driven) are private/offshore
  or terminated - not US open-end accessible. Fidelity Multi-Asset
  Income (FMSDX) resolved and screens as weak alpha.
- app Fund Lab: 'Alpha search - all screened funds, ranked' table
  (80 funds: 13 shortlist + 59 mined + 1 external).
- results (ranked candidates, 5y alpha / t / portfolio-corr):
  wmnix Westwood Alt Income +3.8% t6.5 c0.09 | pyaix Payden ARB +3.0%
  t4.8 c0.13 | srdax Stone Ridge Div Alts +7.7% t4.2 c0.10 | padqx PGIM
  ARB +2.3% t2.4 c0.27 | bxmdx Blackstone Alt MS +3.5% t2.4 c0.30 |
  aqmix AQR Mngd Futures +8.0% t2.2 c0.21 | cmnix/gioix semi-alpha.
  Key insight: AQR MN / L/S-equity / Vanguard MN show strong alpha but
  corr 0.35-0.76 with the portfolio - it is already 50% market-neutral
  (qspnx), so more MN is not diversifying.
- tests: 59/59 fundlab (resolver gates, query ladder, family dedupe,
  ticker regex), 32/32 app, 14/14 data
This commit is contained in:
Greg Pomerantz 2026-08-26 13:34:34 -04:00
parent db5fc4626d
commit afec7bda73
12 changed files with 2265 additions and 1 deletions

60
app.py
View File

@ -615,6 +615,66 @@ with tab_fundlab:
}) })
st.dataframe(pd.DataFrame(_sum_rows), width="stretch") st.dataframe(pd.DataFrame(_sum_rows), width="stretch")
# --- alpha search: all screened funds (shortlist + longlist + harvest)
with st.expander("Alpha search — all screened funds, ranked"):
_all_rows: list[dict] = []
for _src in ("search_results.json", "search_mined.json"):
try:
_j = json.loads((_dc.RESULTS.parent / _src).read_text())
except Exception:
continue
for _k, _v in _j.items():
if isinstance(_v, dict) and _v.get("sym"):
_v.setdefault("source", _src)
_all_rows.append(_v)
if _all_rows:
def _tier(r) -> int:
v = str(r.get("verdict", ""))
if v.startswith("CANDIDATE"):
return 0
if "correlated" in v:
return 1
if "not persistent" in v:
return 2
if "sleeve" in v:
return 3
if "weak" in v:
return 4
return 5
_all_rows.sort(key=lambda r: (_tier(r),
-(r.get("alpha_t_5y")
if isinstance(r.get("alpha_t_5y"),
(int, float)) else -9)))
_tbl = []
for _v in _all_rows:
_a5 = _v.get("alpha_ann_5y")
_tbl.append({
"fund": f"{_v['sym'].upper()}{_v.get('name', '')[:50]}",
"bucket": _v.get("bucket", ""),
"R² 5y": (f"{_v['r2_5y']:.2f}"
if isinstance(_v.get("r2_5y"), (int, float))
else ""),
"alpha 5y": (f"{_a5*100:+.1f}% (t={_v['alpha_t_5y']:+.1f})"
if isinstance(_a5, (int, float)) else ""),
"corr port": (f"{_v['corr_portfolio']:.2f}"
if isinstance(_v.get("corr_portfolio"),
(int, float)) else ""),
"6m + %": (f"{_v['alpha_pos_frac']:.0%}"
if isinstance(_v.get("alpha_pos_frac"),
(int, float)) else ""),
"verdict": _v.get("verdict", _v.get("error", "")),
})
st.dataframe(pd.DataFrame(_tbl), width="stretch")
st.caption(
"Screen: daily total returns vs 21 broad sleeve axes (same "
"set for every fund); 'alpha 5y' = OLS intercept over the "
"last 5 years (t-stat); 'corr port' = correlation with your "
"current qspnx/pmaix portfolio; '6m + %' = share of rolling "
"6-month windows where the fund beat its fitted sleeve mix. "
"CANDIDATE = R²5y < 0.6 (or < 0.85 with strong residual "
"alpha), t5y ≥ 2, t-full ≥ 1.25, ≥ 45% positive windows, "
"portfolio correlation < 0.3.")
_f = _FUNDS.get(_fl_pick, {}) _f = _FUNDS.get(_fl_pick, {})
_man = _MAN.get(_fl_pick, {}) _man = _MAN.get(_fl_pick, {})
st.subheader(f"{_f.get('name', _fl_pick)} · {_fl_pick.upper()}") st.subheader(f"{_f.get('name', _fl_pick)} · {_fl_pick.upper()}")

128
fundlab/dbmine.py Normal file
View File

@ -0,0 +1,128 @@
"""Mine the local stocks DB for alpha-leaning open-end funds and screen
them with the same engine (fundlab.search.screen_fund).
The DB already holds ~8k symbols incl. a rich set of US open-end
alternatives (AQR, PIMCO, JPM, Principal, Calamos, GMO, Franklin K2, ...).
The miner:
1. scans every <sym>.json for alpha-leaning fund names
(market-neutral / long-short / multi-strategy / macro / managed
futures / absolute return / multi-asset TA / risk premia /
alternatives),
2. dedupes share classes (same fund, A/I/N/R6/Z/Instl...) keeping the
class with the longest history,
3. screens each survivor (requires >= 5y of data).
"""
from __future__ import annotations
import glob
import json
import re
import sys
from pathlib import Path
DATA = Path.home() / "prog/fin/stocks"
OUT = Path(__file__).parent / "search_mined.json"
PATTERN = re.compile(
r"(market neutral|long.?/?.?short|multi.?strateg|managed futures|"
r"macro opportunit|global macro|alternative (strateg|strats|risk|asset|"
r"income|core|allocation)|alternatives fund|absolute return|"
r"risk premia|multi.?asset (income|absolute|ult|balanced)|"
r"tactical allocation|trends fund|market trend|opportunistic (equity|long)|"
r"multi.?manager|diversified (alternatives|income))", re.I)
# skip anything that is really an ETF wrapper or index
SKIP = re.compile(
r"(exchange.?traded|etf trust|index fund|s&p 500|nasdaq 100|"
r"real estate trust|reit\b|grayscale|liquidation|royalty|bitcoin|"
r"litecoin|ethereum|multimanager (20|lifestyle)|core plus)", re.I)
# trailing share-class tokens for family dedupe
CLASS_TOK = re.compile(
r"\b(a|i|n|c|b|z|x|r6|r5|r4|svc|inst|instl|institutional|advisor|"
r"retail|investor|plus|class [a-z]?\d?|series [a-z]?)\b\.?$", re.I)
KNOWN = set(json.loads(
(Path(__file__).parent.parent / "funds.json").read_text()))
def _known_families() -> set[str]:
"""Family keys of the shortlist, so other share classes of the SAME
fund (qspix vs the shortlist's qspnx) are not mined as new funds."""
out = set()
for name in json.loads(
(Path(__file__).parent.parent / "funds.json").read_text()):
out.add(family_key(name))
return out
def family_key(name: str) -> str:
t = re.sub(r"[^a-z0-9 ]+", " ", name.lower())
prev = None
while prev != t:
prev = t
t = CLASS_TOK.sub(" ", t)
t = re.sub(r"\s+", " ", t).strip()
# also strip leading trust/series wrappers ("trust for professional...")
t = re.sub(r"^(trust for |investment managers series [a-z0-9 ]*-?\s*)",
"", t)
return t
KNOWN_FAMS = _known_families()
def mine() -> list[dict]:
fams: dict[str, dict] = {}
for f in glob.glob(str(DATA / "*.json")):
sym = Path(f).name[:-5].lower()
# KNOWN = the shortlist in funds.json; eigmx is the I class of the
# shortlist's eagmx (EV Global Macro) - don't double-count it
if sym in KNOWN or sym == "eigmx":
continue
try:
d = json.load(open(f))
res = d["chart"]["result"][0]
meta = res["meta"]
except Exception:
continue
itype = (meta.get("instrumentType") or "").upper()
if itype == "ETF":
continue
name = meta.get("longName") or meta.get("shortName") or ""
if not PATTERN.search(name) or SKIP.search(name):
continue
days = len(res.get("timestamp", []) or [])
if days < 1250: # need >= 5y
continue
key = family_key(name)
if len(key) < 12 or key in KNOWN_FAMS:
continue
if key not in fams or days > fams[key]["days"]:
fams[key] = {"sym": sym, "name": name, "days": days}
out = sorted(fams.values(), key=lambda x: x["name"])
return out
def run(screen: bool = True) -> dict:
fams = mine()
print(f"mined {len(fams)} distinct fund families", flush=True)
results = {}
if screen:
from fundlab import search
for c in fams:
row = search.screen_fund(c["sym"], c["name"], "mined")
results[c["sym"]] = row
print(f"{c['sym']:7} {c['name'][:44]:44} "
f"{row.get('verdict', row.get('error'))[:44]}",
flush=True)
else:
for c in fams:
print(f" {c['sym']:7} {c['days']:5}d {c['name'][:60]}")
OUT.write_text(json.dumps(results or fams, indent=1, default=str))
print(f"wrote {OUT}")
return results
if __name__ == "__main__":
run(screen="--no-screen" not in sys.argv)

View File

@ -55,7 +55,11 @@ def returns_panel(symbols: list[str], start: str | None = None) -> pd.DataFrame:
candidate histories have different vintages (and some end early, e.g. candidate histories have different vintages (and some end early, e.g.
finux stops in 2017), so a global inner join can be empty.""" finux stops in 2017), so a global inner join can be empty."""
cols = [] cols = []
for s in symbols: seen = set()
for s in symbols: # de-dup: a symbol may appear
if s in seen: # in both the benchmark mix and
continue # the sleeve set
seen.add(s)
a = adj_close(s) a = adj_close(s)
if a is not None: if a is not None:
cols.append(a) cols.append(a)

100
fundlab/harvest.py Normal file
View File

@ -0,0 +1,100 @@
"""Harvest candidate multi-asset / alternatives fund tickers from the SEC
company_tickers file, verify each resolves on Yahoo with real history,
download the survivors with goget, and screen them with the same engine.
The SEC file lists exchange symbols; Yahoo's chart API resolves many of
them to the underlying mutual fund (instrumentType + longName) AND serves
full NAV history for them (verified: PDI -> 2012..). CEFs (", Inc.",
"Trust, Inc.") are excluded - the screen targets open-end share classes.
"""
from __future__ import annotations
import json
import re
import subprocess
import urllib.request
from pathlib import Path
from fundlab import decompose, search
from fundlab.searchlist import BROAD_SLEEVES
SEC_TICKERS = Path("/tmp/company_tickers.json")
DATA = Path.home() / "prog/fin/stocks"
GOGET = Path.home() / "go/bin/goget"
OUT = Path(__file__).parent / "search_harvest.json"
FUND_KW = re.compile(r"\b(fund|funds|trust)\b", re.I)
CEF_KW = re.compile(r",\s*Inc\.|Trust,\s*Inc\.|, Inc\b|TRUST\s+[IVX]+,?\s*$",
re.I)
def harvest_candidates() -> list[dict]:
d = json.loads(SEC_TICKERS.read_text())
out = []
for v in d.values():
t, title = v.get("ticker", ""), v.get("title", "")
if not t or len(t) < 4 or not FUND_KW.search(title):
continue
if CEF_KW.search(title):
continue
out.append({"ticker": t, "title": title})
return out
def history_length(ticker: str) -> int:
"""Days of history Yahoo serves for a ticker; 0 if none, or if it's an
exchange-listed instrument (ETF/stock - we want OTC fund classes)."""
url = (f"https://query1.finance.yahoo.com/v8/finance/chart/"
f"{ticker}?range=20y&interval=1d")
req = urllib.request.Request(url, headers=search.UA)
try:
d = json.load(urllib.request.urlopen(req, timeout=30))
res = (d.get("chart") or {}).get("result")
if not res:
return 0
meta = res[0].get("meta") or {}
exch = (meta.get("fullExchangeName") or "").upper()
if any(e in exch for e in ("NASDAQ", "NYSE", "ARCA")):
return 0
return len(res[0].get("timestamp", []))
except Exception:
return 0
def run(min_days: int = 1250) -> dict:
cands = harvest_candidates()
print(f"harvested {len(cands)} fund-like SEC tickers", flush=True)
verified = []
for c in cands:
n = history_length(c["ticker"])
if n >= min_days:
verified.append({**c, "days": n})
print(f" keep {c['ticker']:6} {n:5}d {c['title'][:46]}",
flush=True)
print(f"{len(verified)} with >= {min_days}d history", flush=True)
missing = [c["ticker"].lower() for c in verified
if not (DATA / f"{c['ticker'].lower()}-history.csv").exists()]
if missing and GOGET.exists():
print(f"goget downloading {len(missing)} symbols...", flush=True)
subprocess.run([str(GOGET), *missing], cwd=DATA,
capture_output=True, timeout=1800)
results = {}
for c in verified:
sym = c["ticker"].lower()
if not (DATA / f"{sym}-history.csv").exists():
results[sym] = {"sym": sym, "title": c["title"],
"error": "no history after download"}
continue
row = search.screen_fund(sym, c["title"], "harvest")
results[sym] = row
print(f"{sym:7} {row.get('verdict', row.get('error'))[:60]}",
flush=True)
OUT.write_text(json.dumps(results, indent=1, default=str))
print(f"wrote {OUT}")
return results
if __name__ == "__main__":
run()

View File

@ -0,0 +1,128 @@
{
"Fidelity Multi-Asset Income Fund": {
"symbol": "fmsdx",
"name": "Fidelity Multi-Asset Income",
"sim": 1.0,
"guess": "FMSDX"
},
"AQR Diversified Event-Driven Fund": {
"symbol": null,
"name": "AQR Diversified Event-Driven Fund",
"error": "unresolved"
},
"Bridgewater Pure Alpha II Fund": {
"symbol": null,
"name": "Bridgewater Pure Alpha II Fund",
"error": "unresolved"
},
"Winton Global Quantitative Fund": {
"symbol": null,
"name": "Winton Global Quantitative Fund",
"error": "unresolved"
},
"Two Sigma Dynamic Strategy Fund": {
"symbol": null,
"name": "Two Sigma Dynamic Strategy Fund",
"error": "unresolved"
},
"Brevan Howard Dymon Asia Fund": {
"symbol": null,
"name": "Brevan Howard Dymon Asia Fund",
"error": "unresolved"
},
"Marshall Wace Global Opportunities Fund": {
"symbol": null,
"name": "Marshall Wace Global Opportunities Fund",
"error": "unresolved"
},
"ExodusPoint Diversified Fund": {
"symbol": null,
"name": "ExodusPoint Diversified Fund",
"error": "unresolved"
},
"Verition Dynamic Risk Fund": {
"symbol": null,
"name": "Verition Dynamic Risk Fund",
"error": "unresolved"
},
"Millennium Focus Fund": {
"symbol": null,
"name": "Millennium Focus Fund",
"error": "unresolved"
},
"Balyasny Absolute Return Multi-Strategy Fund": {
"symbol": null,
"name": "Balyasny Absolute Return Multi-Strategy Fund",
"error": "unresolved"
},
"Schonfeld Strategic Opportunities Fund": {
"symbol": null,
"name": "Schonfeld Strategic Opportunities Fund",
"error": "unresolved"
},
"Bridgewater All Weather Fund": {
"symbol": null,
"name": "Bridgewater All Weather Fund",
"error": "unresolved"
},
"Oak Hill Tactical Allocation Fund": {
"symbol": null,
"name": "Oak Hill Tactical Allocation Fund",
"error": "unresolved"
},
"Janus Henderson Global Dynamic Dividend Fund": {
"symbol": null,
"name": "Janus Henderson Global Dynamic Dividend Fund",
"error": "unresolved"
},
"Wellington Dynamic Global Diversified Fund": {
"symbol": null,
"name": "Wellington Dynamic Global Diversified Fund",
"error": "unresolved"
},
"Lord Abbett Global Opportunities Fund": {
"symbol": null,
"name": "Lord Abbett Global Opportunities Fund",
"error": "unresolved"
},
"BlackRock Multi-Asset Income Fund": {
"symbol": null,
"name": "BlackRock Multi-Asset Income Fund",
"error": "unresolved"
},
"PIMCO Income Strategy Fund": {
"symbol": null,
"name": "PIMCO Income Strategy Fund",
"error": "unresolved"
},
"JPMorgan Diversified Return Fund": {
"symbol": null,
"name": "JPMorgan Diversified Return Fund",
"error": "unresolved"
},
"Invesco Diversified Equity and Income Fund": {
"symbol": null,
"name": "Invesco Diversified Equity and Income Fund",
"error": "unresolved"
},
"T. Rowe Price Global Allocation Fund": {
"symbol": null,
"name": "T. Rowe Price Global Allocation Fund",
"error": "unresolved"
},
"Morgan Stanley Global Multi Asset Fund": {
"symbol": null,
"name": "Morgan Stanley Global Multi Asset Fund",
"error": "unresolved"
},
"Fidelity Diversified Multi-Asset Fund": {
"symbol": null,
"name": "Fidelity Diversified Multi-Asset Fund",
"error": "unresolved"
},
"PIMCO Dynamic Income Fund": {
"symbol": null,
"name": "PIMCO Dynamic Income Fund",
"error": "unresolved"
}
}

326
fundlab/search.py Normal file
View File

@ -0,0 +1,326 @@
"""Alpha search: screen the curated longlist (+ the 13-fund shortlist)
for idiosyncratic alpha-driven funds that complement the current
portfolio (qspnx/pmaix 50/50; benchmarks spy/agg/tlt).
Method (same engine as fundlab/decompose.py, broad candidate set):
1. resolve tickers from fund NAMES via Yahoo search (precision-gated;
unresolvable candidates are dropped, never guessed),
2. download missing returns with the goget tool (if the user has
authorized it and the binary is present),
3. regress each fund's daily total returns on the same 21 broad sleeve
axes (BIC forward selection, |t|>2), full history + last 5y,
4. compute correlation vs the current portfolio and vs the benchmark
mix, and a rolling 6m alpha persistence check,
5. rank: CANDIDATE = low R^2 (not a sleeve mix) + significant alpha
(5y t>=2, full t>=1.5) + persistent (>=50% of rolling windows
positive) + low portfolio correlation (<0.3).
Output: fundlab/search_results.json (read by the app).
"""
from __future__ import annotations
import difflib
import json
import re
import subprocess
import urllib.parse
import urllib.request
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
from fundlab import decompose
from fundlab.searchlist import (BENCHMARKS, BROAD_SLEEVES, LONGLIST,
PORTFOLIO, SHORTLIST)
DATA = Path.home() / "prog/fin/stocks"
GOGET = Path.home() / "go/bin/goget"
RESULTS = Path(__file__).parent / "search_results.json"
RESOLVED = Path(__file__).parent / "resolved_tickers.json"
def _load_resolved() -> dict:
try:
return json.loads(RESOLVED.read_text())
except Exception:
return {}
def _save_resolved(d: dict) -> None:
RESOLVED.write_text(json.dumps(d, indent=1))
UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"}
# ------------------------------------------------------------------ resolve
def _norm_name(s: str) -> str:
s = s.lower()
s = re.sub(r"[^a-z0-9]+", " ", s)
# strip share-class suffixes
for suf in ("advisor", "institutional", "retail", "plus", "a", "i", "c",
"b", "x", "z"):
s = re.sub(rf"\b{re.escape(suf)}\b\s*$", " ", s)
s = re.sub(r"\bfund\b\s*$", "", s)
return re.sub(r"\s+", " ", s).strip()
def chart_meta(sym: str) -> dict | None:
"""Yahoo chart API (no crumb needed): returns meta for a symbol."""
url = ("https://query1.finance.yahoo.com/v8/finance/chart/"
f"{sym.upper()}?range=1y&interval=1d")
req = urllib.request.Request(url, headers=UA)
try:
d = json.load(urllib.request.urlopen(req, timeout=30))
except Exception:
return None
res = (d.get("chart") or {}).get("result")
return res[0].get("meta") if res else None
_STOP = {"the", "and", "for", "of", "to", "a", "i", "c", "fund", "funds",
"series", "class", "in", "on"}
def _tokens(s: str) -> set[str]:
return {t for t in _norm_name(s).split() if t not in _STOP}
def _name_match(target: str, cand: str) -> float:
"""Fraction of the target's significant tokens present in cand."""
tt, ct = _tokens(target), _tokens(cand)
return len(tt & ct) / len(tt) if tt else 0.0
def resolve(name: str, guess: str | None = None) -> dict | None:
"""Resolve a fund name to a ticker, verifying against Yahoo chart
metadata (instrumentType + longName). A guess is accepted only if the
chart says it's a mutual fund whose name shares >= 2/3 of the
target's significant tokens AND has sim >= 0.5; otherwise the
candidate is dropped - never guessed.
Returns {symbol, name, sim, guess} or {symbol: None, error}.
"""
if not guess:
return {"symbol": None, "name": name,
"error": "no ticker guess (add one or resolve manually)"}
meta = chart_meta(guess)
if not meta:
return {"symbol": None, "name": name,
"error": f"no chart data for guess {guess}"}
itype = (meta.get("instrumentType") or "").upper()
if itype not in ("MUTUALFUND", "FUND"):
return {"symbol": None, "name": name,
"error": f"{guess} is not a mutual fund ({itype})"}
cand = meta.get("longName") or meta.get("shortName") or ""
sim = difflib.SequenceMatcher(None, _norm_name(name),
_norm_name(cand)).ratio()
tok = _name_match(name, cand)
if tok < 2 / 3 or sim < 0.5:
return {"symbol": None, "name": name,
"error": f"{guess} is {cand!r} (tok={tok:.2f} sim={sim:.2f})"}
return {"symbol": guess.lower(), "name": cand, "sim": round(sim, 3),
"guess": guess}
# ------------------------------------------------------------------ data
@lru_cache(maxsize=None)
def has_data(sym: str) -> bool:
return (DATA / f"{sym}-history.csv").exists()
def ensure_data(symbols: list[str]) -> list[str]:
"""goget-download the missing symbols (into the data dir). Returns the
symbols still without data."""
missing = [s for s in symbols if not has_data(s)]
if missing and GOGET.exists():
try:
subprocess.run([str(GOGET), *missing], cwd=DATA,
capture_output=True, timeout=900)
except Exception as e:
print(f"goget failed: {e}", flush=True)
has_data.cache_clear()
return [s for s in symbols if not has_data(s)]
# ------------------------------------------------------------------ screen
def _mix(returns: pd.DataFrame, weights: dict[str, float]) -> pd.Series:
out = None
for s, w in weights.items():
if s in returns.columns:
v = returns[s] * w
out = v if out is None else out.add(v, fill_value=0.0)
return out
def _max_drawdown(r: pd.Series) -> float:
eq = (1 + r.fillna(0)).cumprod()
return float((eq / eq.cummax() - 1).min())
def screen_fund(sym: str, name: str, bucket: str,
in_portfolio: bool = False) -> dict:
full = decompose.decompose(sym, candidates={sym: BROAD_SLEEVES})
if "r2" not in full:
return {"sym": sym, "name": name, "bucket": bucket,
"error": full.get("error", "no data")}
rec = decompose.decompose(sym, start=decompose.RECENT_WINDOW,
candidates={sym: BROAD_SLEEVES})
r = decompose.returns_panel(
[sym] + list(PORTFOLIO) + list(BENCHMARKS) + BROAD_SLEEVES)
corr_port = corr_bench = np.nan
if sym in r.columns:
syms = list(dict.fromkeys(
[sym] + list(PORTFOLIO) + list(BENCHMARKS))) # dedup: the
c = r[[s for s in syms if s in r.columns]].dropna() # screened fund may BE a portfolio component
if len(c) > 252:
pf = _mix(c, PORTFOLIO)
bm = _mix(c, BENCHMARKS)
if pf is not None:
corr_port = float(np.corrcoef(c[sym], pf)[0, 1])
if bm is not None:
corr_bench = float(np.corrcoef(c[sym], bm)[0, 1])
# rolling 6m alpha persistence: fraction of 126d windows where the
# fund beat its fitted sleeve mix. Use the 5y model when the full-
# sample model is empty (vintage funds), else the full model.
frac_pos = np.nan
_model = rec if ("components" in rec and rec["components"]) else full
if _model.get("components"):
y = r[sym].to_numpy()
X = np.column_stack(
[np.ones(len(y)), *[r[c["sym"]].to_numpy()
for c in _model["components"]]])
ok = ~(np.isnan(y) | np.isnan(X).any(axis=1))
y, X = y[ok], X[ok]
if len(y) >= 252:
beta, *_ = np.linalg.lstsq(X, y, rcond=None)
ex = y - X @ beta
w = 126
if len(ex) > w * 3:
fr = [ex[i - w:i].mean() > 0
for i in range(w, len(ex), 21)]
frac_pos = float(np.mean(fr))
f5 = rec.get("r2", np.nan)
t5 = rec.get("alpha_t", np.nan)
tf = full.get("alpha_t", np.nan)
# kind: alpha (mostly idiosyncratic), semi_alpha (mostly explained by
# net exposure but with significant residual alpha - typical of
# market-neutral funds), sleeve (a static mix), weak (neither)
if f5 < 0.6 and t5 >= 2.0 and tf >= 1.25:
kind = "alpha"
elif f5 < 0.85 and t5 > 0 and tf >= 3.0:
kind = "semi_alpha" # positive 5y alpha required for candidacy
elif f5 >= 0.85:
kind = "sleeve"
else:
kind = "weak"
persistent = frac_pos is not None and frac_pos >= 0.45
complementary = corr_port == corr_port and corr_port < 0.3
if "r2" not in rec:
verdict = "no 5y window"
elif kind in ("alpha", "semi_alpha") and persistent and complementary:
verdict = ("CANDIDATE - idiosyncratic alpha, complements portfolio"
if kind == "alpha" else
"CANDIDATE (semi-alpha: mostly explained by net exposure)")
elif kind in ("alpha", "semi_alpha") and not complementary:
verdict = "alpha, but correlated with current portfolio"
elif kind in ("alpha", "semi_alpha") and not persistent:
verdict = "alpha in 5y window, but not persistent (lucky stretch?)"
elif kind == "sleeve":
verdict = "sleeve mix (R² high) - not alpha-driven"
else:
verdict = "weak/unstable alpha"
return {
"sym": sym, "name": name, "bucket": bucket,
"in_portfolio": in_portfolio,
"alpha_ann_5y": rec.get("alpha_ann"),
"alpha_t_5y": t5, "alpha_t_full": tf,
"r2_5y": f5, "r2_full": full.get("r2"),
"corr_portfolio": corr_port, "corr_benchmark": corr_bench,
"alpha_pos_frac": frac_pos,
"fund_max_dd": _max_drawdown(r[sym]) if sym in r.columns else None,
"first": full.get("start"),
"verdict": verdict,
}
def _shortlist_names() -> dict[str, str]:
try:
f = json.loads((Path(__file__).parent.parent / "funds.json").read_text())
return {k: v["name"] for k, v in f.items()}
except Exception:
return {}
def run(shortlist_only: bool = False) -> dict:
rows: dict[str, dict] = {}
names = _shortlist_names()
# 1) the shortlist (known tickers, names from funds.json)
for sym in SHORTLIST:
in_pf = sym in PORTFOLIO
row = screen_fund(sym, names.get(sym, sym), "shortlist",
in_portfolio=in_pf)
rows[sym] = row
print(f"{sym:7} {row.get('verdict', row.get('error'))[:70]}",
flush=True)
if shortlist_only:
RESULTS.write_text(json.dumps(rows, indent=1, default=str))
return rows
# 2) the curated longlist: chart-verified guess first, then EDGAR
# prospectus covers, else drop (never guess)
from fundlab import tickers as _tickers
resolved_cache = _load_resolved()
for name, bucket, guess in LONGLIST:
cached = resolved_cache.get(name)
if cached is not None:
res = cached
if res.get("symbol"):
print(f"CACHED {name[:40]:40} -> {res['symbol'].upper()}",
flush=True)
else:
res = resolve(name, guess)
if not res.get("symbol"): # guess missing/rejected -> EDGAR
res = _tickers.resolve_via_edgar(name)
if res:
print(f"EDGAR {name[:40]:40} -> {res['symbol'].upper()} "
f"(tok={res['sim']}) {res['name'][:44]}", flush=True)
resolved_cache[name] = res or {"symbol": None, "name": name,
"error": "unresolved"}
_save_resolved(resolved_cache)
if res is None or not res.get("symbol"):
print(f"DROP {name[:45]:45} {res.get('error', '') if res else ''}")
rows[f"__drop__{name[:30]}"] = {
"sym": None, "name": name, "bucket": bucket,
"error": (res or {}).get("error", "unresolved"),
"guess": guess}
continue
sym = res["symbol"]
if sym in rows:
print(f"SKIP {name[:45]:45} already in shortlist as {sym}")
continue
print(f"RESOLVED {name[:40]:40} -> {sym.upper()} "
f"(sim={res['sim']})", flush=True)
rows[sym] = {"_resolve": res} # placeholder for the download pass
# 3) download whatever is missing, then screen
to_screen = {s: d for s, d in rows.items() if "_resolve" in d}
if to_screen:
still_missing = ensure_data(list(to_screen))
for s in still_missing:
rows[s]["error"] = "no return data (download failed)"
to_screen = {s: d for s, d in to_screen.items() if "error" not in d}
for sym, d in to_screen.items():
res = d["_resolve"]
row = screen_fund(sym, res["name"], bucket)
row["resolved_from"] = res
rows[sym] = row
print(f"{sym:7} {row.get('verdict', row.get('error'))[:70]}",
flush=True)
RESULTS.write_text(json.dumps(rows, indent=1, default=str))
n_cand = sum(1 for r in rows.values()
if str(r.get("verdict", "")).startswith("CANDIDATE"))
print(f"\nwrote {RESULTS} - {n_cand} candidate(s)")
return rows
if __name__ == "__main__":
import sys
run(shortlist_only="--shortlist" in sys.argv)

223
fundlab/search_harvest.json Normal file
View File

@ -0,0 +1,223 @@
{
"bxsy": {
"sym": "bxsy",
"name": "BEXIL INVESTMENT TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.06927005667397715,
"alpha_t_5y": 1.2149541754228392,
"alpha_t_full": 1.8229873570027744,
"r2_5y": 0.4618348730312034,
"r2_full": -2.220446049250313e-16,
"corr_portfolio": 0.33892135334668516,
"corr_benchmark": 0.45602489423127307,
"alpha_pos_frac": 0.515695067264574,
"fund_max_dd": -0.7433132163156555,
"first": "1998-06-25",
"verdict": "weak/unstable alpha"
},
"wbqnl": {
"sym": "wbqnl",
"name": "Woodbridge Liquidation Trust",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.9961091932730224,
"alpha_t_5y": 1.1101705514920563,
"alpha_t_full": 1.0226428696167786,
"r2_5y": 3.3306690738754696e-16,
"r2_full": 9.992007221626409e-16,
"corr_portfolio": -0.0035199119784513296,
"corr_benchmark": -0.03393673842772135,
"alpha_pos_frac": NaN,
"fund_max_dd": -0.9399999571200082,
"first": "2020-05-11",
"verdict": "weak/unstable alpha"
},
"chkr": {
"sym": "chkr",
"name": "CHESAPEAKE GRANITE WASH TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.5212096221629036,
"alpha_t_5y": 1.9305694961835806,
"alpha_t_full": 1.9305694961835806,
"r2_5y": 2.220446049250313e-16,
"r2_full": 2.220446049250313e-16,
"corr_portfolio": 0.07574482096405284,
"corr_benchmark": 0.05001005398172679,
"alpha_pos_frac": NaN,
"fund_max_dd": -0.6324567415625832,
"first": "2021-01-11",
"verdict": "weak/unstable alpha"
},
"gultu": {
"sym": "gultu",
"name": "Gulf Coast Ultra Deep Royalty Trust",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 1.0634379242764702,
"alpha_t_5y": 1.94477678998177,
"alpha_t_full": 1.382409605533132,
"r2_5y": -4.440892098500626e-16,
"r2_full": 1.4432899320127035e-15,
"corr_portfolio": 0.04066632940436689,
"corr_benchmark": 0.040088555722086666,
"alpha_pos_frac": NaN,
"fund_max_dd": -0.997829688469555,
"first": "2013-06-05",
"verdict": "weak/unstable alpha"
},
"mmtrs": {
"sym": "mmtrs",
"name": "MILLS MUSIC TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.07762726973809353,
"alpha_t_5y": 0.42528679541423187,
"alpha_t_full": 1.6768081213200938,
"r2_5y": 6.661338147750939e-16,
"r2_full": 0.031157054651066884,
"corr_portfolio": -0.050398683241705884,
"corr_benchmark": -0.044939596922708436,
"alpha_pos_frac": 0.5163043478260869,
"fund_max_dd": -0.605949540821771,
"first": "2010-10-18",
"verdict": "weak/unstable alpha"
},
"hgtxu": {
"sym": "hgtxu",
"name": "HUGOTON ROYALTY TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.7307734743494634,
"alpha_t_5y": 1.2232580470623284,
"alpha_t_full": 1.170973341611815,
"r2_5y": 0.02610774179213693,
"r2_full": 0.003643640247751545,
"corr_portfolio": 0.034422175407928385,
"corr_benchmark": 0.009653641140543325,
"alpha_pos_frac": 0.41379310344827586,
"fund_max_dd": -0.997779953094849,
"first": "1999-04-12",
"verdict": "weak/unstable alpha"
},
"ltcn": {
"sym": "ltcn",
"name": "Grayscale Litecoin Trust (LTC)",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": -0.4170605500709843,
"alpha_t_5y": -0.910466285049782,
"alpha_t_full": 0.44872825005523104,
"r2_5y": 0.09926912946027,
"r2_full": 0.06380014823207847,
"corr_portfolio": 0.006207970423322662,
"corr_benchmark": 0.1679271530301851,
"alpha_pos_frac": 0.3333333333333333,
"fund_max_dd": -0.9958,
"first": "2020-08-19",
"verdict": "weak/unstable alpha"
},
"etcg": {
"sym": "etcg",
"name": "Grayscale Ethereum Classic Trust (ETC)",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.10920362929835496,
"alpha_t_5y": 0.26335909961725734,
"alpha_t_full": 0.2598552018670923,
"r2_5y": 0.1222132658141114,
"r2_full": 0.09099648179825504,
"corr_portfolio": 0.08516993583383066,
"corr_benchmark": 0.1947569069467775,
"alpha_pos_frac": 0.40425531914893614,
"fund_max_dd": -0.9658798207673674,
"first": "2018-05-11",
"verdict": "weak/unstable alpha"
},
"bchg": {
"sym": "bchg",
"name": "Grayscale Bitcoin Cash Trust (BCH)",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": -1.0370219629357933,
"alpha_t_5y": -1.8073793086518852,
"alpha_t_full": 0.3919639833205906,
"r2_5y": 0.09353824409219647,
"r2_full": 0.07438637030766404,
"corr_portfolio": 0.012668117748772984,
"corr_benchmark": 0.14827766691441965,
"alpha_pos_frac": 0.3484848484848485,
"fund_max_dd": -0.9936056837230139,
"first": "2020-08-19",
"verdict": "weak/unstable alpha"
},
"vnorp": {
"sym": "vnorp",
"name": "VORNADO REALTY TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.6920962869579677,
"alpha_t_5y": 1.2244489160916494,
"alpha_t_full": 1.0787712165659415,
"r2_5y": -4.440892098500626e-16,
"r2_full": 6.661338147750939e-16,
"corr_portfolio": 0.02669554078119929,
"corr_benchmark": 0.0365901046059585,
"alpha_pos_frac": NaN,
"fund_max_dd": -0.7554747010633259,
"first": "2017-07-11",
"verdict": "weak/unstable alpha"
},
"grtuf": {
"sym": "grtuf",
"name": "GRANITE REAL ESTATE INVESTMENT TRUST",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.01169284649739261,
"alpha_t_5y": 0.09570009539903869,
"alpha_t_full": 0.6503721376518062,
"r2_5y": 0.1997409551483339,
"r2_full": 0.2222961586750618,
"corr_portfolio": 0.1777157657393587,
"corr_benchmark": 0.3142402348163954,
"alpha_pos_frac": 0.5126582278481012,
"fund_max_dd": -0.49333112124169476,
"first": "2013-01-07",
"verdict": "weak/unstable alpha"
},
"hctpf": {
"sym": "hctpf",
"name": "Hutchison Port Holdings Trust/ADR",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.38841147995406544,
"alpha_t_5y": 1.187463388113121,
"alpha_t_full": 0.6569127338633608,
"r2_5y": -2.220446049250313e-16,
"r2_full": 0.0037065997019110064,
"corr_portfolio": 0.05797833071874448,
"corr_benchmark": 0.0313773507142334,
"alpha_pos_frac": 0.37349397590361444,
"fund_max_dd": -0.85155740750894,
"first": "2012-04-19",
"verdict": "weak/unstable alpha"
},
"ismcf": {
"sym": "ismcf",
"name": "iShares S&P GSCI Commodity-Indexed Trust",
"bucket": "harvest",
"in_portfolio": false,
"alpha_ann_5y": 0.13726732445593715,
"alpha_t_5y": 1.2547956518417025,
"alpha_t_full": 0.31953542544447894,
"r2_5y": 0.5752024336058434,
"r2_full": 0.7685431108511669,
"corr_portfolio": 0.2648024508467313,
"corr_benchmark": 0.645255483182384,
"alpha_pos_frac": 0.3333333333333333,
"fund_max_dd": -0.2518381238626939,
"first": "2018-09-14",
"verdict": "weak/unstable alpha"
}
}

862
fundlab/search_mined.json Normal file
View File

@ -0,0 +1,862 @@
{
"qrprx": {
"sym": "qrprx",
"name": "AQR Alternative Risk Premia R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.1600539320146026,
"alpha_t_5y": 3.746900415536075,
"alpha_t_full": 2.519423053570873,
"r2_5y": 0.20826455753890027,
"r2_full": 0.12135409106563044,
"corr_portfolio": 0.7631471591786326,
"corr_benchmark": -0.12262262208507653,
"alpha_pos_frac": 0.5148514851485149,
"fund_max_dd": -0.317289520568956,
"first": "2017-09-20",
"verdict": "alpha, but correlated with current portfolio"
},
"qmnnx": {
"sym": "qmnnx",
"name": "AQR Equity Market Neutral N",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.15278577055161027,
"alpha_t_5y": 4.438381081698417,
"alpha_t_full": 3.55541908381974,
"r2_5y": 0.26967439068510246,
"r2_full": 0.0757410367367195,
"corr_portfolio": 0.607275193429323,
"corr_benchmark": -0.13627368396966566,
"alpha_pos_frac": 0.5474452554744526,
"fund_max_dd": -0.39217588487587307,
"first": "2014-10-10",
"verdict": "alpha, but correlated with current portfolio"
},
"qleix": {
"sym": "qleix",
"name": "AQR Long-Short Equity I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.16578977304134326,
"alpha_t_5y": 4.4015935945881,
"alpha_t_full": 4.011544078674546,
"r2_5y": 0.25840152025440843,
"r2_full": 0.44615723686486575,
"corr_portfolio": 0.7071423062698972,
"corr_benchmark": 0.2670155502700607,
"alpha_pos_frac": 0.5695364238410596,
"fund_max_dd": -0.391976987856436,
"first": "2013-07-17",
"verdict": "alpha, but correlated with current portfolio"
},
"qgmrx": {
"sym": "qgmrx",
"name": "AQR Macro Opportunities R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.057980519614889287,
"alpha_t_5y": 1.5977947998407327,
"alpha_t_full": 2.435129166036336,
"r2_5y": 0.22291108011334915,
"r2_full": 0.08727126384018968,
"corr_portfolio": 0.36214588351890326,
"corr_benchmark": -0.1756799669770561,
"alpha_pos_frac": 0.5,
"fund_max_dd": -0.13533835978044884,
"first": "2014-09-04",
"verdict": "weak/unstable alpha"
},
"qmhrx": {
"sym": "qmhrx",
"name": "AQR Managed Futures Strategy HV R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.09824897354855751,
"alpha_t_5y": 1.8083554349675617,
"alpha_t_full": 2.1668606129500954,
"r2_5y": 0.35128536900753526,
"r2_full": 0.16284423243263402,
"corr_portfolio": 0.1902974299432177,
"corr_benchmark": -0.18975892567115601,
"alpha_pos_frac": 0.42028985507246375,
"fund_max_dd": -0.39058979113920433,
"first": "2014-09-04",
"verdict": "weak/unstable alpha"
},
"aqmix": {
"sym": "aqmix",
"name": "AQR Managed Futures Strategy I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.08005605082170343,
"alpha_t_5y": 2.1784878724553636,
"alpha_t_full": 2.492282530709043,
"r2_5y": 0.34683324124502435,
"r2_full": 0.11642882514098007,
"corr_portfolio": 0.20517533934397383,
"corr_benchmark": -0.1757448117047195,
"alpha_pos_frac": 0.4948453608247423,
"fund_max_dd": -0.2654230054569442,
"first": "2010-01-06",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"warrx": {
"sym": "warrx",
"name": "Allspring Absolute Return R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.033144306024387275,
"alpha_t_5y": 1.3146300225087615,
"alpha_t_full": 0.6450360204937103,
"r2_5y": 0.4096067761908916,
"r2_full": 0.3925574402466193,
"corr_portfolio": 0.3888931145600147,
"corr_benchmark": 0.33189487333668094,
"alpha_pos_frac": 0.5703703703703704,
"fund_max_dd": -0.23090428701476684,
"first": "2014-12-01",
"verdict": "weak/unstable alpha"
},
"eksrx": {
"sym": "eksrx",
"name": "Allspring Diversified Income Bldr R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.007912370994162737,
"alpha_t_5y": 0.7248836188102821,
"alpha_t_full": 0.11380161203748929,
"r2_5y": 0.8712354981772631,
"r2_full": 0.8666793384341597,
"corr_portfolio": 0.2909267060359785,
"corr_benchmark": 0.6401931480941127,
"alpha_pos_frac": 0.4945054945054945,
"fund_max_dd": -0.2257163497358341,
"first": "2018-08-06",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"bkmix": {
"sym": "bkmix",
"name": "BlackRock Multi-Asset Income Portfolio K",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.0013661837406016611,
"alpha_t_5y": 0.1717813919596725,
"alpha_t_full": 0.4050232448112397,
"r2_5y": 0.9094131536032556,
"r2_full": 0.8936803659916046,
"corr_portfolio": 0.3139285671999697,
"corr_benchmark": 0.7165194921939918,
"alpha_pos_frac": 0.5321100917431193,
"fund_max_dd": -0.1973451734470697,
"first": "2017-02-09",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"bxmdx": {
"sym": "bxmdx",
"name": "Blackstone Alternative Multi-Strategy D",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.03491646193195598,
"alpha_t_5y": 2.3968211702431157,
"alpha_t_full": 1.7792956578721164,
"r2_5y": 0.28636888773620184,
"r2_full": 0.398432871732595,
"corr_portfolio": 0.29622985929330004,
"corr_benchmark": 0.3250033230998725,
"alpha_pos_frac": 0.4888888888888889,
"fund_max_dd": -0.19319231161826877,
"first": "2014-11-20",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"burfx": {
"sym": "burfx",
"name": "Burnham Financial Long/Short A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": null,
"alpha_t_5y": NaN,
"alpha_t_full": 0.9942402027129364,
"r2_5y": NaN,
"r2_full": 0.7475045831670635,
"corr_portfolio": 0.2677504925335687,
"corr_benchmark": 0.18598003223530538,
"alpha_pos_frac": 0.4142857142857143,
"fund_max_dd": -0.3836435558268434,
"first": "2004-05-05",
"verdict": "no 5y window"
},
"cmnix": {
"sym": "cmnix",
"name": "Calamos Market Neutral Income I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.020194291397578824,
"alpha_t_5y": 2.797664087264935,
"alpha_t_full": 4.270226253902055,
"r2_5y": 0.7405097352200456,
"r2_full": 0.6092636885705476,
"corr_portfolio": 0.2885184137546138,
"corr_benchmark": 0.5236308979040174,
"alpha_pos_frac": 0.47766323024054985,
"fund_max_dd": -0.20593822768920278,
"first": "2000-05-30",
"verdict": "CANDIDATE (semi-alpha: mostly explained by net exposure)"
},
"cplsx": {
"sym": "cplsx",
"name": "Calamos Phineus Long/Short A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.00015958554490860363,
"alpha_t_5y": -0.004435247962308447,
"alpha_t_full": 1.200988338503044,
"r2_5y": 0.541363993913625,
"r2_full": 0.6192563617744269,
"corr_portfolio": 0.28162855590905017,
"corr_benchmark": 0.3604673541731384,
"alpha_pos_frac": 0.46218487394957986,
"fund_max_dd": -0.34053557415202185,
"first": "2016-04-06",
"verdict": "weak/unstable alpha"
},
"cltix": {
"sym": "cltix",
"name": "Catalyst Tactical Allocation Fund I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.04456170305768831,
"alpha_t_5y": -1.26755312021696,
"alpha_t_full": -0.7564123705589307,
"r2_5y": 0.6852411466890216,
"r2_full": 0.6638756866683736,
"corr_portfolio": 0.23847580532582827,
"corr_benchmark": 0.5381142781143685,
"alpha_pos_frac": 0.48226950354609927,
"fund_max_dd": -0.28630516786071747,
"first": "2014-06-09",
"verdict": "weak/unstable alpha"
},
"taltx": {
"sym": "taltx",
"name": "Consulting Group Capital Markets Funds - Alternative Strategy Fund",
"bucket": "mined",
"error": "no return history in the data set"
},
"cmalx": {
"sym": "cmalx",
"name": "Crawford Multi-Asset Income",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.016394380923237965,
"alpha_t_5y": 1.0161202707404007,
"alpha_t_full": 0.3817633032313494,
"r2_5y": 0.8191174808757729,
"r2_full": 0.7736058170932395,
"corr_portfolio": 0.3827550564186549,
"corr_benchmark": 0.544426034703509,
"alpha_pos_frac": 0.6039603960396039,
"fund_max_dd": -0.3903768839466234,
"first": "2017-09-13",
"verdict": "weak/unstable alpha"
},
"dmsfx": {
"sym": "dmsfx",
"name": "Destinations Multi Strategy Alts I",
"bucket": "mined",
"error": "no return history in the data set"
},
"diayx": {
"sym": "diayx",
"name": "Diamond Hill Long-Short Y",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.001981324982722584,
"alpha_t_5y": -0.09335373100975897,
"alpha_t_full": 0.4520617676334624,
"r2_5y": 0.7791683109831256,
"r2_full": 0.8456521452529505,
"corr_portfolio": 0.3738215361306875,
"corr_benchmark": 0.46331563031570455,
"alpha_pos_frac": 0.5470588235294118,
"fund_max_dd": -0.31517374676226695,
"first": "2012-01-03",
"verdict": "weak/unstable alpha"
},
"fsmmx": {
"sym": "fsmmx",
"name": "FS Multi-Strategy Alternatives A",
"bucket": "mined",
"error": "no return history in the data set"
},
"fiwbx": {
"sym": "fiwbx",
"name": "Fidelity Advisor Multi-Asset Income Z",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.020727506850474794,
"alpha_t_5y": 1.1281227395081184,
"alpha_t_full": 1.425120797719713,
"r2_5y": 0.8133897393682811,
"r2_full": 0.8557754005261389,
"corr_portfolio": 0.24474961361413924,
"corr_benchmark": 0.7136768077831611,
"alpha_pos_frac": 0.4044943820224719,
"fund_max_dd": -0.21636594184198255,
"first": "2018-10-05",
"verdict": "weak/unstable alpha"
},
"fmsdx": {
"sym": "fmsdx",
"name": "Fidelity Multi-Asset Income",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.020065620099702027,
"alpha_t_5y": 1.0942433920841528,
"alpha_t_full": 1.37561098737718,
"r2_5y": 0.8138427119012845,
"r2_full": 0.8528139741095889,
"corr_portfolio": 0.2487814171872356,
"corr_benchmark": 0.7139377060887578,
"alpha_pos_frac": 0.4270833333333333,
"fund_max_dd": -0.21636659636345534,
"first": "2018-02-27",
"verdict": "weak/unstable alpha"
},
"ftmax": {
"sym": "ftmax",
"name": "First Trust Multi-Strategy Cl A",
"bucket": "mined",
"error": "no return history in the data set"
},
"faaax": {
"sym": "faaax",
"name": "Franklin Alternative Strategies A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.01309203663717352,
"alpha_t_5y": 1.2283383720658967,
"alpha_t_full": 1.6275872976359376,
"r2_5y": 0.607887440695306,
"r2_full": 0.6553175930301992,
"corr_portfolio": 0.28793504560310224,
"corr_benchmark": 0.47738642110979973,
"alpha_pos_frac": 0.46938775510204084,
"fund_max_dd": -0.1117951457283084,
"first": "2013-11-21",
"verdict": "weak/unstable alpha"
},
"gaagx": {
"sym": "gaagx",
"name": "GMO Alternative Allocation I",
"bucket": "mined",
"error": "no return history in the data set"
},
"gmamx": {
"sym": "gmamx",
"name": "Goldman Sachs Multi-Strategy Alternatives Fund",
"bucket": "mined",
"error": "no return history in the data set"
},
"gpaix": {
"sym": "gpaix",
"name": "Grant Park Multi Alternative Strats I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.008035877843127431,
"alpha_t_5y": 0.3735357407955509,
"alpha_t_full": 0.9846312406722558,
"r2_5y": 0.3870196716719805,
"r2_full": 0.30386830162653355,
"corr_portfolio": 0.20324552382593455,
"corr_benchmark": 0.4141139479733764,
"alpha_pos_frac": 0.4657534246575342,
"fund_max_dd": -0.17161194715948835,
"first": "2014-01-06",
"verdict": "weak/unstable alpha"
},
"gioix": {
"sym": "gioix",
"name": "Guggenheim Macro Opportunities Instl",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.015410219407521897,
"alpha_t_5y": 2.3610357649028573,
"alpha_t_full": 4.416604738332403,
"r2_5y": 0.763725975931929,
"r2_full": 0.47854017954161043,
"corr_portfolio": 0.16595563271383124,
"corr_benchmark": 0.44600909254459775,
"alpha_pos_frac": 0.49707602339181284,
"fund_max_dd": -0.122231668089825,
"first": "2011-12-01",
"verdict": "CANDIDATE (semi-alpha: mostly explained by net exposure)"
},
"gfsyx": {
"sym": "gfsyx",
"name": "GuideStone Funds - Strategic Alternatives Fund",
"bucket": "mined",
"error": "no return history in the data set"
},
"piffx": {
"sym": "piffx",
"name": "Invesco Multi-Asset Income R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.0033625121745873713,
"alpha_t_5y": -0.31735541545172474,
"alpha_t_full": -1.5456145262241747,
"r2_5y": 0.8662808420139416,
"r2_full": 0.7444930152458893,
"corr_portfolio": 0.2944534009486804,
"corr_benchmark": 0.7239162969462484,
"alpha_pos_frac": 0.6211180124223602,
"fund_max_dd": -0.3038145640820791,
"first": "2012-09-25",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"qvopx": {
"sym": "qvopx",
"name": "Invesco Multi-Strategy Fund A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.013307866825721153,
"alpha_t_5y": -0.8684139396624104,
"alpha_t_full": 3.7481298369307288,
"r2_5y": 0.3261525760675942,
"r2_full": -2.220446049250313e-16,
"corr_portfolio": 0.2812182137557509,
"corr_benchmark": 0.4186621735141323,
"alpha_pos_frac": 0.5067264573991032,
"fund_max_dd": -0.30552532857851666,
"first": "1990-01-03",
"verdict": "weak/unstable alpha"
},
"jaaax": {
"sym": "jaaax",
"name": "JHancock Alternative Asset Allc A",
"bucket": "mined",
"error": "no return history in the data set"
},
"jhaax": {
"sym": "jhaax",
"name": "JHancock Multi-Asset Absolute Return A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.01657599676490538,
"alpha_t_5y": 0.9102288544471362,
"alpha_t_full": -1.2999375934531863,
"r2_5y": 0.6368496214350496,
"r2_full": 0.5857621323143583,
"corr_portfolio": 0.18627034104514065,
"corr_benchmark": 0.5740329776144575,
"alpha_pos_frac": 0.5294117647058824,
"fund_max_dd": -0.1086538355925708,
"first": "2011-12-21",
"verdict": "weak/unstable alpha"
},
"lotix": {
"sym": "lotix",
"name": "LoCorr Market Trend I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.0007375529286507751,
"alpha_t_5y": 0.015717595792688714,
"alpha_t_full": 0.9954645022981967,
"r2_5y": 0.31753350966115257,
"r2_full": 0.1506937744585053,
"corr_portfolio": 0.24657982458097683,
"corr_benchmark": 0.06954555932845519,
"alpha_pos_frac": 0.42142857142857143,
"fund_max_dd": -0.28317369088420274,
"first": "2014-07-03",
"verdict": "weak/unstable alpha"
},
"blavx": {
"sym": "blavx",
"name": "Lord Abbett Multi-Asset Balanced Opp R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.011797325556756307,
"alpha_t_5y": -1.010085865942392,
"alpha_t_full": -0.7357659599702105,
"r2_5y": 0.9265728887932777,
"r2_full": 0.9328197776952308,
"corr_portfolio": 0.3103505887208745,
"corr_benchmark": 0.6379261074081366,
"alpha_pos_frac": 0.421875,
"fund_max_dd": -0.260437261686727,
"first": "2015-07-01",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"lixvx": {
"sym": "lixvx",
"name": "Lord Abbett Multi-Asset Income R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.004383174411143441,
"alpha_t_5y": -0.4568981299201697,
"alpha_t_full": -0.15577519270389997,
"r2_5y": 0.9042459815827678,
"r2_full": 0.8951857833403745,
"corr_portfolio": 0.3062653746695319,
"corr_benchmark": 0.6813260480847899,
"alpha_pos_frac": 0.4765625,
"fund_max_dd": -0.20178167405603809,
"first": "2015-07-01",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"difhx": {
"sym": "difhx",
"name": "MFS Diversified Income R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.0039245713054233385,
"alpha_t_5y": 0.5466028351398784,
"alpha_t_full": 0.5949328291259101,
"r2_5y": 0.9344580249970595,
"r2_full": 0.934363839968193,
"corr_portfolio": 0.33988525357150673,
"corr_benchmark": 0.6931115151589421,
"alpha_pos_frac": 0.5853658536585366,
"fund_max_dd": -0.23744092135921013,
"first": "2012-07-03",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"dvrlx": {
"sym": "dvrlx",
"name": "MFS Global Alternative Strategy R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.03657211770555811,
"alpha_t_5y": -2.5657867620747057,
"alpha_t_full": -0.1291828242329135,
"r2_5y": 0.6895569997257555,
"r2_full": 0.4897033044299437,
"corr_portfolio": 0.28971086546170755,
"corr_benchmark": 0.52598078027921,
"alpha_pos_frac": 0.5229357798165137,
"fund_max_dd": -0.492406289109413,
"first": "2007-12-20",
"verdict": "weak/unstable alpha"
},
"csaax": {
"sym": "csaax",
"name": "Mast Managed Futures Strategy A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.021839231499901426,
"alpha_t_5y": -0.5670420422388186,
"alpha_t_full": 1.1787995000841218,
"r2_5y": 0.25105581683621214,
"r2_full": 0.11110482665395294,
"corr_portfolio": 0.10211184167780919,
"corr_benchmark": -0.13429454987458886,
"alpha_pos_frac": 0.4472049689440994,
"fund_max_dd": -0.28773051050447884,
"first": "2012-10-04",
"verdict": "weak/unstable alpha"
},
"mstvx": {
"sym": "mstvx",
"name": "Morningstar Funds Trust - Morningstar Alternatives Fund",
"bucket": "mined",
"error": "no return history in the data set"
},
"czamx": {
"sym": "czamx",
"name": "Multi-Manager Alternative Strat Inst",
"bucket": "mined",
"error": "no return history in the data set"
},
"dpzrx": {
"sym": "dpzrx",
"name": "Nomura Diversified Income R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.000730284066316539,
"alpha_t_5y": 0.12826526384995596,
"alpha_t_full": 1.0276307131100524,
"r2_5y": 0.9461638110297028,
"r2_full": 0.9003950238884322,
"corr_portfolio": -0.013103435138370314,
"corr_benchmark": 0.7207677383087638,
"alpha_pos_frac": 0.5,
"fund_max_dd": -0.19438389559225144,
"first": "2016-05-05",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"pasix": {
"sym": "pasix",
"name": "PACE Alternative Strategies A",
"bucket": "mined",
"error": "no return history in the data set"
},
"padqx": {
"sym": "padqx",
"name": "PGIM Absolute Return Bond R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.02253366213400559,
"alpha_t_5y": 2.421479706530911,
"alpha_t_full": 3.842984939716189,
"r2_5y": 0.31680161727319966,
"r2_full": 0.42634234905883495,
"corr_portfolio": 0.2749211688974797,
"corr_benchmark": 0.14414798935984474,
"alpha_pos_frac": 0.4913294797687861,
"fund_max_dd": -0.18058226282780776,
"first": "2011-03-31",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"pwlix": {
"sym": "pwlix",
"name": "PIMCO RAE Worldwide Long/Short PLUS Inst",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.07660282789667064,
"alpha_t_5y": 2.2896624565350545,
"alpha_t_full": 1.715326163582548,
"r2_5y": 0.2993915194908645,
"r2_full": 0.3849850156716639,
"corr_portfolio": 0.4290639036766289,
"corr_benchmark": 0.2060141552122883,
"alpha_pos_frac": 0.5777777777777777,
"fund_max_dd": -0.26923060673685906,
"first": "2014-12-09",
"verdict": "alpha, but correlated with current portfolio"
},
"pqtix": {
"sym": "pqtix",
"name": "PIMCO TRENDS Fund Institutional",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.02847297577275021,
"alpha_t_5y": 0.742679931550064,
"alpha_t_full": 2.335970708368272,
"r2_5y": 0.15216577250089436,
"r2_full": 0.12647924317600812,
"corr_portfolio": 0.033638037506023684,
"corr_benchmark": -0.13324860245978445,
"alpha_pos_frac": 0.4863013698630137,
"fund_max_dd": -0.27647343409415515,
"first": "2014-01-07",
"verdict": "weak/unstable alpha"
},
"pyaix": {
"sym": "pyaix",
"name": "Payden Absolute Return Bond SI",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.02968496968469588,
"alpha_t_5y": 4.784530488694529,
"alpha_t_full": 4.200797542613163,
"r2_5y": 0.3451232727740463,
"r2_full": 0.3250972688950624,
"corr_portfolio": 0.12526637204534918,
"corr_benchmark": 0.1844971141588079,
"alpha_pos_frac": 0.47794117647058826,
"fund_max_dd": -0.15680465360347773,
"first": "2014-11-10",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"pgblx": {
"sym": "pgblx",
"name": "Principal Diversified Income R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.01412872049404619,
"alpha_t_5y": 1.5199678294021424,
"alpha_t_full": 0.12964149147420354,
"r2_5y": 0.6978234439108565,
"r2_full": 0.6010198995642109,
"corr_portfolio": 0.27492234459641884,
"corr_benchmark": 0.608590270031421,
"alpha_pos_frac": 0.5904761904761905,
"fund_max_dd": -0.23771744813108997,
"first": "2017-06-13",
"verdict": "weak/unstable alpha"
},
"pmsax": {
"sym": "pmsax",
"name": "Principal Global Multi-Strategy A",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.015538371548211896,
"alpha_t_5y": 1.8001012780965024,
"alpha_t_full": 1.665528177023728,
"r2_5y": 0.7847873201394527,
"r2_full": 0.6186592627892099,
"corr_portfolio": 0.39358726932232607,
"corr_benchmark": 0.53030328440744,
"alpha_pos_frac": 0.4941860465116279,
"fund_max_dd": -0.13947126763842388,
"first": "2011-11-02",
"verdict": "weak/unstable alpha"
},
"pglsx": {
"sym": "pglsx",
"name": "Principal Global Multi-Strategy R-6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.022834716219407386,
"alpha_t_5y": 2.6351709042494034,
"alpha_t_full": 2.2131792526289487,
"r2_5y": 0.7785150170658945,
"r2_full": 0.6229229469007576,
"corr_portfolio": 0.36642507552789066,
"corr_benchmark": 0.5511123168886634,
"alpha_pos_frac": 0.5238095238095238,
"fund_max_dd": -0.1395139992317488,
"first": "2017-06-13",
"verdict": "weak/unstable alpha"
},
"rlsfx": {
"sym": "rlsfx",
"name": "RiverPark Long/Short Opportunity Retail",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.16115615211661527,
"alpha_t_5y": -2.9697402107938906,
"alpha_t_full": -2.535083944427401,
"r2_5y": 0.7308724540747933,
"r2_full": 0.6421080886088519,
"corr_portfolio": -0.055074612442200174,
"corr_benchmark": 0.5146127994733334,
"alpha_pos_frac": 0.5988023952095808,
"fund_max_dd": -0.608949397322442,
"first": "2012-04-03",
"verdict": "weak/unstable alpha"
},
"smsax": {
"sym": "smsax",
"name": "SEI Multi Strategy Alternatives F (SIMT)",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.017757331922862697,
"alpha_t_5y": 1.564490845308455,
"alpha_t_full": 0.3248577204647862,
"r2_5y": 0.6693784935352178,
"r2_full": 0.4972808858277301,
"corr_portfolio": 0.2729803768339342,
"corr_benchmark": 0.41511767643815717,
"alpha_pos_frac": 0.450261780104712,
"fund_max_dd": -0.10984441701958725,
"first": "2010-04-05",
"verdict": "weak/unstable alpha"
},
"sioax": {
"sym": "sioax",
"name": "SEI Multi-Asset Income F (SIMT)",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.014537259779691733,
"alpha_t_5y": 1.8693036401798908,
"alpha_t_full": 2.2624177895387705,
"r2_5y": 0.8242395177988513,
"r2_full": 0.7457014660275068,
"corr_portfolio": 0.2938126221954463,
"corr_benchmark": 0.6592508517591171,
"alpha_pos_frac": 0.5269461077844312,
"fund_max_dd": -0.221002354954007,
"first": "2012-04-10",
"verdict": "weak/unstable alpha"
},
"srdax": {
"sym": "srdax",
"name": "Stone Ridge Diversified Alternatives I",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.077315378593081,
"alpha_t_5y": 4.194473136324014,
"alpha_t_full": 4.335855416124276,
"r2_5y": 0.024737567509405922,
"r2_full": 0.025626537670158656,
"corr_portfolio": 0.10433532078238511,
"corr_benchmark": -0.05093300486016625,
"alpha_pos_frac": 0.5,
"fund_max_dd": -0.06326727678361443,
"first": "2020-10-19",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"tmssx": {
"sym": "tmssx",
"name": "T. Rowe Price Multi-Strategy Total Return Fund",
"bucket": "mined",
"error": "no return history in the data set"
},
"vmnfx": {
"sym": "vmnfx",
"name": "Vanguard Market Neutral Inv",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.1213151413486212,
"alpha_t_5y": 4.1229748270195845,
"alpha_t_full": 2.480579289606245,
"r2_5y": 0.039369566462858496,
"r2_full": 1.5543122344752192e-15,
"corr_portfolio": 0.34813727658135635,
"corr_benchmark": -0.018378123746314586,
"alpha_pos_frac": 0.4824561403508772,
"fund_max_dd": -0.25936210806201754,
"first": "1998-11-17",
"verdict": "alpha, but correlated with current portfolio"
},
"maukx": {
"sym": "maukx",
"name": "Victory Pioneer Multi-Asset Ult Inc R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.03870281318242449,
"alpha_t_5y": 7.512773707625843,
"alpha_t_full": 5.4955644552594665,
"r2_5y": 0.3189960905860648,
"r2_full": 0.2813303336084766,
"corr_portfolio": 0.12275189616530456,
"corr_benchmark": 0.034450897116474206,
"alpha_pos_frac": 0.3987341772151899,
"fund_max_dd": -0.09969803918560205,
"first": "2012-12-26",
"verdict": "alpha in 5y window, but not persistent (lucky stretch?)"
},
"vtarx": {
"sym": "vtarx",
"name": "Virtus Tactical Allocation R6",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": -0.05335515876677799,
"alpha_t_5y": -2.480328859971851,
"alpha_t_full": -2.6929563012168445,
"r2_5y": 0.8608844289577577,
"r2_full": 0.8580871497621856,
"corr_portfolio": -0.06863060772036257,
"corr_benchmark": 0.7202560361372964,
"alpha_pos_frac": 0.59375,
"fund_max_dd": -0.3628908267131912,
"first": "2020-10-21",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"wmnix": {
"sym": "wmnix",
"name": "Westwood Alternative Income Instl",
"bucket": "mined",
"in_portfolio": false,
"alpha_ann_5y": 0.038442357173482246,
"alpha_t_5y": 6.529116764845873,
"alpha_t_full": 5.306875605255055,
"r2_5y": 0.367349767186132,
"r2_full": 0.17596775396502795,
"corr_portfolio": 0.09120167169588639,
"corr_benchmark": 0.18140246101188268,
"alpha_pos_frac": 0.5153846153846153,
"fund_max_dd": -0.07640201924349688,
"first": "2015-05-04",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"masfx": {
"sym": "masfx",
"name": "iMGP Alternative Strategies Fund",
"bucket": "mined",
"error": "no return history in the data set"
}
}

212
fundlab/search_results.json Normal file
View File

@ -0,0 +1,212 @@
{
"atesx": {
"sym": "atesx",
"name": "Anchor Risk Mgd Equity Strategies Instl",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.0055503014317009975,
"alpha_t_5y": 0.1484113857382209,
"alpha_t_full": 0.9293227873648724,
"r2_5y": 0.3610309574164604,
"r2_full": 0.3450047574870089,
"corr_portfolio": 0.02673861645107734,
"corr_benchmark": 0.32376107223953887,
"alpha_pos_frac": 0.49122807017543857,
"fund_max_dd": -0.12863626413945228,
"first": "2016-09-07",
"verdict": "weak/unstable alpha"
},
"atrfx": {
"sym": "atrfx",
"name": "Catalyst Systematic Alpha I",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": -0.018652787752631877,
"alpha_t_5y": -0.28898498571118264,
"alpha_t_full": 0.18349887648678206,
"r2_5y": 0.2604813339838056,
"r2_full": 0.11614807049400844,
"corr_portfolio": 0.22142653468828064,
"corr_benchmark": 0.20713067913226307,
"alpha_pos_frac": 0.4676258992805755,
"fund_max_dd": -0.3515020833110952,
"first": "2014-08-04",
"verdict": "weak/unstable alpha"
},
"cvsix": {
"sym": "cvsix",
"name": "Calamos Market Neutral Income A",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.017888238619296486,
"alpha_t_5y": 2.4839246727508577,
"alpha_t_full": 6.693203697489781,
"r2_5y": 0.738709320350339,
"r2_full": 3.3306690738754696e-15,
"corr_portfolio": 0.29027243816488385,
"corr_benchmark": 0.5237277356889887,
"alpha_pos_frac": 0.48109965635738833,
"fund_max_dd": -0.20766965351534095,
"first": "1990-09-04",
"verdict": "CANDIDATE (semi-alpha: mostly explained by net exposure)"
},
"jlpsx": {
"sym": "jlpsx",
"name": "JPMorgan US Large Cap Core Plus I",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": -0.002431497257594755,
"alpha_t_5y": -0.1629307787117402,
"alpha_t_full": 0.4343924766190256,
"r2_5y": 0.9585561291856182,
"r2_full": 0.8354903597942736,
"corr_portfolio": 0.22676613719990743,
"corr_benchmark": 0.52227955380103,
"alpha_pos_frac": 0.47540983606557374,
"fund_max_dd": -0.513285225905074,
"first": "2005-11-02",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"pmaix": {
"sym": "pmaix",
"name": "Victory Pioneer Multi-Asset Income A",
"bucket": "shortlist",
"in_portfolio": true,
"alpha_ann_5y": 0.049278056125020175,
"alpha_t_5y": 2.968511924087205,
"alpha_t_full": 3.9672810281689546,
"r2_5y": 0.7003658056055937,
"r2_full": 0.7087086927628709,
"corr_portfolio": 0.6036927197479715,
"corr_benchmark": 0.37605309741869536,
"alpha_pos_frac": 0.4764705882352941,
"fund_max_dd": -0.24116000785637026,
"first": "2011-12-23",
"verdict": "alpha, but correlated with current portfolio"
},
"pmorx": {
"sym": "pmorx",
"name": "Putnam Mortgage Opportunities A",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.05216102895935035,
"alpha_t_5y": 4.144374847398222,
"alpha_t_full": 1.4357260819167152,
"r2_5y": 0.03721366605296683,
"r2_full": 0.10084825638507544,
"corr_portfolio": 0.15610236827399032,
"corr_benchmark": 0.1549485287519335,
"alpha_pos_frac": 0.7,
"fund_max_dd": -0.19308848044669336,
"first": "2019-07-01",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"qspnx": {
"sym": "qspnx",
"name": "AQR Style Premia Alternative N",
"bucket": "shortlist",
"in_portfolio": true,
"alpha_ann_5y": 0.16785818097663643,
"alpha_t_5y": 3.3082513187185243,
"alpha_t_full": 3.2654332888524036,
"r2_5y": 0.2616252851889068,
"r2_full": 0.11754675753202926,
"corr_portfolio": 0.852144510349361,
"corr_benchmark": -0.16758094685348948,
"alpha_pos_frac": 0.49324324324324326,
"fund_max_dd": -0.41792301574889723,
"first": "2013-10-31",
"verdict": "alpha, but correlated with current portfolio"
},
"svarx": {
"sym": "svarx",
"name": "Spectrum Low Volatility Investor",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.023983827438686146,
"alpha_t_5y": 2.2535550856884474,
"alpha_t_full": 4.583134072763739,
"r2_5y": 0.28289328741120845,
"r2_full": 0.16745275696353723,
"corr_portfolio": 0.13269895533798984,
"corr_benchmark": 0.27028110365794056,
"alpha_pos_frac": 0.3698630136986301,
"fund_max_dd": -0.06486054560652632,
"first": "2013-12-18",
"verdict": "alpha in 5y window, but not persistent (lucky stretch?)"
},
"cosix": {
"sym": "cosix",
"name": "Columbia Strategic Income A",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.010918442324802066,
"alpha_t_5y": 1.6544462573044783,
"alpha_t_full": 8.97921782878531,
"r2_5y": 0.8663944005206073,
"r2_full": 3.3306690738754696e-16,
"corr_portfolio": 0.14900065311998206,
"corr_benchmark": 0.5726198691530625,
"alpha_pos_frac": 0.5086705202312138,
"fund_max_dd": -0.261588575393748,
"first": "1990-01-03",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"mbxix": {
"sym": "mbxix",
"name": "Catalyst/Millburn Hedge Strategy I",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.031018107560385543,
"alpha_t_5y": 0.8817953110008355,
"alpha_t_full": 0.9829918601685478,
"r2_5y": 0.4921477665240592,
"r2_full": 0.5706527612045402,
"corr_portfolio": 0.31809032443125856,
"corr_benchmark": 0.31985204190867955,
"alpha_pos_frac": 0.5491803278688525,
"fund_max_dd": -0.317313385665538,
"first": "2015-12-29",
"verdict": "weak/unstable alpha"
},
"eagmx": {
"sym": "eagmx",
"name": "Eaton Vance Glbl Macr Absolute Return A",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": 0.05559035554775872,
"alpha_t_5y": 5.2379756386224,
"alpha_t_full": 8.039707312489185,
"r2_5y": 0.07396669655814492,
"r2_full": -1.1102230246251565e-15,
"corr_portfolio": 0.23667567422802827,
"corr_benchmark": -0.02151029112701147,
"alpha_pos_frac": 0.44223107569721115,
"fund_max_dd": -0.0931393650735658,
"first": "1997-11-03",
"verdict": "alpha in 5y window, but not persistent (lucky stretch?)"
},
"lcorx": {
"sym": "lcorx",
"name": "Leuthold Core Investment Retail",
"bucket": "shortlist",
"error": "no return history in the data set"
},
"lamhx": {
"sym": "lamhx",
"name": "Lord Abbett Dividend Growth R6",
"bucket": "shortlist",
"in_portfolio": false,
"alpha_ann_5y": -0.005326437096891267,
"alpha_t_5y": -0.3518378999299911,
"alpha_t_full": 0.4775716671703663,
"r2_5y": 0.9437584450573062,
"r2_full": 0.9561509121649294,
"corr_portfolio": 0.29977485384501745,
"corr_benchmark": 0.6191978200471758,
"alpha_pos_frac": 0.515625,
"fund_max_dd": -0.3345219095634927,
"first": "2015-07-01",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
}
}

68
fundlab/searchlist.py Normal file
View File

@ -0,0 +1,68 @@
"""Curated longlist of candidate funds for the alpha search.
Universe rationale: we cannot meaningfully screen all ~10k registered
funds (most are small, illiquid or strategy-unstable and would not
deserve portfolio consideration anyway). So the UNIVERSE is curated by
reputation large, liquid, long-tracked funds in the buckets where
idiosyncratic alpha lives (market-neutral/quant, multi-strategy, global
macro, dynamic TA, dynamic credit/convertibles) and the SELECTION is
data-driven (fundlab/search.py). Tickers are resolved from the fund NAME
via Yahoo search with a precision gate; a candidate that cannot be
resolved cleanly is dropped, never guessed.
The 13-fund shortlist already in funds.json is screened with the same
code so the ranking is consistent (qspnx/pmaix are the user's current
holdings and act as controls).
"""
from __future__ import annotations
# (fund name as searched, strategy bucket, ticker guess or None)
LONGLIST: list[tuple[str, str, str | None]] = [
# --- market-neutral / quant / pure alpha / event-driven ------------
("AQR Diversified Event-Driven Fund", "event_driven", None),
("Bridgewater Pure Alpha II Fund", "pure_alpha", None),
("Winton Global Quantitative Fund", "cta_quant", None),
("Two Sigma Dynamic Strategy Fund", "systematic", None),
("Brevan Howard Dymon Asia Fund", "macro_relative_value", None),
("Marshall Wace Global Opportunities Fund", "macro_relative_value",
None),
# --- multi-strategy -------------------------------------------------
("ExodusPoint Diversified Fund", "multi_strategy", None),
("Verition Dynamic Risk Fund", "multi_strategy", None),
("Millennium Focus Fund", "multi_strategy", None),
("Balyasny Absolute Return Multi-Strategy Fund", "multi_strategy", None),
("Schonfeld Strategic Opportunities Fund", "multi_strategy", None),
# --- global macro / risk parity ------------------------------------
# (T. Rowe Price New Global Opportunity: fund terminated - no data)
("Bridgewater All Weather Fund", "risk_parity", None),
("Oak Hill Tactical Allocation Fund", "tactical_allocation", None),
# --- multi-asset / dynamic TA open-ends (the accessible alpha pool) -
("Fidelity Multi-Asset Income Fund", "tactical_allocation", None),
("Janus Henderson Global Dynamic Dividend Fund", "dynamic_equity", None),
("Wellington Dynamic Global Diversified Fund", "tactical_allocation", None),
("Lord Abbett Global Opportunities Fund", "tactical_allocation", None),
("BlackRock Multi-Asset Income Fund", "tactical_allocation", None),
("PIMCO Income Strategy Fund", "tactical_allocation", None),
("JPMorgan Diversified Return Fund", "tactical_allocation", None),
("Invesco Diversified Equity and Income Fund", "tactical_allocation", None),
("T. Rowe Price Global Allocation Fund", "tactical_allocation", None),
("Morgan Stanley Global Multi Asset Fund", "tactical_allocation", None),
("Fidelity Diversified Multi-Asset Fund", "tactical_allocation", None),
# --- controls: credit (expected to classify as sleeve mix)
# (Calamos Dynamic Convertible CCD is a CEF - excluded)
("PIMCO Dynamic Income Fund", "dynamic_credit", None),
]
# the 13 unique shortlist funds (share classes resolved once)
SHORTLIST = ["atesx", "atrfx", "cvsix", "jlpsx", "pmaix", "pmorx", "qspnx",
"svarx", "cosix", "mbxix", "eagmx", "lcorx", "lamhx"]
# broad sleeve set used by the screen (same 21 axes for every fund -
# nothing is tuned to a specific fund, so selection is comparable)
BROAD_SLEEVES = ["qqq", "ivv", "iwm", "vea", "efa", "vwo", "vnq", "bil",
"shv", "ief", "tlt", "vblix", "agg", "vweax", "vmbix",
"finux", "djp", "gsg", "gld", "fxe", "fxy"]
# the user's current portfolio + benchmark mix (settings.json)
PORTFOLIO = {"qspnx": 0.5, "pmaix": 0.5}
BENCHMARKS = {"spy": 1 / 3, "agg": 1 / 3, "tlt": 1 / 3}

120
fundlab/tickers.py Normal file
View File

@ -0,0 +1,120 @@
"""Ticker resolution via EDGAR prospectus covers + Yahoo chart verification.
For a fund NAME:
1. EDGAR full-text search for the name in 497/497K prospectuses
(exact-phrase FTS is fragile to hyphens / "Fund" variants, so a
small query ladder is tried: exact -> hyphen-free -> 3-word windows),
2. fetch the most relevant prospectus, extract every "Ticker Symbol: XXX"
from the cover (one per share class),
3. verify each candidate ticker against Yahoo chart metadata
(instrumentType MUTUALFUND + name token overlap),
4. accept the first that passes, else report unresolved.
Both gates are precision-oriented: a wrong fund's ticker is worse than
no ticker, so ambiguity drops the candidate.
"""
from __future__ import annotations
import re
import time
from fundlab import edgar
from fundlab.search import _name_match, chart_meta
# two cover-page formats:
# "Ticker Symbol: XXXXX" and the Class/Ticker table "Fund Name /XXXIX"
TICKER_RX = re.compile(
r"(?:ticker\s*symbol|ticker|symbol)\s*[:\-]?\s*([A-Z][A-Z0-9]{3,8})\b",
re.I)
SLASH_RX = re.compile(r"\s/\s*([A-Z][A-Z0-9]{3,8})\b")
_STOP = {"the", "and", "of", "to", "a", "i", "c", "b", "z", "x", "fund",
"funds", "series", "class"}
def _queries(name: str) -> list[str]:
"""Fallback query ladder for EDGAR FTS phrase search."""
n = re.sub(r"[-]", " ", name)
words = re.findall(r"[A-Za-z0-9.]+", n)
sig = [w for w in words if w.lower() not in _STOP]
qs = [f'"{name}"', f'"{n}"']
if len(sig) >= 3:
qs += [f'"{ " ".join(sig[:2]) }"']
if len(sig) >= 4:
qs += [f'"{ " ".join(sig[:3]) }"', f'"{ " ".join(sig[-3:]) }"']
out, seen = [], set()
for q in qs:
if q not in seen:
seen.add(q)
out.append(q)
return out
def tickers_from_prospectus(doc_url: str) -> list[str]:
"""Extract ticker candidates from the first 150KB of a prospectus."""
try:
raw = edgar.sec_get(doc_url, timeout=60)
except Exception:
return []
text = edgar.to_text(raw[:150_000])
found = re.findall(TICKER_RX, text) + re.findall(SLASH_RX, text)
out, seen = [], set()
for t in found:
t = t.upper()
if t in seen or not re.fullmatch(r"[A-Z][A-Z0-9]{3,8}", t):
continue
if t[0].isdigit():
continue
seen.add(t)
out.append(t)
return out
def resolve_via_edgar(name: str) -> dict | None:
"""Resolve fund name -> ticker via EDGAR 497 covers + chart gate.
Returns {symbol, name, sim, guess} or None.
"""
tried = set()
fetches = 0
for q in _queries(name):
try:
hits = edgar.fts_search(q, forms="497,497K", size=10)
except Exception:
continue
ciks = set()
for h in sorted(hits, key=lambda x: -x.get("score", 0)):
cik = h.get("cik")
if not cik or cik in ciks:
continue
if len(ciks) >= 6: # phrase hits span several registrants;
break # the right one is not always ranked first
ciks.add(cik)
url = edgar.doc_url(cik, h["accession"], h["filename"])
fetches += 1
for t in tickers_from_prospectus(url):
if t in tried:
continue
tried.add(t)
r = _chart_verify(t, name)
if r:
return r
time.sleep(0.2)
if fetches >= 15: # hard cap on prospectus fetches
return None
return None
def _chart_verify(ticker: str, name: str) -> dict | None:
meta = chart_meta(ticker)
if not meta:
return None
itype = (meta.get("instrumentType") or "").upper()
if itype not in ("MUTUALFUND", "FUND"):
return None
cand = meta.get("longName") or meta.get("shortName") or ""
tok = _name_match(name, cand)
if tok < 2 / 3:
return None
return {"symbol": ticker.lower(), "name": cand, "sim": round(tok, 3),
"guess": ticker}

View File

@ -273,6 +273,38 @@ def test_decompose() -> None:
check("fwd handles nan overlap", "s1" in chosen_p, f"chosen={chosen_p}") check("fwd handles nan overlap", "s1" in chosen_p, f"chosen={chosen_p}")
def test_search() -> None:
print("search engine", flush=True)
from fundlab import dbmine, search, tickers
# precision gate: a different fund sharing some words must fail
check("name gate rejects wrong fund",
search._name_match("PIMCO Access to Global Markets Fund",
"PIMCO Access Income Fund") < 2 / 3, "")
check("name gate accepts right fund",
search._name_match("Fidelity Multi-Asset Income Fund",
"Fidelity Multi-Asset Income") >= 2 / 3, "")
# query ladder handles hyphens + word-count fallbacks
q = tickers._queries("AQR Diversified Event-Driven Fund")
check("query ladder exact first", q[0] ==
'"AQR Diversified Event-Driven Fund"', str(q))
check("query ladder hyphen-free", '"AQR Diversified Event Driven Fund"'
in q, str(q))
check("query ladder 2-word prefix", '"AQR Diversified"' in q, str(q))
# family dedupe: share classes collapse, distinct funds don't
check("family dedupe same fund",
dbmine.family_key("AQR Style Premia Alternative R6")
== dbmine.family_key("AQR Style Premia Alternative I"), "")
check("family dedupe distinct funds",
dbmine.family_key("AQR Style Premia Alternative R6")
!= dbmine.family_key("AQR Managed Futures Strategy I"), "")
# ticker regex: both cover formats
import re
t1 = re.findall(tickers.TICKER_RX, "Ticker Symbol: ABCDX")
t2 = re.findall(tickers.SLASH_RX, "Fidelity Multi-Asset Income Fund /FMSDX ")
check("ticker regex label format", t1 == ["ABCDX"], str(t1))
check("ticker regex slash format", t2 == ["FMSDX"], str(t2))
def test_curated() -> None: def test_curated() -> None:
print("curated", flush=True) print("curated", flush=True)
import fundlab.fundinfo as fi import fundlab.fundinfo as fi
@ -294,6 +326,7 @@ def main() -> int:
test_strategy() test_strategy()
test_nport() test_nport()
test_decompose() test_decompose()
test_search()
test_curated() test_curated()
test_edgar_live() test_edgar_live()
print(f"\n{PASS} passed, {FAIL} failed") print(f"\n{PASS} passed, {FAIL} failed")