Commit Graph

23 Commits

Author SHA1 Message Date
19b33d5a18 Add NIIT + state/local rates to the after-tax model (NYC support)
NYC residents pay, on a capital-gain distribution, roughly 20% federal
+ 3.8% NIIT + 6.85-9.65% NY + 3.876% NYC = ~34-37% — the old model's
flat 20% understated the real after-tax drag of high-distribution
funds for this user by ~15 points on exactly the flows that matter.

tax.py: new niit + sl_rate params (decimals). sl_rate is the
state+local marginal rate applied at ORDINARY rates to every flow —
state and local have NO preferential cap-gain rate, so the composite
is lt_rate+niit+sl_rate on cap-gain dists, div_rate+niit+sl_rate on
dividends, and (st/lt_rate)+niit+sl_rate on realized gains.

app.py: two new sidebar fields (persisted in settings.json), wired
through _compute_portfolio's cache key.

tests/test_tax.py: 3 new cases (capg and div composite rates,
net-taxed realized ST gains/losses at st+NIIT+SL).

README: NYC rate note with the 2025 IT-201 schedule values (NYC 3.876%
over $50k; NY 6.85% at $215,400-$1.077M, 9.65% at $1.077M-$5M,
single filer) and the composite example.
2026-09-01 09:37:58 -04:00
576d9fba97 Fix after-tax model: raw prices + reinvestment (was 2.7x over-taxing)
The old model valued holdings in ADJ (total-return index) units but
computed distribution flows as raw per-share dollars — so every
distribution, and its tax, was overstated by the raw/adj ratio
(JLPSX: 30.10/11.25 = 2.7x; the Dec-2020 cap-gain tax showed as 12.1%
of the account instead of the true 4.5%). The wiggle in the after-tax
curve was this bug, not a convention issue.

tax.py now:
- values holdings in RAW share units on close prices (bundle.close);
- receives the per-share distribution on its event date, pays the tax
  (recorded in TaxResult.taxes), and reinvests the after-tax remainder
  at the same day's raw close — the tax's effect lives in the (smaller)
  reinvested units and is NOT also deducted from cash (double-count
  caught and fixed in review);
- recomputes market value after the reinvestment so equity[t] is the
  post-event liquidation value.

With the fix, the 'as-if-liquidated' equity on JLPSX's ex-div day drops
by exactly the true tax cost (4.72% vs 12.1% before); the -22.9% price
drop is offset by the distribution kept.

Also:
- app.py passes bundle.close to the after-tax model (pre-tax
  portfolio_returns still uses adj);
- JLPSX/JLPYX: the 2020-12-11 6.824 capital-gain distribution is moved
  to the true ex-div date 2020-12-14 (remove/add correction ops), so
  the reinvestment prices at the post-drop close;
- tests/test_tax.py: 8 synthetic regression tests (tax magnitude,
  reinvestment MV, no double-count, ex-div equity step, per-component
  rates); run_tests.sh now runs it.
2026-08-31 23:58:15 -04:00
2bbe4e58ec Style-tilt battery + commentary in the fund report pipeline
fundlab/styletilt.py: 22 style/asset sleeves regressed on excess-of-T-bill
returns (full history + 5y); BIC forward selection identifies the tilt
stack; residual-alpha verdict ('factor exposure, not skill' when t<1.75);
data-driven English commentary with sign-specific phrasing. Rendered as a
'Style tilts' block (factor table + prose) in both the app and the HTML
report. All 24 pre-built + 8 ad-hoc fund reports rebuilt.
2026-08-30 20:36:04 -04:00
d2ff607c79 On-demand per-fund reports for standalone Symbol-box funds
Any symbol that stands alone in the Symbol box (not a comma-joined
portfolio component) now gets a full report, built on demand and cached
in reports/report_data_adhoc.json. Rendered at the top of the Fund Lab
Summary as group 'A'; pointer at the top of the page. Rendering logic
extracted into render_fund_report() shared by pre-built and on-demand
entries. ~0.3-0.9 s per new fund after one-time panel warmup; instant
afterwards (memory + disk cache).
2026-08-30 18:35:42 -04:00
e28f8fcf85 Report charts: re-basing-on-zoom widget (same as Equity tab); layout fixes
- per-fund report charts now use chart_widget.equity_chart_html
  (st.iframe) instead of static st.plotly_chart: every visible window
  re-bases each line to 1.0 at its left edge, so fund/reference/index
  compare in any zoom level - same semantics as the Equity curves tab
