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")
|
||||
|
||||
# --- 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, {})
|
||||
_man = _MAN.get(_fl_pick, {})
|
||||
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.
|
||||
finux stops in 2017), so a global inner join can be empty."""
|
||||
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)
|
||||
if a is not None:
|
||||
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}")
|
||||
|
||||
|
||||
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:
|
||||
print("curated", flush=True)
|
||||
import fundlab.fundinfo as fi
|
||||
|
|
@ -294,6 +326,7 @@ def main() -> int:
|
|||
test_strategy()
|
||||
test_nport()
|
||||
test_decompose()
|
||||
test_search()
|
||||
test_curated()
|
||||
test_edgar_live()
|
||||
print(f"\n{PASS} passed, {FAIL} failed")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user