Find candidate funds NOT in the DB: exhaustive EDGAR 497-universe pass

fundlab/edgar_universe.py - the 'search' for funds we don't have:
  1. SEC full-index (Archives/edgar/full-index/YYYY/QTRn/company.gz)
     lists every filing; CIKs that filed a base 497/497K in the past 4
     quarters = every currently-active US open-end fund (1,668).
  2. one small fetch per CIK: the full-submission .txt carries the
     line-based SGML prospectus cover (<SERIES-NAME> ... unclosed
     <CLASS-CONTRACT-TICKER-SYMBOL> tags) - fund name + every class
     ticker, often several funds per filing.
  3. alpha-leaning name filter (expanded dbmine PATTERN: +relative
     value, risk allocation, dynamic global, real return, hedged),
     drop local-DB + shortlist tickers,
  4. Yahoo chart verify: instrumentType MUTUALFUND (OTC open-end;
     exchange name is useless - OTC funds report 'Nasdaq'),
     >=5y daily history,
  5. share-class dedupe (longest history), goget download, same
     screen_fund engine.
  Resumable (per-CIK covers cache), 4-thread, Range-free small files.

First pass results (46 funds screened, 5 NEW candidates):
  egrix/ecgmx Eaton Vance Global Macro Absolute Return: R2 0.07,
    +7.9%/+4.8% 5y alpha, t 4.9/4.6, corr-port 0.22 - pure macro idio
  dmszx Destinations Multi-Strategy Alternatives: R2 0.57, +3.3%, t3.5
  cbhax Victory Market Neutral Income: R2 0.07, +4.6%, t2.9, corr 0.11
  pdinx Putnam Diversified Income: semi-alpha (full t5.8, 62% 6m+)
  (+ wmnux/gioax = 2nd share classes of already-known candidates)
  vmnix Vanguard MN: alpha but corr 0.35 (portfolio already 50% MN)