- removed 'expand all' checkbox (24 expanded sections drowned the page)
- added 'rest of Fund Lab continues below' marker after the report
  section; the other Fund Lab sections (alpha search, clusters, N-PORT,
  drawdown, tax, CEF) were never deleted and are unchanged
2026-08-30 18:15:11 -04:00
3fbf332b31 Per-fund report: app Summary page + narrative engine + mix_series beta fix
- fundlab/narrative.py: data-driven English prose per fund (performance,
  drivers tiered by fit, explicit 'what we do NOT know', bottom line)
- fundlab/reportdata.py: static build -> reports/report_data.json
- app.py Fund Lab Summary: at-a-glance table + per-fund expanders
  (narrative, equity curve, period table with fund-ref gap, drivers,
  reference mix, tax, cluster peers)
- fundlab/report.py: narrative in the HTML report; forward-selected
  reference (weak-fit funds anchor to cash); SLEEVE_DESC exposure
  explanations
- BUG: mix_series() never applied the betas (reference curves were raw
  sleeve sums; JLPSX 'reference' +407% vs fund +123%) - fixed and all
  reference curves/tables regenerated
- reports/fund_report.html + report_data.json regenerated
2026-08-30 17:36:58 -04:00
89674c24dd Compute all alphas in excess of the 3-mo T-bill rate (BIL)
Raw-intercept alphas absorbed the T-bill yield on uninvested/levered
portions (582 well-fitted funds >2%/yr off; sum-of-betas polluted by
level-matching). Now fund AND sleeves are netted against BIL daily
total return before every regression; a cash position contributes
exactly zero.

- decompose: rf_series()/excess(); shv+bil dropped from regressors
  (~0 columns in excess space); FULL_WINDOW -> 2007-06-01 (BIL
  inception; mixing raw pre-2007 with excess breaks the fit).
- factors: same excess treatment; shv out of DRIVERS.
- CASH axis redefined: alpha/cash_yield -> net cash position = 1 -
  sum(betas) (label 'cash (net posn)').
- CANDIDATE list 250 -> 11: the old list was mostly under-invested
  funds whose 'alpha' was cash yield, not skill.
- refback.py: per-fund fitted reference (forward-selected sleeves)
  stored as ref_5y/ref_full in search_all.json; app alpha-search
  table gains a 'reference (5y)' column - the answer to 'what is
  alpha computed against' (the fund's OWN fitted sleeve mix, not one
  index).
- App captions updated; raw-alpha-era results backed up as
  *_rawalpha.json (not deleted).
2026-08-30 15:50:15 -04:00
9f666538c3 Add CASH axis: money-market/cash-equivalents as a return driver
A fund's yield is a near-constant in return space, so OLS puts it in
the intercept (alpha_ann), not in the shv/bil betas (which only
measure rate-CHANGE sensitivity). Cash funds therefore showed no
dominant driver.

- factors: cash_yield() (shv trailing-1y total return = local
  stand-in for the 13-wk T-bill / 0-3m Treasury index), AXES =
  DRIVERS + ['cash'] (alpha_ann / cash_yield, 1.0 = earns the cash
  rate); a display/clustering axis, not a regressor.
- cluster: cash column in the loading matrix; k-means distances use
  the cash-2x-emphasized matrix (a pure cash fund's level axis was
  otherwise swallowed by the low-exposure cloud); labels use raw
  values; label() cash-aware (0.25 threshold, 1.5x runner-up).
- app: cluster view over AXES with the emphasized distance.

The 331-fund 'no dominant driver' grab-bag now splits into
'cash (yield) +0.64' (n=187, MM + ultra-short) and 'cash (yield)
+1.26' (n=59 pure). 123/123 tests.
2026-08-30 13:27:41 -04:00
6f6d6b8047 CEF app tab: full 239-fund verified tax-arb ranking 2026-08-28 16:04:59 -04:00
81ef602d52 CEF app tab + server watchdog + full-universe verification batch
- Fund Lab tab: CEF tax-arb shortlist table (char actual-vs-model,
  discount, 5y return/vol, scenario hits, tenders, score) built from
  cef_character.json + cef_annual.json.
