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:
parent
db5fc4626d
commit
afec7bda73
60
app.py
60
app.py
|
|
@ -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
128
fundlab/dbmine.py
Normal 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)
|
||||||
|
|
@ -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
100
fundlab/harvest.py
Normal 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()
|
||||||
128
fundlab/resolved_tickers.json
Normal file
128
fundlab/resolved_tickers.json
Normal 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
326
fundlab/search.py
Normal 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
223
fundlab/search_harvest.json
Normal 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
862
fundlab/search_mined.json
Normal 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
212
fundlab/search_results.json
Normal 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
68
fundlab/searchlist.py
Normal 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
120
fundlab/tickers.py
Normal 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}
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user