app Fund Lab alpha table now also reads search_external.json.
tests: parse_cover unit tests (unclosed-tag SGML, ticker series
attach, malformed rejected). 65/65 fundlab, 32/32 app.
This commit is contained in:
Greg Pomerantz 2026-08-26 15:22:52 -04:00
parent afec7bda73
commit 592d12958f
6 changed files with 1050 additions and 2 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ __pycache__/
settings.json
funds.json
fundlab/nport_cache/*.html
fundlab/universe_cache/

3
app.py
View File

@ -618,7 +618,8 @@ with tab_fundlab:
# --- 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"):
for _src in ("search_results.json", "search_mined.json",
"search_external.json"):
try:
_j = json.loads((_dc.RESULTS.parent / _src).read_text())
except Exception:

View File

@ -29,7 +29,8 @@ PATTERN = re.compile(
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)
r"multi.?manager|diversified (alternatives|income)|relative value|"
r"risk allocation|dynamic global|real return|hedge?d (allocation|strateg))", re.I)
# skip anything that is really an ETF wrapper or index
SKIP = re.compile(

227
fundlab/edgar_universe.py Normal file
View File

@ -0,0 +1,227 @@
"""Find candidate funds NOT in the local database - exhaustive pass over
the SEC's full filing index.
Source: the EDGAR full-index (https://www.sec.gov/Archives/edgar/
full-index/YYYY/QTRn/company.gz) lists every filing. Unique CIKs that
filed a 497-family form in the quarter = currently registered US
open-end funds (1,732 in 2026 Q2). One fetch per CIK - the full
submission .txt (~10-30KB) contains the SGML prospectus cover with
<SERIES-NAME> (fund name) and <CLASS-CONTRACT-TICKER-SYMBOL> (ticker)
tags, often several series per filing.
Pass:
1. full-index -> latest 497 filing per CIK
2. fetch the .txt (small), extract series names + tickers from the
SGML cover
3. keep alpha-leaning series, drop local-DB and shortlist tickers
4. verify on Yahoo chart metadata (OTC fund class), require >=5y
history, goget-download, screen with the same engine.
Politeness: 0.15s between requests; resumable (per-CIK cache).
"""
from __future__ import annotations
import gzip
import json
import re
import subprocess
import time
import urllib.request
from concurrent.futures import as_completed
from pathlib import Path
from fundlab import edgar, search
from fundlab.dbmine import PATTERN as ALPHA_KW, SKIP
# A fund files its base 497 (and 497K) about once a year, in whatever
# quarter its re-filing lands - so the FULL universe is the union over
# ~4 consecutive quarters.
QUARTERS = [("2026", "2"), ("2026", "1"), ("2025", "4"), ("2025", "3")]
def _index_url(year: str, qtr: str) -> str:
return (f"https://www.sec.gov/Archives/edgar/full-index/{year}"
f"/QTR{qtr}/company.gz")
# index paths are relative to /Archives/ (edgar/data/<cik>/<acc>.txt)
ARCH = "https://www.sec.gov/Archives/"
CACHE = Path(__file__).parent / "universe_cache"
COVERS = CACHE / "covers.json"
RESULTS = Path(__file__).parent / "search_external.json"
DATA = Path.home() / "prog/fin/stocks"
GOGET = Path.home() / "go/bin/goget"
UA = {"User-Agent": "research test@example.com"}
# The prospectus cover is line-based SGML with UNCLOSED tags:
# <SERIES-NAME>1290 GAMCO Small/Mid Cap Value Fund
# <CLASS-CONTRACT-TICKER-SYMBOL>TNVAX
# so parse line by line, not with closed-tag regexes.
def fetch_index(quarters: list[tuple[str, str]] = QUARTERS) -> dict:
"""cik -> path of its LATEST 497/497K filing's .txt across quarters."""
rx = re.compile(r"^(.*?)\s+(497\w*)\s+(\d+)\s+(\d{4}-\d{2}-\d{2})\s+"
r"(edgar/data/\d+/[0-9\-]+)\.txt\s*$")
best: dict[str, tuple[tuple, str]] = {}
for year, qtr in quarters:
req = urllib.request.Request(_index_url(year, qtr), headers=UA)
text = gzip.decompress(urllib.request.urlopen(req, timeout=120)
.read()).decode("utf-8", "ignore")
for line in text.splitlines():
m = rx.match(line)
if not m:
continue
_name, form, cik, date, path = m.groups()
if form not in ("497", "497K"): # base/annual carry tickers
continue
key = (year, qtr, date)
if cik not in best or key > best[cik][0]:
best[cik] = (key, path)
return {c: p + ".txt" for c, (_k, p) in best.items()}
def parse_cover(text: str) -> list[dict]:
"""Line-based SGML cover -> [{name, tickers[]}].
The 497 cover uses UNCLOSED tags, one per line:
<SERIES-NAME>1290 Multi-Alternative Strategies Fund
<CLASS-CONTRACT-TICKER-SYMBOL>TNMAX
Each ticker attaches to the most recent <SERIES-NAME>.
"""
series: list[dict] = []
cur: dict | None = None
for line in text.splitlines():
if line.startswith("<SERIES-NAME>"):
cur = {"name": line[len("<SERIES-NAME>"):].strip(),
"tickers": []}
series.append(cur)
elif line.startswith("<CLASS-CONTRACT-TICKER-SYMBOL>") \
and cur is not None:
t = line[len("<CLASS-CONTRACT-TICKER-SYMBOL>"):]
t = t.split("<")[0].strip()
if re.fullmatch(r"[A-Z][A-Z0-9]{3,8}", t):
cur["tickers"].append(t)
return series
def _fetch_one(cik: str, path: str) -> tuple[str, dict]:
url = ARCH + path
try:
req = urllib.request.Request(url, headers=UA)
raw = urllib.request.urlopen(req, timeout=90).read()
text = raw.decode("utf-8", "ignore")
return cik, {"series": parse_cover(text)}
except Exception as e:
return cik, {"error": str(e)}
def fetch_covers(max_ciks: int | None = None,
workers: int = 4) -> dict:
from concurrent.futures import ThreadPoolExecutor
ciks = fetch_index()
if max_ciks:
ciks = dict(list(ciks.items())[:max_ciks])
CACHE.mkdir(exist_ok=True)
cache = (json.loads(COVERS.read_text()) if COVERS.exists() else {})
todo = sorted(c for c, p in ciks.items()
if c not in cache or "error" in cache.get(c, {}))
print(f"{len(ciks)} registered funds, {len(todo)} covers to fetch",
flush=True)
done = 0
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = {ex.submit(_fetch_one, c, ciks[c]): c for c in todo}
for fut in as_completed(futs):
cik, res = fut.result()
cache[cik] = res
done += 1
if done % 100 == 0:
COVERS.write_text(json.dumps(cache))
print(f" {done}/{len(todo)} covers", flush=True)
COVERS.write_text(json.dumps(cache))
n_err = sum(1 for v in cache.values() if "error" in v)
print(f"covers done: {len(cache)} ({n_err} errors)", flush=True)
return cache
def run(max_ciks: int | None = None) -> dict:
covers = fetch_covers(max_ciks)
# candidate (name, ticker) pairs, alpha-leaning, not already known
local = {p.name[:-5].lower() for p in DATA.glob("*.json")}
shortlist = set(json.loads(
(Path(__file__).parent.parent / "funds.json").read_text()))
known = local | shortlist
cands: dict[str, str] = {}
for v in covers.values():
for s in v.get("series", []):
name = s.get("name", "")
if not name or ALPHA_KW.search(name) is None:
continue
if SKIP.search(name):
continue
for t in s.get("tickers", []):
if t.lower() not in known:
cands.setdefault(t, name)
print(f"{len(cands)} alpha-leaning tickers not in local DB", flush=True)
# Yahoo verify: OTC fund class + >=5y history
verified = []
for t, name in sorted(cands.items()):
meta = search.chart_meta(t)
if not meta:
continue
# keep OTC open-end fund classes; drop exchange-listed ETFs
# (Yahoo shows OTC funds as exchange "Nasdaq" - instrumentType
# is the real discriminator)
if (meta.get("instrumentType") or "").upper() not in ("MUTUALFUND", ""):
continue
n = 0
try:
req = urllib.request.Request(
f"https://query1.finance.yahoo.com/v8/finance/chart/"
f"{t}?range=20y&interval=1d", headers=search.UA)
d = json.load(urllib.request.urlopen(req, timeout=30))
res = (d.get("chart") or {}).get("result")
n = len(res[0].get("timestamp", [])) if res else 0
except Exception:
pass
if n >= 1250:
verified.append({"ticker": t, "name": name, "days": n})
time.sleep(0.15)
print(f"{len(verified)} with >=5y daily history", flush=True)
# one class per fund (share classes share the series name): keep the
# longest-history class
by_name: dict[str, dict] = {}
for v in verified:
cur = by_name.get(v["name"])
if cur is None or v["days"] > cur["days"]:
by_name[v["name"]] = v
verified = list(by_name.values())
print(f"{len(verified)} after share-class dedupe", flush=True)
missing = [v["ticker"].lower() for v in verified
if not (DATA / f"{v['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=3600)
results = {}
for v in verified:
sym = v["ticker"].lower()
if not (DATA / f"{sym}-history.csv").exists():
results[sym] = {"sym": sym, "name": v["name"],
"error": "no history after download"}
continue
row = search.screen_fund(sym, v["name"], "external")
results[sym] = row
print(f"{sym:7} {v['name'][:44]:44} "
f"{row.get('verdict', row.get('error'))[:40]}", flush=True)
RESULTS.write_text(json.dumps(results, indent=1, default=str))
print(f"wrote {RESULTS}")
return results
if __name__ == "__main__":
import sys
run(max_ciks=int(sys.argv[1]) if len(sys.argv) > 1 else None)

View File

@ -0,0 +1,784 @@
{
"abrcx": {
"sym": "abrcx",
"name": "INVESCO BALANCED-RISK ALLOCATION FUND",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.02431079155580088,
"alpha_t_5y": -1.1380135810071292,
"alpha_t_full": -0.10075378460712034,
"r2_5y": 0.6866538793533667,
"r2_full": 0.5702527229743425,
"corr_portfolio": 0.2971053326694507,
"corr_benchmark": 0.4914378774853647,
"alpha_pos_frac": 0.5024875621890548,
"fund_max_dd": -0.2648698714930865,
"first": "2009-06-04",
"verdict": "weak/unstable alpha"
},
"acmtx": {
"sym": "acmtx",
"name": "AB All Market Real Return Portfolio",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.008977906685541883,
"alpha_t_5y": -0.6185466916546568,
"alpha_t_full": -1.8049661183808015,
"r2_5y": 0.9308174525558985,
"r2_full": 0.9148055700727797,
"corr_portfolio": 0.36203745363580186,
"corr_benchmark": 0.4820324206386384,
"alpha_pos_frac": 0.5491329479768786,
"fund_max_dd": -0.5000395256742471,
"first": "2010-03-10",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"asfcx": {
"sym": "asfcx",
"name": "Virtus AlphaSimplex Managed Futures Strategy Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.050524321896119224,
"alpha_t_5y": -1.0719592828469746,
"alpha_t_full": 0.6737711569229176,
"r2_5y": 0.32403220569525515,
"r2_full": 0.1460167321719531,
"corr_portfolio": 0.2109754219397301,
"corr_benchmark": 0.007680002259431506,
"alpha_pos_frac": 0.5294117647058824,
"fund_max_dd": -0.38006080801338593,
"first": "2010-08-02",
"verdict": "weak/unstable alpha"
},
"bflax": {
"sym": "bflax",
"name": "Lord Abbett Multi-Asset Balanced Opportunity Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.022357735532510435,
"alpha_t_5y": -1.8796166397835283,
"alpha_t_full": 0.5860029940181443,
"r2_5y": 0.9239162542024056,
"r2_full": 0.8946990731548573,
"corr_portfolio": 0.3206688120952309,
"corr_benchmark": 0.6237577355062855,
"alpha_pos_frac": 0.441025641025641,
"fund_max_dd": -0.4235010445079658,
"first": "2000-05-30",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"cabdx": {
"sym": "cabdx",
"name": "AB RELATIVE VALUE FUND, INC.",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.01688728798911227,
"alpha_t_5y": 0.92785161175982,
"alpha_t_full": 3.2687846347821083,
"r2_5y": 0.9097028368139576,
"r2_full": -1.7763568394002505e-15,
"corr_portfolio": 0.4020338058839423,
"corr_benchmark": 0.5313385053392616,
"alpha_pos_frac": 0.5336322869955157,
"fund_max_dd": -0.5740208303115659,
"first": "1990-01-03",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"cabix": {
"sym": "cabix",
"name": "AB GLOBAL RISK ALLOCATION FUND, INC.",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.001291960694234989,
"alpha_t_5y": -0.1012090629895647,
"alpha_t_full": -0.5420762428779787,
"r2_5y": 0.8845638778610537,
"r2_full": 0.8378160490700229,
"corr_portfolio": 0.30433399548201784,
"corr_benchmark": 0.7062567757399001,
"alpha_pos_frac": 0.4666666666666667,
"fund_max_dd": -0.43568595855945036,
"first": "2005-03-11",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"cbhax": {
"sym": "cbhax",
"name": "Victory Market Neutral Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.045718981254340874,
"alpha_t_5y": 2.9376692841672387,
"alpha_t_full": 2.7012545394942338,
"r2_5y": 0.06743907115396375,
"r2_full": 0.03677108818850705,
"corr_portfolio": 0.10906099415010886,
"corr_benchmark": -0.07857056635130409,
"alpha_pos_frac": 0.5345911949685535,
"fund_max_dd": -0.07015276807421733,
"first": "2012-11-20",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"cdazx": {
"sym": "cdazx",
"name": "Multi-Manager Directional Alternative Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.05512684432523355,
"alpha_t_5y": 2.2291065782162365,
"alpha_t_full": 0.24661830289541073,
"r2_5y": 0.6012669420463201,
"r2_full": 0.7197363703485559,
"corr_portfolio": 0.4579231297920752,
"corr_benchmark": 0.4914274850949033,
"alpha_pos_frac": 0.45871559633027525,
"fund_max_dd": -0.30928871122470447,
"first": "2017-01-13",
"verdict": "weak/unstable alpha"
},
"clabx": {
"sym": "clabx",
"name": "Columbia Multi Strategy Alternatives Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.020379471998732104,
"alpha_t_5y": 1.1500006945087957,
"alpha_t_full": -1.2272095247584698,
"r2_5y": 0.1173597867273184,
"r2_full": 0.09449345933312647,
"corr_portfolio": 0.35584270960870523,
"corr_benchmark": 0.1343210644348764,
"alpha_pos_frac": 0.5413533834586466,
"fund_max_dd": -0.32821470382715756,
"first": "2015-01-30",
"verdict": "weak/unstable alpha"
},
"cmiex": {
"sym": "cmiex",
"name": "Multi-Manager International Equity Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.002331855387643293,
"alpha_t_5y": 0.17639529799774897,
"alpha_t_full": 0.6102573342678537,
"r2_5y": 0.9638651024018033,
"r2_full": 0.9670082571200014,
"corr_portfolio": 0.34449815461155114,
"corr_benchmark": 0.5772912659976659,
"alpha_pos_frac": 0.3763440860215054,
"fund_max_dd": -0.3535156169126247,
"first": "2018-05-23",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"craax": {
"sym": "craax",
"name": "Columbia Adaptive Risk Allocation Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.003469056636668165,
"alpha_t_5y": 0.22637061663726601,
"alpha_t_full": 0.16227895712073426,
"r2_5y": 0.8224101463318675,
"r2_full": 0.7224477226328625,
"corr_portfolio": 0.2273261133310748,
"corr_benchmark": 0.7459126348558662,
"alpha_pos_frac": 0.5853658536585366,
"fund_max_dd": -0.18617586300486277,
"first": "2012-06-20",
"verdict": "weak/unstable alpha"
},
"crihx": {
"sym": "crihx",
"name": "CRM LONG/SHORT OPPORTUNITIES FUND",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.007485374462626657,
"alpha_t_5y": -0.23393840653966574,
"alpha_t_full": 0.11386798000136084,
"r2_5y": 0.5970928094523608,
"r2_full": 0.5767322167951201,
"corr_portfolio": 0.23881152028192626,
"corr_benchmark": 0.4717239779508285,
"alpha_pos_frac": 0.5350877192982456,
"fund_max_dd": -0.2132648191040607,
"first": "2016-08-23",
"verdict": "weak/unstable alpha"
},
"ctrzx": {
"sym": "ctrzx",
"name": "Multi-Manager Total Return Bond Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.002841792738683169,
"alpha_t_5y": 0.5646938678714031,
"alpha_t_full": 0.08126445518692349,
"r2_5y": 0.9580842437964088,
"r2_full": 0.9203142086222048,
"corr_portfolio": -0.09873744886889077,
"corr_benchmark": 0.7090443634269116,
"alpha_pos_frac": 0.6272727272727273,
"fund_max_dd": -0.19196661534815573,
"first": "2017-01-04",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"czmsx": {
"sym": "czmsx",
"name": "Multi-Manager Small Cap Equity Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.02151433452245592,
"alpha_t_5y": -1.4827326889061718,
"alpha_t_full": -0.7673926997498722,
"r2_5y": 0.9742635338088821,
"r2_full": 0.9775861673404711,
"corr_portfolio": 0.2651823518503439,
"corr_benchmark": 0.5719735967346525,
"alpha_pos_frac": 0.5272727272727272,
"fund_max_dd": -0.4156571769366513,
"first": "2017-01-04",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"czmvx": {
"sym": "czmvx",
"name": "Multi-Manager Value Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -7.609071065594716e-06,
"alpha_t_5y": -0.0004964458525896136,
"alpha_t_full": 0.5857934648428721,
"r2_5y": 0.9384666021247926,
"r2_full": 0.9581696404079431,
"corr_portfolio": 0.388798668971174,
"corr_benchmark": 0.5577091562530161,
"alpha_pos_frac": 0.5181818181818182,
"fund_max_dd": -0.37423219651958606,
"first": "2017-01-04",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"dbicx": {
"sym": "dbicx",
"name": "DWS Global Macro Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.008958521689852273,
"alpha_t_5y": -0.781548491843171,
"alpha_t_full": -0.1580111206369707,
"r2_5y": 0.844675487118287,
"r2_full": 0.7865956066939078,
"corr_portfolio": 0.30334616811530735,
"corr_benchmark": 0.5034606122491824,
"alpha_pos_frac": 0.6053811659192825,
"fund_max_dd": -0.6394527499376437,
"first": "2001-08-28",
"verdict": "weak/unstable alpha"
},
"difax": {
"sym": "difax",
"name": "MFS Diversified Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.0007242113326169416,
"alpha_t_5y": 0.1032694900725035,
"alpha_t_full": -0.00015137256732595007,
"r2_5y": 0.9369388051120815,
"r2_full": 0.9453956827386147,
"corr_portfolio": 0.33920779248964134,
"corr_benchmark": 0.694429298012178,
"alpha_pos_frac": 0.5527426160337553,
"fund_max_dd": -0.35241227466974046,
"first": "2006-05-30",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"dltrx": {
"sym": "dltrx",
"name": "Nomura Limited-Term Diversified Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.014804666027616438,
"alpha_t_5y": -2.410119727303281,
"alpha_t_full": 2.639559373890048,
"r2_5y": 0.7141450251814119,
"r2_full": 0.53699842620344,
"corr_portfolio": -0.023842928935355934,
"corr_benchmark": 0.4670207303004375,
"alpha_pos_frac": 0.5953757225433526,
"fund_max_dd": -0.0744118782838673,
"first": "2003-06-02",
"verdict": "weak/unstable alpha"
},
"dmszx": {
"sym": "dmszx",
"name": "Destinations Multi Strategy Alternatives Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.033133369430583086,
"alpha_t_5y": 3.4731356626798315,
"alpha_t_full": 2.537079157986993,
"r2_5y": 0.5658661130101695,
"r2_full": 0.6183622430517614,
"corr_portfolio": 0.27114533278059666,
"corr_benchmark": 0.4543087195661785,
"alpha_pos_frac": 0.46153846153846156,
"fund_max_dd": -0.2111802646567632,
"first": "2018-07-19",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"dpcfx": {
"sym": "dpcfx",
"name": "Nomura Diversified Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.009827995765921152,
"alpha_t_5y": -1.7573068717890346,
"alpha_t_full": -0.3963947957386501,
"r2_5y": 0.9482988236237405,
"r2_full": 0.38187509130722885,
"corr_portfolio": -0.009656929667627531,
"corr_benchmark": 0.7216922354923229,
"alpha_pos_frac": 0.5260115606936416,
"fund_max_dd": -0.2062761957149597,
"first": "2002-10-28",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"drrax": {
"sym": "drrax",
"name": "BNY Mellon Global Real Return Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": null,
"alpha_t_5y": NaN,
"alpha_t_full": 0.2067529835369513,
"r2_5y": NaN,
"r2_full": 0.6736425116088385,
"corr_portfolio": 0.4427608420456678,
"corr_benchmark": 0.600649061594668,
"alpha_pos_frac": 0.4603174603174603,
"fund_max_dd": -0.15954052131449503,
"first": "2010-05-13",
"verdict": "no 5y window"
},
"dvrax": {
"sym": "dvrax",
"name": "MFS Global Alternative Strategy Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.035882882257714986,
"alpha_t_5y": -2.5193768255416247,
"alpha_t_full": -0.0674127708966125,
"r2_5y": 0.6930253728284286,
"r2_full": 0.5952916098414378,
"corr_portfolio": 0.28894962782454986,
"corr_benchmark": 0.5257765107933304,
"alpha_pos_frac": 0.44495412844036697,
"fund_max_dd": -0.36758478867230726,
"first": "2007-12-20",
"verdict": "weak/unstable alpha"
},
"ecgmx": {
"sym": "ecgmx",
"name": "Eaton Vance Global Macro Absolute Return Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.04800543517208868,
"alpha_t_5y": 4.548806986875946,
"alpha_t_full": 3.8498789524571984,
"r2_5y": 0.07984819805042198,
"r2_full": 0.18131703836443613,
"corr_portfolio": 0.2343247370123227,
"corr_benchmark": -0.02155412510980591,
"alpha_pos_frac": 0.4619289340101523,
"fund_max_dd": -0.09333598943370047,
"first": "2009-10-02",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"egrix": {
"sym": "egrix",
"name": "Eaton Vance Global Macro Absolute Return Advantage Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.0792507580920934,
"alpha_t_5y": 4.921025818208902,
"alpha_t_full": 4.652680988414438,
"r2_5y": 0.0671402533785459,
"r2_full": 0.17053389078371528,
"corr_portfolio": 0.22290815991893495,
"corr_benchmark": 0.024683988785995706,
"alpha_pos_frac": 0.45698924731182794,
"fund_max_dd": -0.1416510427878418,
"first": "2010-09-02",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
},
"fabzx": {
"sym": "fabzx",
"name": "Franklin Alternative Strategies Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.012738708711606987,
"alpha_t_5y": 1.1947373029346038,
"alpha_t_full": 1.9134745157272255,
"r2_5y": 0.6077635800797977,
"r2_full": 0.6629632968079515,
"corr_portfolio": 0.2909820766951521,
"corr_benchmark": 0.4807663872981757,
"alpha_pos_frac": 0.46938775510204084,
"fund_max_dd": -0.11030793015233042,
"first": "2013-11-21",
"verdict": "weak/unstable alpha"
},
"gioax": {
"sym": "gioax",
"name": "Guggenheim Macro Opportunities Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.011568520239574102,
"alpha_t_5y": 1.8383699917598801,
"alpha_t_full": 3.8799980791127155,
"r2_5y": 0.7738061385123135,
"r2_full": 0.4830733305761531,
"corr_portfolio": 0.16797052815506486,
"corr_benchmark": 0.4568757652827977,
"alpha_pos_frac": 0.47368421052631576,
"fund_max_dd": -0.12467053383664317,
"first": "2011-12-01",
"verdict": "CANDIDATE (semi-alpha: mostly explained by net exposure)"
},
"gpmfx": {
"sym": "gpmfx",
"name": "GuidePath(R) Managed Futures Strategy Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.11800376453789357,
"alpha_t_5y": 1.8358247548739033,
"alpha_t_full": -0.28744309359507936,
"r2_5y": 0.27995149895389726,
"r2_full": 0.16750941649381756,
"corr_portfolio": 0.18881712161413045,
"corr_benchmark": -0.064267212870549,
"alpha_pos_frac": 0.48760330578512395,
"fund_max_dd": -0.35986645151233765,
"first": "2016-01-20",
"verdict": "weak/unstable alpha"
},
"iigix": {
"sym": "iigix",
"name": "Voya Multi-Manager International Equity Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.030861993999147203,
"alpha_t_5y": -2.177612694458389,
"alpha_t_full": -1.8750744724697312,
"r2_5y": 0.9562913394853136,
"r2_full": 0.9433765294584024,
"corr_portfolio": 0.35607863948851787,
"corr_benchmark": 0.5396437711368752,
"alpha_pos_frac": 0.5384615384615384,
"fund_max_dd": -0.37669522005302647,
"first": "2011-01-10",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"napix": {
"sym": "napix",
"name": "Voya Multi-Manager International Small Cap Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.00010772911027657289,
"alpha_t_5y": 0.005725340198476609,
"alpha_t_full": -0.7870548179900667,
"r2_5y": 0.9278725823430072,
"r2_full": 0.8810943332512947,
"corr_portfolio": 0.3828035762522231,
"corr_benchmark": 0.5469650177880592,
"alpha_pos_frac": 0.5919282511210763,
"fund_max_dd": -0.6835497479017605,
"first": "2005-12-22",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"isfax": {
"sym": "isfax",
"name": "Lord Abbett Multi-Asset Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.0073214844421354945,
"alpha_t_5y": -0.7525786987083127,
"alpha_t_full": -0.6764599676493112,
"r2_5y": 0.901272562930632,
"r2_full": 0.865791715298438,
"corr_portfolio": 0.3141685091103915,
"corr_benchmark": 0.6673364375704959,
"alpha_pos_frac": 0.47692307692307695,
"fund_max_dd": -0.31904288027730954,
"first": "2005-06-30",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"nlsax": {
"sym": "nlsax",
"name": "Neuberger Berman Long Short Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": null,
"alpha_t_5y": NaN,
"alpha_t_full": -1.0489649800504441,
"r2_5y": NaN,
"r2_full": 0.7597914180929699,
"corr_portfolio": 0.3938191096744254,
"corr_benchmark": 0.5145794299058428,
"alpha_pos_frac": 0.5471698113207547,
"fund_max_dd": -0.17941457915503312,
"first": "2011-12-30",
"verdict": "no 5y window"
},
"nmmgx": {
"sym": "nmmgx",
"name": "MULTI-MANAGER GLOBAL REAL ESTATE FUND",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.03924589113646899,
"alpha_t_5y": -1.6674972243241934,
"alpha_t_full": -1.5105559984393955,
"r2_5y": 0.8810856670385193,
"r2_full": 0.8953011928041585,
"corr_portfolio": 0.28490440299527303,
"corr_benchmark": 0.6072181334227575,
"alpha_pos_frac": 0.5776699029126213,
"fund_max_dd": -0.4028238812682835,
"first": "2008-12-16",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"orilx": {
"sym": "orilx",
"name": "NORTH SQUARE MULTI STRATEGY FUND",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.004094564146332569,
"alpha_t_5y": 0.37896947051588986,
"alpha_t_full": -1.112500680629622,
"r2_5y": 0.9633390192463587,
"r2_full": 0.9141513074005219,
"corr_portfolio": 0.3126301165977903,
"corr_benchmark": 0.5953364968349182,
"alpha_pos_frac": 0.5433526011560693,
"fund_max_dd": -0.505846985181069,
"first": "2000-05-30",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"padcx": {
"sym": "padcx",
"name": "PGIM Absolute Return Bond Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.009662575109199524,
"alpha_t_5y": 1.1017317652378988,
"alpha_t_full": 2.0011643519435998,
"r2_5y": 0.2658887884471809,
"r2_full": 0.403778553758245,
"corr_portfolio": 0.29093168096348476,
"corr_benchmark": 0.1517945188590106,
"alpha_pos_frac": 0.49710982658959535,
"fund_max_dd": -0.18070752793130407,
"first": "2011-03-31",
"verdict": "weak/unstable alpha"
},
"paiex": {
"sym": "paiex",
"name": "T. Rowe Price Dynamic Global Bond Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.01584136070271343,
"alpha_t_5y": 0.8810708941705994,
"alpha_t_full": 2.3197870779795036,
"r2_5y": 0.20317125310300488,
"r2_full": 0.17606625688884936,
"corr_portfolio": 0.11453845034098124,
"corr_benchmark": -0.33569146349242185,
"alpha_pos_frac": 0.5263157894736842,
"fund_max_dd": -0.09938973154057051,
"first": "2015-01-23",
"verdict": "weak/unstable alpha"
},
"pdinx": {
"sym": "pdinx",
"name": "PUTNAM DIVERSIFIED INCOME TRUST",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.007870595552057455,
"alpha_t_5y": 0.25603001007596793,
"alpha_t_full": 5.7757613946139115,
"r2_5y": 0.10939675559633755,
"r2_full": 8.881784197001252e-16,
"corr_portfolio": 0.1515436580369243,
"corr_benchmark": 0.20295102993548317,
"alpha_pos_frac": 0.6205128205128205,
"fund_max_dd": -0.4551253654646945,
"first": "1990-01-03",
"verdict": "CANDIDATE (semi-alpha: mostly explained by net exposure)"
},
"pgbax": {
"sym": "pgbax",
"name": "Diversified Income Fund (f/k/a Global Diversified Income Fund)",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.007947133715129676,
"alpha_t_5y": 0.8551070419390819,
"alpha_t_full": 2.0440573123169528,
"r2_5y": 0.6959518160946334,
"r2_full": 0.6373465751237053,
"corr_portfolio": 0.3056432590768371,
"corr_benchmark": 0.5868996479444599,
"alpha_pos_frac": 0.46116504854368934,
"fund_max_dd": -0.2372903725951624,
"first": "2008-12-17",
"verdict": "weak/unstable alpha"
},
"rmyax": {
"sym": "rmyax",
"name": "Multi-Strategy Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.006094862604192386,
"alpha_t_5y": -0.48457991057958427,
"alpha_t_full": -0.5027716674783317,
"r2_5y": 0.8694775275329713,
"r2_full": 0.8302135520066936,
"corr_portfolio": 0.2998455767741149,
"corr_benchmark": 0.6806849145630515,
"alpha_pos_frac": 0.5615384615384615,
"fund_max_dd": -0.22042694713026867,
"first": "2015-05-04",
"verdict": "sleeve mix (R\u00b2 high) - not alpha-driven"
},
"rrpax": {
"sym": "rrpax",
"name": "SIIT Real Return Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.02091888763838627,
"alpha_t_5y": 2.584930522788122,
"alpha_t_full": 0.4909488346026396,
"r2_5y": 0.61894306092845,
"r2_full": 0.41403797029199063,
"corr_portfolio": 0.00768717905343307,
"corr_benchmark": 0.4478738908791492,
"alpha_pos_frac": 0.517948717948718,
"fund_max_dd": -0.1297444134289042,
"first": "2007-09-05",
"verdict": "weak/unstable alpha"
},
"rymfx": {
"sym": "rymfx",
"name": "Guggenheim Managed Futures Strategy Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": -0.02014247769464265,
"alpha_t_5y": -0.5003121906583069,
"alpha_t_full": 0.10839288377627028,
"r2_5y": 0.35143615168816555,
"r2_full": 0.07807781174439732,
"corr_portfolio": 0.23525317740277807,
"corr_benchmark": 0.07780897382137142,
"alpha_pos_frac": 0.47085201793721976,
"fund_max_dd": -0.36522050666552897,
"first": "2007-02-23",
"verdict": "weak/unstable alpha"
},
"seiax": {
"sym": "seiax",
"name": "SIIT Multi-Asset Real Return Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.035435990088980525,
"alpha_t_5y": 3.1751919551670036,
"alpha_t_full": 2.1185230216684676,
"r2_5y": 0.7773777017443931,
"r2_full": 0.7252216131385265,
"corr_portfolio": 0.3128935530074972,
"corr_benchmark": 0.2406739900426457,
"alpha_pos_frac": 0.5144508670520231,
"fund_max_dd": -0.21536400628633312,
"first": "2011-08-10",
"verdict": "weak/unstable alpha"
},
"sliyx": {
"sym": "sliyx",
"name": "SIMT MULTI-ASSET INCOME FUND",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.015465333823961345,
"alpha_t_5y": 1.9739121355197624,
"alpha_t_full": 1.6812343716635079,
"r2_5y": 0.8231097368330949,
"r2_full": 0.7872854440391012,
"corr_portfolio": 0.2924883461170799,
"corr_benchmark": 0.6656446172919034,
"alpha_pos_frac": 0.5597014925373134,
"fund_max_dd": -0.22093172752186285,
"first": "2015-01-02",
"verdict": "weak/unstable alpha"
},
"smuyx": {
"sym": "smuyx",
"name": "SIMT Multi-Strategy Alternative Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.01998789450507779,
"alpha_t_5y": 1.777909283290362,
"alpha_t_full": 1.8693972743108902,
"r2_5y": 0.6752006687187604,
"r2_full": 0.5298642820854836,
"corr_portfolio": 0.2651429895547443,
"corr_benchmark": 0.4228461393590128,
"alpha_pos_frac": 0.43846153846153846,
"fund_max_dd": -0.10995859828985377,
"first": "2015-05-01",
"verdict": "weak/unstable alpha"
},
"sryrx": {
"sym": "sryrx",
"name": "SIMT Real Return Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.017901995011180328,
"alpha_t_5y": 2.262183111453143,
"alpha_t_full": 2.911802667812134,
"r2_5y": 0.6291427249170225,
"r2_full": 0.568808359358441,
"corr_portfolio": 0.018063742050577297,
"corr_benchmark": 0.46374202412222304,
"alpha_pos_frac": 0.4626865671641791,
"fund_max_dd": -0.06547787926424076,
"first": "2015-01-02",
"verdict": "weak/unstable alpha"
},
"vmnix": {
"sym": "vmnix",
"name": "Vanguard Market Neutral Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.12912065637455916,
"alpha_t_5y": 4.361786874321171,
"alpha_t_full": 2.5879167048776455,
"r2_5y": 0.029380364668611603,
"r2_full": -1.1102230246251565e-15,
"corr_portfolio": 0.3467529140312448,
"corr_benchmark": -0.018030253731384765,
"alpha_pos_frac": 0.5055762081784386,
"fund_max_dd": -0.25345525956682635,
"first": "1998-10-27",
"verdict": "alpha, but correlated with current portfolio"
},
"wmnux": {
"sym": "wmnux",
"name": "Westwood Alternative Income Fund",
"bucket": "external",
"in_portfolio": false,
"alpha_ann_5y": 0.03973682932057431,
"alpha_t_5y": 6.860324585370505,
"alpha_t_full": 5.647006753747289,
"r2_5y": 0.3807595020726805,
"r2_full": 0.1737006681781419,
"corr_portfolio": 0.09371513014491326,
"corr_benchmark": 0.17652624930520902,
"alpha_pos_frac": 0.5076923076923077,
"fund_max_dd": -0.07640266654441785,
"first": "2015-05-04",
"verdict": "CANDIDATE - idiosyncratic alpha, complements portfolio"
}
}

View File

@ -305,6 +305,39 @@ def test_search() -> None:
check("ticker regex slash format", t2 == ["FMSDX"], str(t2))
def test_universe() -> None:
print("edgar universe cover parser", flush=True)
from fundlab import edgar_universe as eu
sample = (
"<SERIES-AND-CLASSES-CONTRACTS-DATA>\n"
"<SERIES>\n<SERIES-NAME>1290 Multi-Alternative Strategies Fund\n"
"<CLASS-CONTRACT>\n<CLASS-CONTRACT-NAME>Class A\n"
"<CLASS-CONTRACT-TICKER-SYMBOL>TNMAX\n</CLASS-CONTRACT>\n"
"<CLASS-CONTRACT-NAME>Class I\n"
"<CLASS-CONTRACT-TICKER-SYMBOL>TNMIX\n</CLASS-CONTRACT>\n"
"</SERIES>\n"
"<SERIES-NAME>1290 High Yield Bond Fund\n"
"<CLASS-CONTRACT-TICKER-SYMBOL>TNHAX\n</CLASS-CONTRACT>\n"
"</SERIES-AND-CLASSES-CONTRACTS-DATA>\n"
"<OTHER>JUNK-TICKER-LINE\n" # after the series block: ignored
)
s = eu.parse_cover(sample)
check("two series", len(s) == 2, str(s))
check("series 1 name", s[0]["name"] ==
"1290 Multi-Alternative Strategies Fund", s[0]["name"])
check("series 1 tickers", s[0]["tickers"] == ["TNMAX", "TNMIX"],
str(s[0]["tickers"]))
check("series 2 tickers", s[1]["tickers"] == ["TNHAX"],
str(s[1]["tickers"]))
check("ticker before any series ignored",
eu.parse_cover("<CLASS-CONTRACT-TICKER-SYMBOL>NOPE\n") == [],
"")
check("malformed ticker rejected",
eu.parse_cover("<SERIES-NAME>F\n"
"<CLASS-CONTRACT-TICKER-SYMBOL>1BAD\n")[0]
["tickers"] == [], "")
def test_curated() -> None:
print("curated", flush=True)
import fundlab.fundinfo as fi
@ -327,6 +360,7 @@ def main() -> int:
test_nport()
test_decompose()
test_search()
test_universe()
test_curated()
test_edgar_live()
print(f"\n{PASS} passed, {FAIL} failed")