- fundlab/server_watchdog.sh: relaunches Streamlit on :8599 if the
  health endpoint stops answering (the old watchdog only covered the
  overnight screen and had exited).
- fundlab/cef_universe_run.py: resumable batch extending per-share
  verification to all 295 CEFs -> cef_annual_all.json.
2026-08-28 14:57:01 -04:00
895efc9bff Correct ROC placement: ROC defers to the investor's LTCG, like
appreciation - a taxable-account feature, not an IRA one

Correction after user pushback. The earlier note "ROC does not help
the taxable case - the deferral replicates the IRA" was WRONG: a
traditional IRA defers to the ORDINARY rate at withdrawal, while ROC
in a taxable account defers to the LTCG rate on a >1y sale (the
distribution is basis-reducing and reappears inside the shareholder's
own capital gain). Under the premise LTCG rate < future ordinary
rate, ROC - like NAV appreciation - favors the taxable account.
Contrast ordinary income (interest, ordinary divs, STCG): taxed at
the ordinary rate in BOTH accounts, so only the IRA's deferral wins.

taxplan.py:
- _deferred_share(): per fund, share of 5y total return that defers
  to the investor = (NAV change + ROC) / total return, from the
  parsed per-share N-CSR table, max'd with the taxsplit appreciation
  share.
- >= 50% deferred -> location "TAXABLE (defers to LTCG)" (renamed
  from "TAXABLE (accrues)"); 10-50% ROC in distributions -> note.
- Merger-arb cap 0.35 -> 0.50: HMEZX's per-share table (52% NII /
  30% gains / 18% ROC over 5y) refutes "mostly STCG" - HMEZX/MERVX
  are now MIXED (check 1099), not clean IRA.
- RESEARCH.md: corrected placement write-up, incl. the distinction
  between the tax question and the fund-quality question (heavy ROC
  can mean principal erosion - PGSIX NAV -34%/5y - which affects
  selection, not the optimal account).

App: order dict + filter updated to the new location name.
Tests: merger-arb expectation updated to MIXED; 2 new checks for
the ROC upgrade/note logic. 99/99 fundlab + 32/32 app.
2026-08-27 17:11:55 -04:00
9458e316cb Price-appreciation vs payout split (fundlab/taxsplit.py)
Follow-up to the tax-location plan: the taxplan score only measured
DISTRIBUTION character. The user rightly noted that NAV appreciation
is also a capital gain (LTCG on a >1y sale). The fund price files
carry both series - Close = raw NAV with distributions paid out,
Adj Close = total return reinvested - so the split is computable
directly per fund (5y window + most-recent-12m payout).

Findings:
- ACCUMULATORS (>=50% of 5y return is price appreciation) get a new
  location "TAXABLE (accrues)": MBXIX 76% (0% payout 12m), ATESX 66%,
  LAMHX 62%, CVSIX 61%, candidate PBAIX 60% (0% payout 12m). For
  these the taxable account's LTCG-on-sale benefit is the dominant
  tax event.
- PAY-OUT funds: HMEZX (99% of return distributed - the STCG merger-
  arb case), MERVX, COSIX, PMORX, SVARX, SCFZX, DMSZX, munis, credit.
  IRA placement stands.
- Data artifacts caught: JLPSX/QSPNX one-time NAV gap events ~2022
  (special distribution or reorg) skew the 5y payout average; the
  12m payout column reflects current behavior. QCMMRX (MMF) series
  is not NAV-based - flagged.

App: tax-location expander gains 5y price / 5y payout / 12m payout
columns and the "TAXABLE (accrues)" filter. RESEARCH.md documents
the capital-loss question: registered RICs cannot distribute net
capital losses; the usable benefit is the fund's internal harvest
reserve (low capital-gain distributions after up-years), which needs
N-CSR/1099 history to verify. 97/32 suites green.
2026-08-27 14:34:42 -04:00
8a9ca25750 Tax-location plan: taxable account vs IRA per fund
fundlab/taxplan.py categorizes the 16-fund shortlist, the 22 N-PORT
cross-checked candidates, and all 250 screened candidates by the
expected CHARACTER of their distributions, given the user's premise
that the current LTCG rate < the post-retirement ordinary rate:

  qualified div + LTCG      -> TAXABLE  (score >= 0.60)
  tax-exempt (munis)        -> TAXABLE
  ordinary / STCG / REIT    -> IRA      (score <= 0.35)
  in between                -> MIXED (pull the 1099-DIV)
  cash                      -> FLEXIBLE

