From 592d12958ffb8f095353397d1537cb3f6104c9b8 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 26 Aug 2026 15:22:52 -0400 Subject: [PATCH] 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 ( ... unclosed 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. --- .gitignore | 1 + app.py | 3 +- fundlab/dbmine.py | 3 +- fundlab/edgar_universe.py | 227 ++++++++++ fundlab/search_external.json | 784 +++++++++++++++++++++++++++++++++++ tests/test_fundlab.py | 34 ++ 6 files changed, 1050 insertions(+), 2 deletions(-) create mode 100644 fundlab/edgar_universe.py create mode 100644 fundlab/search_external.json diff --git a/.gitignore b/.gitignore index d443ec3..a122371 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ settings.json funds.json fundlab/nport_cache/*.html +fundlab/universe_cache/ diff --git a/app.py b/app.py index b567dbc..153b328 100644 --- a/app.py +++ b/app.py @@ -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: diff --git a/fundlab/dbmine.py b/fundlab/dbmine.py index b5af314..d62d4df 100644 --- a/fundlab/dbmine.py +++ b/fundlab/dbmine.py @@ -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( diff --git a/fundlab/edgar_universe.py b/fundlab/edgar_universe.py new file mode 100644 index 0000000..3cc4115 --- /dev/null +++ b/fundlab/edgar_universe.py @@ -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 + (fund name) and (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//.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: +# 1290 GAMCO Small/Mid Cap Value Fund +# 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: + 1290 Multi-Alternative Strategies Fund + TNMAX + Each ticker attaches to the most recent . + """ + series: list[dict] = [] + cur: dict | None = None + for line in text.splitlines(): + if line.startswith(""): + cur = {"name": line[len(""):].strip(), + "tickers": []} + series.append(cur) + elif line.startswith("") \ + and cur is not None: + t = line[len(""):] + 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) diff --git a/fundlab/search_external.json b/fundlab/search_external.json new file mode 100644 index 0000000..1eac4f8 --- /dev/null +++ b/fundlab/search_external.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/test_fundlab.py b/tests/test_fundlab.py index 0fd7527..8e3b552 100644 --- a/tests/test_fundlab.py +++ b/tests/test_fundlab.py @@ -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 = ( + "\n" + "\n1290 Multi-Alternative Strategies Fund\n" + "\nClass A\n" + "TNMAX\n\n" + "Class I\n" + "TNMIX\n\n" + "\n" + "1290 High Yield Bond Fund\n" + "TNHAX\n\n" + "\n" + "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("NOPE\n") == [], + "") + check("malformed ticker rejected", + eu.parse_cover("F\n" + "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")