Commit Graph

10 Commits

Author SHA1 Message Date
592d12958f 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.
2026-08-26 15:22:52 -04:00
afec7bda73 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
2026-08-26 13:34:34 -04:00
db5fc4626d Fund Lab: returns-based strategy decomposition for the 16 shortlist funds
- fundlab/decompose.py: per-fund OLS forward selection (BIC-gated, |t|>2,
  per-model complete cases so differently-vintaged candidates stay
  comparable) against curated DISTINCT-AXIS candidate sets; full-history
  + last-5y models; rolling 1y beta drift for static-vs-time-varying
  verdicts; per-fund holdings cross-check notes
- results (13 unique funds; pmfkx/lcrix/egrsx are share classes):
  * jlpsx  ~1.04x S&P 500, R2 0.96 5y  (cleanest)
  * lamhx  S&P + value/mid tilt, R2 0.95, stable
  * cosix  5y: HY +0.30 / MBS +0.29 / IG +0.18, R2 0.86
  * cvsix  market neutral, 5y R2 0.74, +5.5%/yr alpha (t 6.7)
  * pmaix  multi-asset: HY .62 / EFA .23 / comm .05 / bonds -.15
  * mbxix  hedge: ivv .39 / ief -.67 / fxe -.28, R2 0.53
  * atesx  NOT a static mix - rolling beta to its own QQQ/SPY holdings
            is 0.13-0.89 (median 0.30): the 'risk managed' overlay is real
  * qspnx/svarx/eagmx/atrfx/pmorx: market-neutral or idiosyncratic -
            alpha, not sleeves (qspnx +12.8%/yr alpha t 4.0)
  * lcorx/lcrix: new classes (Jul 2026), no history yet - holdings only
- atesx holdings: pulled from the adviser's SOI PDF (anchor-soi-5.31.26):
  QQQ 65.2% + SPY 29.3% + MMF 0.6%, options overlay 4.9%
- pool: added qqq (Nasdaq 100) - needed to fit tech-concentrated funds
- app Fund Lab tab: per-fund decomposition (verdict, R2 full/5y, alpha,
  tracking error, beta drift, component table + bar chart, holdings
  cross-check note) and an all-funds summary expander
- tests: ols/forward-select engine tests (50/50 fundlab)
2026-08-26 11:59:59 -04:00
54d26939dc Fund Lab: N-PORT holdings page for the 16-fund shortlist
- fundlab/nport.py: parse the fund's own category/percentage lines,
  as-of date, net assets and dollar-valued positions from N-PORT
  schedules of investments (handles per-fund and combined multi-fund
  family filings; conservative keyword bucketing of positions)
- fundlab/nport_cache/<sym>.json: parsed snapshots for 15 of 16 funds
  (raw SOI HTML kept locally, gitignored; source URLs + filing dates in
  nport_manifest.json, md5-verified against EDGAR)
- atesx: no current SOI found (Anchor's recent filings cover the Income
  fund) - listed with an honest note
- app.py: new 'Fund Lab' tab - pick a fund, see objective, reported
  composition (bar + table), rough keyword buckets, top positions, and
  the prospectus strategy excerpt
- tests: parser unit tests (section finding, category regex, buckets)
2026-08-26 09:59:24 -04:00
7b27920a77 fundlab: extract strategy sections from EDGAR docs; --strategy CLI mode
- extract_strategy(): finds the 'Principal Investment Strategies' /
  'main investment strategies' section, scores candidates (strategy prose
  +3, Q&A heading +2, TOC -5, risk subheading -5, stop-heading -2),
  truncates at the next section heading; falls back to the prose after
  the objective sentence when no heading exists
- fundinfo --strategy [--refresh]: populates the strategy field of
  funds.json from each fund's EDGAR document
- funds.json now carries objective + strategy for 20 funds (the 9
  curated index funds have no strategy: their objective is the strategy)
2026-08-25 23:06:13 -04:00
5c6d95a8b4 fundlab: actively-managed fund support (16/16 tickers)
- ticker -> CIK scoped FTS (ciks filter) over the fund's own registrant,
  word queries shed Yahoo abbreviations shortest-first
- fuzzy cover gate (SequenceMatcher, sliding window for wrapped names)
  + abbreviation table (Mgd/Glbl/Macr/Abs/Ret/Advtg/...) applied to both
  sides of the match
- family-section matching inside multi-fund filings; similarity tiers:
  gate (0.85) returns immediately, floor (0.70) is a CIK-scoped fallback
  ranked against sibling funds' documents (best sim, then newest)
- objective phrasings: 'seeks to ...', 'seeks investment results ...',
  'The Fund's investment objective is ...', '(the Fund) investment
  objective is ...' (boilerplate 'is not fundamental' rejected); TOC
  headings skipped by trying all heads
- sec_get retries on mid-stream connection drops
2026-08-25 22:10:02 -04:00
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
4f36bc7aea app: background cache refresh, per-benchmark stats, correlation tab, global date range
- data.py: non-blocking load_bundle(); background watcher thread refreshes
  the parquet cache (5s scan, 30s min rebuild cadence); refresh()/
  up_to_date()/generation()
- statistics tab: one table per benchmark (vs <label>), plain column names
  (beta/alpha/return/vol...), selectable+reorderable stat list in
  settings.json
- correlation tab: per-portfolio components-vs-benchmarks +
  all-portfolios-vs-benchmarks; numbered columns
- global date range (window radio + start/end boxes) applied to all tabs;
  metrics.xcorr(); equity window radio gains YTD/3M/1M
2026-08-25 18:15:47 -04:00
cba7291676 data: manifest-based incremental refresh of the parquet cache
The cache now tracks every file in the data dir (mtime_ns + size) in
.cache/manifest.json. On load, a directory scan is compared against the
manifest:
  - changed/added files are re-read and merged into the parquet panels
    (one read + one concat + one write per touched panel; new values
    win where present, old values kept where the new file is short)
  - removed files drop their symbols (and names)
  - an up-to-date cache is a ~30 ms memo hit

Measured on the real 4k-symbol set: full build 54 s, refresh of
5 modified + 1 added + 1 removed files 3.4 s. No scan TTL (a scan is
a few ms); a previous 5 s scan cache masked data updates.

Tests: tests/test_data.py (11 checks) added as step 1 of run_tests.sh.
2026-08-24 17:28:20 -04:00
d8703a7a63 Stock & Portfolio Analyzer: full UI rework
- single spec grammar for symbol and benchmark fields: commas join one
  portfolio (MSFT:0.6,V:0.4), spaces separate distinct symbols/portfolios;
  both fields accept one or many entries
- benchmarks simulated with the same scheme/cost/tax rules; per-benchmark
  beta/alpha columns; after-tax benchmark curves
- global Curve mode (pre/after/both) above the tabs; clean names in
  single-curve mode
- live updates: field commits on Enter/blur, page recomputes per rerun;
  portfolio+tax sims cached (st.cache_data); plotly.js from CDN (4.6MB ->
  browser-cached) with F_INLINE_PLOTLY=1 offline fallback
- chart: legend underneath, solid lines, pan sticks to data edges
  (width-preserving), zoom edge-clamped
- inputs persist in settings.json across reloads/restarts/devices
- tests: tests/test_app.py (AppTest) + tests/test_e2e_browser.py
  (Playwright) via ./run_tests.sh
2026-08-24 16:05:27 -04:00