score = estimated share of distributions that are tax-favorable,
from three tiers of ground truth: N-PORT keyword buckets (16), SEC
assetCat/issuerCat buckets (22), sleeve loadings (250), with a
sleeve fallback when the keyword parser left >50% of a book
unclassified, and a manual override for the Leuthold wrappers
(91.7% Leuthold Core ETF, no return history yet).

Key findings:
- shortlist: TAXABLE = ATESX, JLPSX, LAMHX, LCORX, LCRIX (equity);
  IRA = ATRFX, COSIX, CVSIX, PMORX, SVARX, EAGMX/EGRSX;
  MIXED = MBXIX, QSPNX, PMAIX/PMFKX (same fund, two classes)
- cross-checked: 4 munis -> TAXABLE; HMEZX + MERVX are the merger-
  arb trap - equity-looking books whose distributions are mostly
  SHORT-TERM gains -> IRA
- candidates: 109 munis TAXABLE, 127 IRA, 6 equity TAXABLE, 7 MIXED

App: Fund Lab "Tax location" expander. Output:
fundlab/taxplan_results.json. Tests: test_taxplan() (9 checks).
97/32 suites green.
2026-08-27 14:08:15 -04:00
d0ae2ec348 Drawdown-resilience screen: who was positive when equities crashed
fundlab/drawdown.py detects the severe equity drawdown scenarios from
the index (IVV) rather than hard-coding them: one worst peak->trough
per calendar year since 2022, min depth 8% (a 10% floor would silently
drop the 2023 rate shock at -9.9% and the 2024 Aug-5 dip at -8.4%).
Detected: 2022 bear mkt (-24.5%), 2023 rate shock (-9.9%), 2024 vol
spike (-8.4%), 2025 tariff crash (-18.8%), 2026 Q1 drawdown (-8.9%).

For each of the 2,384 screened funds it computes that fund's own-NAV
return over each peak->trough window (first print after the peak to
the last print on/before the trough) and ranks the 250 CANDIDATEs by
# scenarios positive.

Key finding: positive in all 5 scenarios = only 7 funds, all
ultra-short/cash (BILS, QCMMRX, PULS, FHCOX, FHMIX, SAFEX, COIAX).
Drawdown resilience at the top tier is a duration property, not alpha.
The interesting tier is 4/5 WITH real 5y alpha: HMEZX merger arb
(+1.5% 2022, +3.1% 2023, t5 +7.1), MERVX, CBHCX market-neutral, SCFZX
securitized credit (t5 +8.4), ENIAX (t5 +10.1), WMNUX (t5 +6.9), RCTIX.

App: Fund Lab "Drawdown resilience" expander (scenario table +
candidate table). Output: fundlab/drawdown_results.json.
Tests: test_drawdown() added (4 checks). 88/32 suites green.
2026-08-27 13:29:01 -04:00
a09861f39f N-PORT cross-check: verify top candidates' actual holdings
fundlab/xcheck.py - for each screen candidate, resolve the fund's OWN
registrant CIK (browse-edgar; the 497-cover CIK is the family/trust),
get the exact series name for the ticker (the only reliable
disambiguator between sibling funds), walk the 4 most recent NPORT-P
filing dates, and parse holdings from the interactive NPORT XML
(primary_doc.xml at the accession root - NOT the XSL-rendered view the
submissions API points at). Exact seriesName match > best htm exhibit
parse. Buckets from the authoritative assetCat+issuerCat codes (ABS-O,
ABS-CBDO, DBT+UST/CORP/MUN/NUSS, LON, STIV, RA, EC+RF=fund, ...), not
position-name keywords. Resumable; raw filings cached under
nport_cache/raw/ (gitignored).

nport.py - _SECTION gains the "INVESTMENT PORTFOLIO (unaudited)"
variant (NPORT-EX Sch-F files); find_section/build gain a frac
token-tolerance param (Yahoo names drift from filing names); CMBS/ABS
bucket gains CLO/CBDO/DAC terms.

