f/fundlab/pool.py
Greg Pomerantz e9f8dfc462 fundlab: EDGAR investment-objective fetcher + curated fallback
- edgar.py: SEC FTS + submissions API; strict cover-gate extraction
  (name in title position or (TICKER) on the cover; underlying-reference
  names like leveraged wrappers rejected); 4-pass fetch (ticker->CIK
  filings, name search, annual reports, ticker search); keyword category
  classifier. Returns None rather than a wrong fund's objective.
- fundinfo.py CLI: curated -> cached -> EDGAR resolution into funds.json
- funds_curated.json: human-verified objectives for 13 benchmark-pool
  funds (iShares/Vanguard family-trust classes the scraper can't reach)
- tests: 26 checks incl. live EDGAR fetch of VTSAX
2026-08-25 18:15:48 -04:00

50 lines
1.4 KiB
Python

"""Benchmark discovery pool.
A copy of ~/prog/fin/benchmarks.txt (the user's curated benchmark universe),
augmented with a few common index ETFs that exist in the data universe.
The ORIGINAL file outside this repository is never modified.
Format: section header lines (ending in ':' or free-standing), then
'symbol<tab or 2+ spaces>label' lines.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
POOL_FILE = Path(__file__).parent / "pool" / "benchmarks.txt"
_ENTRY = re.compile(r"^(\S{1,6})[\t ]{2,}(.+)$")
@dataclass(frozen=True)
class PoolEntry:
section: str
symbol: str # lowercase
label: str
def load_pool(path: Path = POOL_FILE) -> list[PoolEntry]:
entries: list[PoolEntry] = []
section = ""
for line in path.read_text().splitlines():
line = line.rstrip()
if not line.strip():
continue
m = _ENTRY.match(line)
if m:
entries.append(PoolEntry(section, m.group(1).lower(),
m.group(2).strip()))
else:
section = line.strip().rstrip(":")
return entries
def pool_labels(entries: list[PoolEntry] | None = None) -> dict[str, str]:
"""symbol -> 'section: label' (for keyword matching and display)."""
if entries is None:
entries = load_pool()
return {e.symbol: f"{e.section}: {e.label}" for e in entries if e.section}