app Fund Lab - "N-PORT cross-check" expander: per-candidate table
(as-of, n, t5, top code-bucket, #1 position) + per-fund holdings
detail.

RESEARCH.md - cross-check verdicts. 21/22 resolved (qcmmrx is an MMF,
no holdings). The screen's top names are REAL:
- hmezx/mervx = genuine merger arb (equity in deal targets + escrow)
- egrix = 100% wrapper in one macro managed portfolio (underlying not
  NPORT-disclosed); etsix = fund of EV internal multi-strat accounts
- wmnux = discounted/zero-coupon corporate bonds + equity swaps (the
  "equity names" are bond issuers/swap underlyings)
- scfzx/rctix/aflix = securitized credit/CLO/distressed/levered loans
- hicox/fhmix/usmsx/btmix (munis), aguax/femdx (EM sovereign), anglx
  (agency MBS), lpxax (rotated out of prefs into bank/financial debt)
  = genuine missing-factor exposures the 35-sleeve model lacks
- fhcox/dultx/safex = short-duration carry (a short-duration sleeve
  would explain them)

tests/test_fundlab.py - test_xcheck (14 checks): parse_interactive,
code buckets, name-match normalization, series-name disambiguation.
Also: untrack fundlab/streamlit.log; gitignore raw/ + xcheck_run.log.

84 fundlab / 32 app / 14 data tests pass.
2026-08-27 12:43:17 -04:00
f68b239b9a Factor screen (v2, 35 drivers) + return-driver clusters
factors.py: full OLS of all 2,384 funds on a 35-driver basis
(overinclusive, no portfolio-corr screening - corr is a replacement
signal, not a rejection). v1's 21 + lqd/hyg/prefs/emb/tip/shy/vtv/
8 sectors/CTA/commodities. Basis fixes: drop vea/vug (dupes of
efa/qqq), drop finux (TERMINATED 2017 - silently zeroed the
complete-case mask; v1 forward selection never hit this), drop bil
(shv/bil near-null -> offsetting shv+64/bil-54 noise fits),
residualize vblix on ivv+tlt (pure vol axis), ridge 0.02.

cluster.py: hierarchical tree saved but fixed-k cuts degenerate
(most funds are blends -> one 2250-fund blob); k-means++ (deterministic)
is the useful grouping, re-run live in-app for any k.

app: Return-driver clusters expander (k slider 10-60, summary table,
member table sorted by alpha-t). Findings at k=30 in RESEARCH.md.
2026-08-27 08:35:42 -04:00
a94eac6545 Overnight comprehensive screen: drop the name pre-filter, screen all 2,384 funds
fundlab/RESEARCH.md - running research log: sources that work/die
  (full-index = discovery workhorse; browse-edgar JS-dead;
  investment-company-tickers.json nonexistent; company_tickers.json
  useless for OTC; Yahoo crumb throttled but chart API fine), 13
  hard-won learnings (OTC funds report exchange 'Nasdaq' -> use
  instrumentType; 497 SGML cover uses UNCLOSED line-based tags ->
  parse before tag-stripping; full-index columns drift -> regex the
  line; one quarter != universe -> 4-qtr union; accession paths
  relative to /Archives/ not /Archives/edgar/data/; family CIKs
  repeat -> dedupe by series name; portfolio is 50% MN so MN alpha
  funds are 'correlated', not diversifying).

fundlab/overnight.py - resumable all-stage pipeline (kill/restart safe):
  verify (Yahoo chart per non-local ticker, 4-thread, 429 backoff,
  local tickers measured from CSV row counts) -> select (pure
  select_rows: MUTUALFUND, >=5y, one longest-history class per series
  name, alpha_name as TAG not filter) -> download (goget in 200-sym
  batches) -> screen (streamed, skip-already-done) -> finalize
  (verdict counts + candidates the v1 name-filter would have missed).

Universe: 10,372 class tickers -> 10,260 verified -> 2,384 funds
(407 local, 1,977 external; only 54 match the alpha name pattern -
the v2 point is to screen the other 2,330).

app: alpha table now dedupes by sym with search_all.json winning
(comprehensive superset).
tests: select_rows unit tests (ETF drop, short-history drop, class
collapse, name tagging). 70/70 fundlab.
2026-08-26 21:33:11 -04:00
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
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
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