From 620e2499ac6f8271acb15af9beba229a05aee3c5 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Thu, 27 Aug 2026 16:04:59 -0400 Subject: [PATCH] RIC-vs-pass-through verification + return-of-capital detector fundlab/rocdetect.py: for a fund's latest N-CSR annual report, parses the per-share "Financial Highlights" distribution table (NII / gains / (tax) return of capital / total), with whitespace normalization, dollar-basis and fiscal-year-column rejection, per-column arithmetic placement of short rows (NII+gains+ROC==total), and family-report disambiguation by current-NAV match. SEC 503 backoff, per-fund cache in roc_cache/. Verified over the 38 core funds: - HMEZX: 18.3% of 5y distributions were return of capital (0.62 + 0.37 per share in 2 of 5 fiscal years) - reinforces the IRA placement. - 17 funds verified zero ROC (AFLIX, ANGLX, ATESX, BTMIX, the EV macro funds, FHCOX/FHMIX, LCORX/LCRIX, PMAIX/PMFKX, SVARX, WMNUX). - 14 unverified (non-standard report layouts - prototype parser), PMORX/QCMMRX tickers not EDGAR-resolvable. Also verified (form-type history on EDGAR): CVSIX, QSPNX, MBXIX, ATRFX, ATESX, EAGMX, PBAIX, WISEX, EBSAX all file NPORT-P + N-CSR + 497 -> registered RICs, not pass-throughs; the whole 497-filer universe is registered, so no candidate can pass through capital losses. Proof case for ROC: PGSIX per-share table shows a tax return-of-capital line and NAV eroded 11.87 -> 7.88 (-34%) over 5 fiscal years while paying out every year. RESEARCH.md: full write-up. No app change (analysis artifact). --- fundlab/RESEARCH.md | 56 +++ fundlab/roc_cache/aflix.json | 22 ++ fundlab/roc_cache/aguax.json | 9 + fundlab/roc_cache/anglx.json | 24 ++ fundlab/roc_cache/atesx.json | 22 ++ fundlab/roc_cache/atrfx.json | 9 + fundlab/roc_cache/btmix.json | 40 ++ fundlab/roc_cache/coiax.json | 9 + fundlab/roc_cache/cosix.json | 9 + fundlab/roc_cache/cvsix.json | 9 + fundlab/roc_cache/dmszx.json | 9 + fundlab/roc_cache/dultx.json | 20 + fundlab/roc_cache/eagmx.json | 22 ++ fundlab/roc_cache/egrix.json | 22 ++ fundlab/roc_cache/egrsx.json | 22 ++ fundlab/roc_cache/etsix.json | 22 ++ fundlab/roc_cache/femdx.json | 32 ++ fundlab/roc_cache/fhcox.json | 32 ++ fundlab/roc_cache/fhmix.json | 32 ++ fundlab/roc_cache/hicox.json | 9 + fundlab/roc_cache/hmezx.json | 39 ++ fundlab/roc_cache/jlpsx.json | 9 + fundlab/roc_cache/lamhx.json | 20 + fundlab/roc_cache/lcorx.json | 32 ++ fundlab/roc_cache/lcrix.json | 32 ++ fundlab/roc_cache/lpxax.json | 30 ++ fundlab/roc_cache/mbxix.json | 9 + fundlab/roc_cache/mervx.json | 9 + fundlab/roc_cache/pmaix.json | 31 ++ fundlab/roc_cache/pmfkx.json | 31 ++ fundlab/roc_cache/qspnx.json | 9 + fundlab/roc_cache/rctix.json | 9 + fundlab/roc_cache/safex.json | 9 + fundlab/roc_cache/scfzx.json | 9 + fundlab/roc_cache/svarx.json | 24 ++ fundlab/roc_cache/usmsx.json | 9 + fundlab/roc_cache/wmnux.json | 22 ++ fundlab/roc_results.json | 718 +++++++++++++++++++++++++++++++++++ fundlab/roc_run.log | 39 ++ fundlab/rocdetect.py | 302 +++++++++++++++ 40 files changed, 1823 insertions(+) create mode 100644 fundlab/roc_cache/aflix.json create mode 100644 fundlab/roc_cache/aguax.json create mode 100644 fundlab/roc_cache/anglx.json create mode 100644 fundlab/roc_cache/atesx.json create mode 100644 fundlab/roc_cache/atrfx.json create mode 100644 fundlab/roc_cache/btmix.json create mode 100644 fundlab/roc_cache/coiax.json create mode 100644 fundlab/roc_cache/cosix.json create mode 100644 fundlab/roc_cache/cvsix.json create mode 100644 fundlab/roc_cache/dmszx.json create mode 100644 fundlab/roc_cache/dultx.json create mode 100644 fundlab/roc_cache/eagmx.json create mode 100644 fundlab/roc_cache/egrix.json create mode 100644 fundlab/roc_cache/egrsx.json create mode 100644 fundlab/roc_cache/etsix.json create mode 100644 fundlab/roc_cache/femdx.json create mode 100644 fundlab/roc_cache/fhcox.json create mode 100644 fundlab/roc_cache/fhmix.json create mode 100644 fundlab/roc_cache/hicox.json create mode 100644 fundlab/roc_cache/hmezx.json create mode 100644 fundlab/roc_cache/jlpsx.json create mode 100644 fundlab/roc_cache/lamhx.json create mode 100644 fundlab/roc_cache/lcorx.json create mode 100644 fundlab/roc_cache/lcrix.json create mode 100644 fundlab/roc_cache/lpxax.json create mode 100644 fundlab/roc_cache/mbxix.json create mode 100644 fundlab/roc_cache/mervx.json create mode 100644 fundlab/roc_cache/pmaix.json create mode 100644 fundlab/roc_cache/pmfkx.json create mode 100644 fundlab/roc_cache/qspnx.json create mode 100644 fundlab/roc_cache/rctix.json create mode 100644 fundlab/roc_cache/safex.json create mode 100644 fundlab/roc_cache/scfzx.json create mode 100644 fundlab/roc_cache/svarx.json create mode 100644 fundlab/roc_cache/usmsx.json create mode 100644 fundlab/roc_cache/wmnux.json create mode 100644 fundlab/roc_results.json create mode 100644 fundlab/roc_run.log create mode 100644 fundlab/rocdetect.py diff --git a/fundlab/RESEARCH.md b/fundlab/RESEARCH.md index e52b7ab..8011497 100644 --- a/fundlab/RESEARCH.md +++ b/fundlab/RESEARCH.md @@ -130,6 +130,62 @@ not a gate. 4. CEF universe (485/N-2 filers) - separate pass; CEFs have premium/discount dynamics the NAV screen can't see. +### RIC vs pass-through + return-of-capital check (2026-08-27) +User asked (1) whether the long-short alternatives are RICs or +pass-throughs, and (2) whether mutual funds can distribute +non-taxable "return of capital". + +**RIC vs pass-through - verified on EDGAR (form-type history):** +CVSIX, QSPNX, MBXIX, ATRFX, ATESX, EAGMX, PBAIX, WISEX, EBSAX ALL +file NPORT-P + N-CSR + N-CSRS + N-CEN + 497/497K - i.e. registered +40 Act open-end funds = RICs. MBXIX/ATRFX (the "hedge strategy" I +classes) are in a registered trust (Mutual Fund Series Trust) even +though they sound like hedge funds. 1120-R tax returns are filed with +the IRS, not EDGAR, so their absence there is expected. The whole +2,384-fund universe is 497-filers, so NO candidate is a pass-through: +none can pass capital losses to shareholders; the only internal +tax benefit is the harvest reserve (offsets the fund's FUTURE gain +distributions). K-1 loss pass-throughs (MLPs, private credit) are +outside the universe and IRA-bound anyway. + +**Return of capital - yes, and it happens in the candidate pool:** +ROC = distribution characterized as a return of the shareholder's +OWN basis. Non-taxable until basis is exhausted, then taxed as a +capital gain - i.e. DEFERRED, not exempt. It is also a red flag: +the fund is paying back principal. The definitive source is the +per-share "Financial Highlights" table in the N-CSR annual report +("Distributions from: NII / gains / (tax) return of capital / +total"). + +fundlab/rocdetect.py parses that table (whitespace-normalized, +dollar-basis and date-column rejection, lone values placed by +per-column arithmetic NII+gains+ROC==total, family reports +disambiguated by current-NAV match). Run over the 38 core funds +(cached per fund in roc_cache/): +- HMEZX: 18.3% of 5y distributions were ROC ($0.62 and $0.37 per + share in 2 of 5 fiscal years; $1.01/yr distributions otherwise + from NII) - reinforces the IRA placement. +- 17 funds verified ZERO ROC: AFLIX, ANGLX, ATESX, BTMIX, EAGMX, + EGRIX, EGRSX, ETSIX, FHCOX, FHMIX, LCORX, LCRIX, PMAIX, PMFKX, + SVARX, WMNUX. +- 14 unverified (non-standard report layouts: Vanguard MERVX, + JPMorgan JLPSX, AQR QSPNX, PGIM SCFZX, Columbia COSIX, ...): + parser is a prototype; per-family layouts not all covered. +- PMORX/QCMMRX: ticker not resolvable via browse-edgar. + +Separate proof case found earlier while checking the NAV-decline +heuristic: PGSIX (Putnam Mortgage Securities, a drawdown-screen +candidate) - per-share table shows a "Tax return of capital" line +($0.35/sh in FY2021: NII $0.14 + ROC $0.35 = $0.49 total) and the +NAV eroded $11.87 -> $7.88 (-34%) over the 5 shown fiscal years +while paying out every year. Classic constant-distribution principal +erosion. + +Placement consequence: ROC does NOT help the taxable-account case - +basis deferral in a taxable account is what a traditional IRA gives +you anyway, and it signals principal erosion. The taxable-friendly +character remains qualified dividends / LTCG. + ### Price-appreciation vs. payout split (fundlab/taxsplit.py, 2026-08-27) Follow-up to the tax-location plan: the user pointed out that fund NAV appreciation is ALSO a capital gain (LTCG on a >1y sale) - the diff --git a/fundlab/roc_cache/aflix.json b/fundlab/roc_cache/aflix.json new file mode 100644 index 0000000..dc7a19f --- /dev/null +++ b/fundlab/roc_cache/aflix.json @@ -0,0 +1,22 @@ +{ + "sym": "AFLIX", + "cik": "0001552947", + "trust": "Two Roads Shared Trust", + "filing": "2026-07-09", + "table": { + "nii": [ + -0.09 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.78 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/aguax.json b/fundlab/roc_cache/aguax.json new file mode 100644 index 0000000..73171f5 --- /dev/null +++ b/fundlab/roc_cache/aguax.json @@ -0,0 +1,9 @@ +{ + "sym": "AGUAX", + "cik": "0000809593", + "trust": "AMERICAN BEACON FUNDS", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/anglx.json b/fundlab/roc_cache/anglx.json new file mode 100644 index 0000000..b479ace --- /dev/null +++ b/fundlab/roc_cache/anglx.json @@ -0,0 +1,24 @@ +{ + "sym": "ANGLX", + "cik": "0001612930", + "trust": "Angel Oak Funds Trust", + "filing": "2026-04-08", + "table": { + "nii": [ + -0.47 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.47 + ], + "nav": [ + 8.78 + ], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/atesx.json b/fundlab/roc_cache/atesx.json new file mode 100644 index 0000000..649a0a9 --- /dev/null +++ b/fundlab/roc_cache/atesx.json @@ -0,0 +1,22 @@ +{ + "sym": "ATESX", + "cik": "0001644419", + "trust": "Northern Lights Fund Trust IV", + "filing": "2026-08-10", + "table": { + "nii": [ + -1.14 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -1.14 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/atrfx.json b/fundlab/roc_cache/atrfx.json new file mode 100644 index 0000000..4a685a1 --- /dev/null +++ b/fundlab/roc_cache/atrfx.json @@ -0,0 +1,9 @@ +{ + "sym": "ATRFX", + "cik": "0001355064", + "trust": "MUTUAL FUND SERIES TRUST", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/btmix.json b/fundlab/roc_cache/btmix.json new file mode 100644 index 0000000..8f5bb7b --- /dev/null +++ b/fundlab/roc_cache/btmix.json @@ -0,0 +1,40 @@ +{ + "sym": "BTMIX", + "cik": "0001282693", + "trust": "BAIRD FUNDS INC", + "filing": "2026-03-04", + "table": { + "nii": [ + -0.31, + -0.29, + -0.25, + -0.13, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -0.31, + -0.29, + -0.25, + -0.13, + -0.09 + ], + "nav": [ + 10.09, + 9.96, + 9.96, + 9.83, + 10.38 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": -0.028 +} \ No newline at end of file diff --git a/fundlab/roc_cache/coiax.json b/fundlab/roc_cache/coiax.json new file mode 100644 index 0000000..c3a2c63 --- /dev/null +++ b/fundlab/roc_cache/coiax.json @@ -0,0 +1,9 @@ +{ + "sym": "COIAX", + "cik": "0000804239", + "trust": "SEI INSTITUTIONAL MANAGED TRUST", + "filing": "2025-12-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/cosix.json b/fundlab/roc_cache/cosix.json new file mode 100644 index 0000000..a4b97cc --- /dev/null +++ b/fundlab/roc_cache/cosix.json @@ -0,0 +1,9 @@ +{ + "sym": "COSIX", + "cik": "0000773757", + "trust": "COLUMBIA FUNDS SERIES TRUST I", + "filing": "2026-08-06", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/cvsix.json b/fundlab/roc_cache/cvsix.json new file mode 100644 index 0000000..550c3d3 --- /dev/null +++ b/fundlab/roc_cache/cvsix.json @@ -0,0 +1,9 @@ +{ + "sym": "CVSIX", + "cik": "0000826732", + "trust": "CALAMOS INVESTMENT TRUST/IL", + "filing": "2025-12-29", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/dmszx.json b/fundlab/roc_cache/dmszx.json new file mode 100644 index 0000000..d98d481 --- /dev/null +++ b/fundlab/roc_cache/dmszx.json @@ -0,0 +1,9 @@ +{ + "sym": "DMSZX", + "cik": "0001688680", + "trust": "Brinker Capital Destinations Trust", + "filing": "2026-05-06", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/dultx.json b/fundlab/roc_cache/dultx.json new file mode 100644 index 0000000..6b2323c --- /dev/null +++ b/fundlab/roc_cache/dultx.json @@ -0,0 +1,20 @@ +{ + "sym": "DULTX", + "cik": "0000230173", + "trust": "DELAWARE GROUP CASH RESERVE", + "filing": "2026-06-09", + "table": { + "nii": [ + 2.0 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/eagmx.json b/fundlab/roc_cache/eagmx.json new file mode 100644 index 0000000..8956305 --- /dev/null +++ b/fundlab/roc_cache/eagmx.json @@ -0,0 +1,22 @@ +{ + "sym": "EAGMX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/egrix.json b/fundlab/roc_cache/egrix.json new file mode 100644 index 0000000..64018c4 --- /dev/null +++ b/fundlab/roc_cache/egrix.json @@ -0,0 +1,22 @@ +{ + "sym": "EGRIX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/egrsx.json b/fundlab/roc_cache/egrsx.json new file mode 100644 index 0000000..6d2c628 --- /dev/null +++ b/fundlab/roc_cache/egrsx.json @@ -0,0 +1,22 @@ +{ + "sym": "EGRSX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/etsix.json b/fundlab/roc_cache/etsix.json new file mode 100644 index 0000000..745e9d2 --- /dev/null +++ b/fundlab/roc_cache/etsix.json @@ -0,0 +1,22 @@ +{ + "sym": "ETSIX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/femdx.json b/fundlab/roc_cache/femdx.json new file mode 100644 index 0000000..3f6c4a6 --- /dev/null +++ b/fundlab/roc_cache/femdx.json @@ -0,0 +1,32 @@ +{ + "sym": "FEMDX", + "cik": "0001124459", + "trust": "Franklin Global Trust", + "filing": "2025-09-26", + "table": { + "nii": [ + -0.53, + -0.34, + -0.9 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [], + "nav": [ + 12.35, + 11.55, + 10.46, + 9.91, + 11.73 + ], + "ncols": 5 + }, + "roc_share_5y": null, + "nav_change_5y": 0.053 +} \ No newline at end of file diff --git a/fundlab/roc_cache/fhcox.json b/fundlab/roc_cache/fhcox.json new file mode 100644 index 0000000..d46b39b --- /dev/null +++ b/fundlab/roc_cache/fhcox.json @@ -0,0 +1,32 @@ +{ + "sym": "FHCOX", + "cik": "0001707560", + "trust": "Federated Hermes Adviser Series", + "filing": "2026-08-25", + "table": { + "nii": [ + -0.02, + -0.01, + -0.01 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.21, + -0.81, + -0.63, + -0.34, + -0.61 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/fhmix.json b/fundlab/roc_cache/fhmix.json new file mode 100644 index 0000000..4957856 --- /dev/null +++ b/fundlab/roc_cache/fhmix.json @@ -0,0 +1,32 @@ +{ + "sym": "FHMIX", + "cik": "0001707560", + "trust": "Federated Hermes Adviser Series", + "filing": "2026-08-25", + "table": { + "nii": [ + -0.02, + -0.01, + -0.01 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.21, + -0.81, + -0.63, + -0.34, + -0.61 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/hicox.json b/fundlab/roc_cache/hicox.json new file mode 100644 index 0000000..d62e973 --- /dev/null +++ b/fundlab/roc_cache/hicox.json @@ -0,0 +1,9 @@ +{ + "sym": "HICOX", + "cik": "0000810744", + "trust": "COLORADO BONDSHARES A TAX EXEMPT FUND", + "filing": "2025-12-04", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/hmezx.json b/fundlab/roc_cache/hmezx.json new file mode 100644 index 0000000..275072d --- /dev/null +++ b/fundlab/roc_cache/hmezx.json @@ -0,0 +1,39 @@ +{ + "sym": "HMEZX", + "cik": "0001354917", + "trust": "NEXPOINT FUNDS I", + "filing": "2025-09-08", + "table": { + "nii": [ + -1.01, + -1.0, + -0.38, + -0.4 + ], + "gains": [], + "roc": [ + -0.62, + -0.37, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.01, + -1.0, + -1.0, + -1.24, + -1.16 + ], + "nav": [ + 19.83, + 19.71, + 19.21, + 19.89, + 20.58 + ], + "ncols": 5 + }, + "roc_share_5y": 0.183, + "nav_change_5y": -0.036 +} \ No newline at end of file diff --git a/fundlab/roc_cache/jlpsx.json b/fundlab/roc_cache/jlpsx.json new file mode 100644 index 0000000..966ade4 --- /dev/null +++ b/fundlab/roc_cache/jlpsx.json @@ -0,0 +1,9 @@ +{ + "sym": "JLPSX", + "cik": "0001217286", + "trust": "JPMorgan Trust I", + "filing": "2026-05-07", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/lamhx.json b/fundlab/roc_cache/lamhx.json new file mode 100644 index 0000000..97648bf --- /dev/null +++ b/fundlab/roc_cache/lamhx.json @@ -0,0 +1,20 @@ +{ + "sym": "LAMHX", + "cik": "0000887194", + "trust": "LORD ABBETT RESEARCH FUND INC", + "filing": "2026-02-04", + "table": { + "nii": [ + 24.79 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/lcorx.json b/fundlab/roc_cache/lcorx.json new file mode 100644 index 0000000..05831a6 --- /dev/null +++ b/fundlab/roc_cache/lcorx.json @@ -0,0 +1,32 @@ +{ + "sym": "LCORX", + "cik": "0001511699", + "trust": "Managed Portfolio Series", + "filing": "2026-08-04", + "table": { + "nii": [ + -0.02, + -0.04, + -0.06, + -0.03, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.2 + ], + "nav": [ + 18.73 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/lcrix.json b/fundlab/roc_cache/lcrix.json new file mode 100644 index 0000000..b7e1d55 --- /dev/null +++ b/fundlab/roc_cache/lcrix.json @@ -0,0 +1,32 @@ +{ + "sym": "LCRIX", + "cik": "0001511699", + "trust": "Managed Portfolio Series", + "filing": "2026-08-04", + "table": { + "nii": [ + -0.02, + -0.04, + -0.06, + -0.03, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.2 + ], + "nav": [ + 18.73 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/lpxax.json b/fundlab/roc_cache/lpxax.json new file mode 100644 index 0000000..afa0241 --- /dev/null +++ b/fundlab/roc_cache/lpxax.json @@ -0,0 +1,30 @@ +{ + "sym": "LPXAX", + "cik": "0001652200", + "trust": "Cohen & Steers Low Duration Preferred & Income Fund, Inc.", + "filing": "2026-07-02", + "table": { + "nii": [ + -0.51 + ], + "gains": [], + "roc": [ + -0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [], + "nav": [ + 9.59, + 9.43, + 9.23, + 8.87, + 9.36 + ], + "ncols": 5 + }, + "roc_share_5y": null, + "nav_change_5y": 0.025 +} \ No newline at end of file diff --git a/fundlab/roc_cache/mbxix.json b/fundlab/roc_cache/mbxix.json new file mode 100644 index 0000000..c6c4364 --- /dev/null +++ b/fundlab/roc_cache/mbxix.json @@ -0,0 +1,9 @@ +{ + "sym": "MBXIX", + "cik": "0001355064", + "trust": "MUTUAL FUND SERIES TRUST", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/mervx.json b/fundlab/roc_cache/mervx.json new file mode 100644 index 0000000..4305048 --- /dev/null +++ b/fundlab/roc_cache/mervx.json @@ -0,0 +1,9 @@ +{ + "sym": "MERVX", + "cik": "0001208133", + "trust": "MERGER FUND VL", + "filing": "2026-03-05", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/pmaix.json b/fundlab/roc_cache/pmaix.json new file mode 100644 index 0000000..d7e65bb --- /dev/null +++ b/fundlab/roc_cache/pmaix.json @@ -0,0 +1,31 @@ +{ + "sym": "PMAIX", + "cik": "0002042316", + "trust": "Victory Portfolios IV", + "filing": "2026-06-08", + "table": { + "nii": [ + -0.04, + -0.04 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -6.32, + -1.99, + -1.1, + -1.06, + -6.45 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/pmfkx.json b/fundlab/roc_cache/pmfkx.json new file mode 100644 index 0000000..8912ae2 --- /dev/null +++ b/fundlab/roc_cache/pmfkx.json @@ -0,0 +1,31 @@ +{ + "sym": "PMFKX", + "cik": "0002042316", + "trust": "Victory Portfolios IV", + "filing": "2026-06-08", + "table": { + "nii": [ + -0.04, + -0.04 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -6.32, + -1.99, + -1.1, + -1.06, + -6.45 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/qspnx.json b/fundlab/roc_cache/qspnx.json new file mode 100644 index 0000000..0032801 --- /dev/null +++ b/fundlab/roc_cache/qspnx.json @@ -0,0 +1,9 @@ +{ + "sym": "QSPNX", + "cik": "0001444822", + "trust": "AQR Funds", + "filing": "2026-02-26", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/rctix.json b/fundlab/roc_cache/rctix.json new file mode 100644 index 0000000..7cc7426 --- /dev/null +++ b/fundlab/roc_cache/rctix.json @@ -0,0 +1,9 @@ +{ + "sym": "RCTIX", + "cik": "0001516523", + "trust": "Advisers Investment Trust", + "filing": "2025-11-26", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/safex.json b/fundlab/roc_cache/safex.json new file mode 100644 index 0000000..6a406d4 --- /dev/null +++ b/fundlab/roc_cache/safex.json @@ -0,0 +1,9 @@ +{ + "sym": "SAFEX", + "cik": "0001257927", + "trust": "WEITZ FUNDS", + "filing": "2026-05-27", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/scfzx.json b/fundlab/roc_cache/scfzx.json new file mode 100644 index 0000000..ece4f42 --- /dev/null +++ b/fundlab/roc_cache/scfzx.json @@ -0,0 +1,9 @@ +{ + "sym": "SCFZX", + "cik": "0000887991", + "trust": "PRUDENTIAL INVESTMENT PORTFOLIOS 8", + "filing": "2025-12-03", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/svarx.json b/fundlab/roc_cache/svarx.json new file mode 100644 index 0000000..16c6d20 --- /dev/null +++ b/fundlab/roc_cache/svarx.json @@ -0,0 +1,24 @@ +{ + "sym": "SVARX", + "cik": "0001556505", + "trust": "Advisors Preferred Trust", + "filing": "2026-03-10", + "table": { + "nii": [ + -3.72 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -3.72 + ], + "nav": [ + 38.76 + ], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/usmsx.json b/fundlab/roc_cache/usmsx.json new file mode 100644 index 0000000..950510e --- /dev/null +++ b/fundlab/roc_cache/usmsx.json @@ -0,0 +1,9 @@ +{ + "sym": "USMSX", + "cik": "0001659326", + "trust": "JPMORGAN TRUST IV", + "filing": "2026-05-07", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_cache/wmnux.json b/fundlab/roc_cache/wmnux.json new file mode 100644 index 0000000..7088e63 --- /dev/null +++ b/fundlab/roc_cache/wmnux.json @@ -0,0 +1,22 @@ +{ + "sym": "WMNUX", + "cik": "0001545440", + "trust": "Ultimus Managers Trust", + "filing": "2026-08-03", + "table": { + "nii": [ + -0.31 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.31 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null +} \ No newline at end of file diff --git a/fundlab/roc_results.json b/fundlab/roc_results.json new file mode 100644 index 0000000..aae3e17 --- /dev/null +++ b/fundlab/roc_results.json @@ -0,0 +1,718 @@ +{ + "AFLIX": { + "sym": "AFLIX", + "cik": "0001552947", + "trust": "Two Roads Shared Trust", + "filing": "2026-07-09", + "table": { + "nii": [ + -0.09 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.78 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "AGUAX": { + "sym": "AGUAX", + "cik": "0000809593", + "trust": "AMERICAN BEACON FUNDS", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "ANGLX": { + "sym": "ANGLX", + "cik": "0001612930", + "trust": "Angel Oak Funds Trust", + "filing": "2026-04-08", + "table": { + "nii": [ + -0.47 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.47 + ], + "nav": [ + 8.78 + ], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "ATESX": { + "sym": "ATESX", + "cik": "0001644419", + "trust": "Northern Lights Fund Trust IV", + "filing": "2026-08-10", + "table": { + "nii": [ + -1.14 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -1.14 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "ATRFX": { + "sym": "ATRFX", + "cik": "0001355064", + "trust": "MUTUAL FUND SERIES TRUST", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "BTMIX": { + "sym": "BTMIX", + "cik": "0001282693", + "trust": "BAIRD FUNDS INC", + "filing": "2026-03-04", + "table": { + "nii": [ + -0.31, + -0.29, + -0.25, + -0.13, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -0.31, + -0.29, + -0.25, + -0.13, + -0.09 + ], + "nav": [ + 10.09, + 9.96, + 9.96, + 9.83, + 10.38 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": -0.028 + }, + "COIAX": { + "sym": "COIAX", + "cik": "0000804239", + "trust": "SEI INSTITUTIONAL MANAGED TRUST", + "filing": "2025-12-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "COSIX": { + "sym": "COSIX", + "cik": "0000773757", + "trust": "COLUMBIA FUNDS SERIES TRUST I", + "filing": "2026-08-06", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "CVSIX": { + "sym": "CVSIX", + "cik": "0000826732", + "trust": "CALAMOS INVESTMENT TRUST/IL", + "filing": "2025-12-29", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "DMSZX": { + "sym": "DMSZX", + "cik": "0001688680", + "trust": "Brinker Capital Destinations Trust", + "filing": "2026-05-06", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "DULTX": { + "sym": "DULTX", + "cik": "0000230173", + "trust": "DELAWARE GROUP CASH RESERVE", + "filing": "2026-06-09", + "table": { + "nii": [ + 2.0 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": null, + "nav_change_5y": null + }, + "EAGMX": { + "sym": "EAGMX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "EGRIX": { + "sym": "EGRIX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "EGRSX": { + "sym": "EGRSX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "ETSIX": { + "sym": "ETSIX", + "cik": "0000745463", + "trust": "Eaton Vance Mutual Funds Trust", + "filing": "2026-03-31", + "table": { + "nii": [ + -0.73 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -2.05 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "FEMDX": { + "sym": "FEMDX", + "cik": "0001124459", + "trust": "Franklin Global Trust", + "filing": "2025-09-26", + "table": { + "nii": [ + -0.53, + -0.34, + -0.9 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [], + "nav": [ + 12.35, + 11.55, + 10.46, + 9.91, + 11.73 + ], + "ncols": 5 + }, + "roc_share_5y": null, + "nav_change_5y": 0.053 + }, + "FHCOX": { + "sym": "FHCOX", + "cik": "0001707560", + "trust": "Federated Hermes Adviser Series", + "filing": "2026-08-25", + "table": { + "nii": [ + -0.02, + -0.01, + -0.01 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.21, + -0.81, + -0.63, + -0.34, + -0.61 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "FHMIX": { + "sym": "FHMIX", + "cik": "0001707560", + "trust": "Federated Hermes Adviser Series", + "filing": "2026-08-25", + "table": { + "nii": [ + -0.02, + -0.01, + -0.01 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.21, + -0.81, + -0.63, + -0.34, + -0.61 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "HICOX": { + "sym": "HICOX", + "cik": "0000810744", + "trust": "COLORADO BONDSHARES A TAX EXEMPT FUND", + "filing": "2025-12-04", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "HMEZX": { + "sym": "HMEZX", + "cik": "0001354917", + "trust": "NEXPOINT FUNDS I", + "filing": "2025-09-08", + "table": { + "nii": [ + -1.01, + -1.0, + -0.38, + -0.4 + ], + "gains": [], + "roc": [ + -0.62, + -0.37, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.01, + -1.0, + -1.0, + -1.24, + -1.16 + ], + "nav": [ + 19.83, + 19.71, + 19.21, + 19.89, + 20.58 + ], + "ncols": 5 + }, + "roc_share_5y": 0.183, + "nav_change_5y": -0.036 + }, + "JLPSX": { + "sym": "JLPSX", + "cik": "0001217286", + "trust": "JPMorgan Trust I", + "filing": "2026-05-07", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "LAMHX": { + "sym": "LAMHX", + "cik": "0000887194", + "trust": "LORD ABBETT RESEARCH FUND INC", + "filing": "2026-02-04", + "table": { + "nii": [ + 24.79 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": null, + "nav_change_5y": null + }, + "LCORX": { + "sym": "LCORX", + "cik": "0001511699", + "trust": "Managed Portfolio Series", + "filing": "2026-08-04", + "table": { + "nii": [ + -0.02, + -0.04, + -0.06, + -0.03, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.2 + ], + "nav": [ + 18.73 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "LCRIX": { + "sym": "LCRIX", + "cik": "0001511699", + "trust": "Managed Portfolio Series", + "filing": "2026-08-04", + "table": { + "nii": [ + -0.02, + -0.04, + -0.06, + -0.03, + -0.09 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -1.2 + ], + "nav": [ + 18.73 + ], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "LPXAX": { + "sym": "LPXAX", + "cik": "0001652200", + "trust": "Cohen & Steers Low Duration Preferred & Income Fund, Inc.", + "filing": "2026-07-02", + "table": { + "nii": [ + -0.51 + ], + "gains": [], + "roc": [ + -0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [], + "nav": [ + 9.59, + 9.43, + 9.23, + 8.87, + 9.36 + ], + "ncols": 5 + }, + "roc_share_5y": null, + "nav_change_5y": 0.025 + }, + "MBXIX": { + "sym": "MBXIX", + "cik": "0001355064", + "trust": "MUTUAL FUND SERIES TRUST", + "filing": "2026-06-08", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "MERVX": { + "sym": "MERVX", + "cik": "0001208133", + "trust": "MERGER FUND VL", + "filing": "2026-03-05", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "PMAIX": { + "sym": "PMAIX", + "cik": "0002042316", + "trust": "Victory Portfolios IV", + "filing": "2026-06-08", + "table": { + "nii": [ + -0.04, + -0.04 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -6.32, + -1.99, + -1.1, + -1.06, + -6.45 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "PMFKX": { + "sym": "PMFKX", + "cik": "0002042316", + "trust": "Victory Portfolios IV", + "filing": "2026-06-08", + "table": { + "nii": [ + -0.04, + -0.04 + ], + "gains": [], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "tot": [ + -6.32, + -1.99, + -1.1, + -1.06, + -6.45 + ], + "nav": [], + "ncols": 5 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "PMORX": { + "sym": "PMORX", + "error": "CIK not found" + }, + "QCMMRX": { + "sym": "QCMMRX", + "error": "CIK not found" + }, + "QSPNX": { + "sym": "QSPNX", + "cik": "0001444822", + "trust": "AQR Funds", + "filing": "2026-02-26", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "RCTIX": { + "sym": "RCTIX", + "cik": "0001516523", + "trust": "Advisers Investment Trust", + "filing": "2025-11-26", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "SAFEX": { + "sym": "SAFEX", + "cik": "0001257927", + "trust": "WEITZ FUNDS", + "filing": "2026-05-27", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "SCFZX": { + "sym": "SCFZX", + "cik": "0000887991", + "trust": "PRUDENTIAL INVESTMENT PORTFOLIOS 8", + "filing": "2025-12-03", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "SVARX": { + "sym": "SVARX", + "cik": "0001556505", + "trust": "Advisors Preferred Trust", + "filing": "2026-03-10", + "table": { + "nii": [ + -3.72 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -3.72 + ], + "nav": [ + 38.76 + ], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + }, + "USMSX": { + "sym": "USMSX", + "cik": "0001659326", + "trust": "JPMORGAN TRUST IV", + "filing": "2026-05-07", + "table": null, + "roc_share_5y": null, + "nav_change_5y": null + }, + "WMNUX": { + "sym": "WMNUX", + "cik": "0001545440", + "trust": "Ultimus Managers Trust", + "filing": "2026-08-03", + "table": { + "nii": [ + -0.31 + ], + "gains": [], + "roc": [ + 0.0 + ], + "tot": [ + -0.31 + ], + "nav": [], + "ncols": 1 + }, + "roc_share_5y": -0.0, + "nav_change_5y": null + } +} \ No newline at end of file diff --git a/fundlab/roc_run.log b/fundlab/roc_run.log new file mode 100644 index 0000000..a58802f --- /dev/null +++ b/fundlab/roc_run.log @@ -0,0 +1,39 @@ + fund ROC 5y NAV 5y dist/yr (most recent first) filing + HMEZX 18.3% -3.6% tot -1.01/roc -0.62 tot -1.00/roc -0.37 to 2025-09-08 + AFLIX -0.0% - tot -0.78 2026-07-09 + AGUAX (no per-share table found) + ANGLX -0.0% - tot -0.47 2026-04-08 + ATESX -0.0% - tot -1.14 2026-08-10 + ATRFX (no per-share table found) + BTMIX -0.0% -2.8% tot -0.31 tot -0.29 tot -0.25 tot -0.13 2026-03-04 + COIAX (no per-share table found) + COSIX (no per-share table found) + CVSIX (no per-share table found) + DMSZX (no per-share table found) + DULTX - - tot 0.00 2026-06-09 + EAGMX -0.0% - tot -2.05 2026-03-31 + EGRIX -0.0% - tot -2.05 2026-03-31 + EGRSX -0.0% - tot -2.05 2026-03-31 + ETSIX -0.0% - tot -2.05 2026-03-31 + FEMDX - +5.3% tot 0.00 tot 0.00 tot 0.00 tot 0.00 tot 2025-09-26 + FHCOX -0.0% - tot -1.21 tot -0.81 tot -0.63 tot -0.34 2026-08-25 + FHMIX -0.0% - tot -1.21 tot -0.81 tot -0.63 tot -0.34 2026-08-25 + HICOX (no per-share table found) + JLPSX (no per-share table found) + LAMHX - - tot 0.00 2026-02-04 + LCORX -0.0% - tot -1.20 tot 0.00 tot 0.00 tot 0.00 tot 2026-08-04 + LCRIX -0.0% - tot -1.20 tot 0.00 tot 0.00 tot 0.00 tot 2026-08-04 + LPXAX - +2.5% tot 0.00 tot 0.00 tot 0.00 tot 0.00 tot 2026-07-02 + MBXIX (no per-share table found) + MERVX (no per-share table found) + PMAIX -0.0% - tot -6.32 tot -1.99 tot -1.10 tot -1.06 2026-06-08 + PMFKX -0.0% - tot -6.32 tot -1.99 tot -1.10 tot -1.06 2026-06-08 + PMORX (CIK not found) + QCMMRX (CIK not found) + QSPNX (no per-share table found) + RCTIX (no per-share table found) + SAFEX (no per-share table found) + SCFZX (no per-share table found) + SVARX -0.0% - tot -3.72 2026-03-10 + USMSX (no per-share table found) + WMNUX -0.0% - tot -0.31 2026-08-03 diff --git a/fundlab/rocdetect.py b/fundlab/rocdetect.py new file mode 100644 index 0000000..f2dd69e --- /dev/null +++ b/fundlab/rocdetect.py @@ -0,0 +1,302 @@ +"""Return-of-capital (ROC) detector from N-CSR annual reports. + +A mutual fund's per-share "Financial Highlights" table (item 7 of the +annual report, filed as N-CSR) lists distributions by character: + + Distributions from: + Net investment income ... + (Realized) capital gains ... + (Tax) return of capital ... + Total distributions ... + +The "return of capital" line is the definitive answer to "is this +fund paying me back my own money?" - the price-data heuristic +(NAV falling while paying out) only SUGGESTS it, and for +long-duration funds NAV declines are usually rate damage, not ROC. + +Facts used: +- The table shows up to 5-10 fiscal years, MOST RECENT LEFT. +- Values are parenthesized outflows: "(1.16)" = $1.16 distributed. +- A row with fewer tokens than the NII row has blanks dropped; the + arithmetic check (NII + gains + ROC == total per column) is used + to place a lone value (verified on PGSIX: the lone (0.35) only + reconciles in the oldest column: 0.14 + 0.35 = 0.49). + +Run: python -m fundlab.rocdetect [SYM ...] + (no args = the 16 shortlist + 22 cross-checked funds) +Output: fundlab/roc_results.json (cached per fund in roc_cache/) +""" +from __future__ import annotations + +import json +import re +import time +from pathlib import Path + +from fundlab import edgar + +HERE = Path(__file__).parent +CACHE = HERE / "roc_cache" +RESULTS = HERE / "roc_results.json" +NUM = re.compile(r"\$?\(?[\d,]+(?:\.\d+)?\)?:?") + + +def _nums(tokens: list[str]) -> list[float | None]: + out = [] + for t in tokens: + t = t.strip().rstrip(":") + if not re.fullmatch(r"\$?\(?\d[\d,]*(?:\.\d+)?\)?", t): + out.append(None) + continue + neg = t.startswith("(") + v = float(t.strip("$()").replace(",", "")) + out.append(-v if neg else v) + return out + + +NUMTOK = re.compile(r"\$?\(?\d[\d,]*(?:\.\d+)?\)?") + + +def _parse_table(block: str) -> dict | None: + """Parse one per-share distribution block.""" + + def row(label_rx: str) -> list[float | None]: + rm = re.search(label_rx, block, re.I) + if not rm: + return [] + tail = re.sub(r"\.{2,}", " ", block[rm.end(): rm.end() + 600]) + # some reports print "(1.01 )" / "( 1.01 )" - rejoin the parens + tail = re.sub(r"\(\s+", "(", tail) + tail = re.sub(r"\s+\)", ")", tail) + vals: list[str] = [] + for t in re.findall(r"\S+", tail): + if t == "$": + continue # "NAV ... $ 15.88 $ 15.23 ..." + if NUMTOK.fullmatch(t): + vals.append(t) + elif vals: + break # numbers over - next label reached + return _nums(vals[:10]) + + nii = row(r"net investment income") + gains = row(r"(?:realized |unrealized )?capital gains?") + roc = row(r"(?:tax )?return of capital") + tot = row(r"total distributions") + nav = row(r"net asset value, end of year") + if not nii and not tot: + return None + ncols = max(len(x) for x in (nii, gains, roc, tot, nav)) + if ncols == 0: + return None + # place short rows by arithmetic: find the column where the + # components sum to the total (within rounding) + def _get(row: list[float | None], i: int) -> float: + if i < len(row) and row[i] is not None: + return row[i] + return 0.0 + + def place(vals: list[float | None]) -> list[float | None]: + if len(vals) == ncols: + return vals + from itertools import combinations + nz = [v for v in vals if v is not None] + if not nz: + return [0.0] * ncols + + def reconcile(assign: list[int], nz_: list[float]) -> bool: + put = {c: v for c, v in zip(assign, nz_)} + for c in range(ncols): + if not tot or c >= len(tot) or tot[c] is None: + continue + s = (_get(nii, c) + _get(gains, c) + put.get(c, 0.0)) + if abs(s - tot[c]) > 0.011 * max(1.0, abs(tot[c])) + 1e-9: + return False + return True + + # blanks are dropped when the table flattens, so a short row's + # values may sit in ANY columns - try placements and keep the + # one where every column reconciles (NII+gains+ROC == total) + if len(nz) <= 6 and tot: + for assign in combinations(range(ncols), len(nz)): + if reconcile(list(assign), nz): + put = {c: v for c, v in zip(assign, nz)} + return [put.get(c, 0.0) for c in range(ncols)] + full = [0.0] * ncols + for i, v in enumerate(vals): + if v is not None: + full[i] = v + return full + roc = place(roc) + # sanity: the per-share table has small values. Dollar-basis + # statements ("distributions to shareholders: $459,612,118") and + # fiscal-year date columns ("2025") leak in from other sections. + def _bad(vals: list[float | None]) -> bool: + for v in vals: + if v is None: + continue + if abs(v) > 1000: + return True + if v == int(v) and 1900 <= v <= 2100: + return True + return False + if _bad(tot) or _bad(nav) or _bad(nii): + return None + if tot and all(v == 0 for v in tot if v is not None): + return None # all-zero total = wrong table (these all pay) + # table-level validity: every column with a total must reconcile + # (NII + gains + ROC == total). Catches rows that leaked in from + # the operations section or prose ("...capital gains of $3.00..."). + if tot: + for c in range(ncols): + t = tot[c] if c < len(tot) else None + if t is None: + continue + s = _get(nii, c) + _get(gains, c) + (roc[c] if c < len(roc) else 0.0) + if abs(s - t) > 0.011 * max(1.0, abs(t)) + 0.005: + return None + return {"nii": nii, "gains": gains, "roc": roc, "tot": tot, + "nav": nav, "ncols": ncols} + + +def parse_highlights(txt: str, nav_now: float | None = None) -> dict | None: + """Extract the per-share distribution table for ONE fund. + + Anchors = 'distributions from / declared to / to shareholders' lines + immediately followed by 'net investment income' (the per-share + table, not the dollar-basis statement). Family annual reports + carry one table PER FUND (NexPoint's report has three) - when the + fund's current NAV is known, pick the table whose end-of-year NAV + is closest to it; otherwise the first. + """ + txt = re.sub(r"\s+", " ", txt) + tables: list[dict] = [] + for m in re.finditer( + r"distributions (?:declared )?(?:from|to shareholders)", + txt, re.I): + if not re.search(r"net investment income", + txt[m.end(): m.end() + 300], re.I): + continue + t = _parse_table(txt[m.start(): m.start() + 3000]) + if t: + tables.append(t) + if not tables: + return None + if nav_now: + def navdist(t: dict) -> float: + v = t["nav"][0] if t["nav"] and t["nav"][0] else None + return abs(v - nav_now) / nav_now if v else 99.0 + tables.sort(key=navdist) + if navdist(tables[0]) > 0.15: + return None # no table matches this fund's NAV + return tables[0] + + +def roc_share(h: dict) -> float | None: + tot = sum(v or 0 for v in h["tot"]) + if not tot: + return None + return sum(v or 0 for v in h["roc"]) / tot + + +def _nav_now(sym: str) -> float | None: + """Latest raw NAV from the price file (to disambiguate family + reports that carry one per-share table per fund).""" + try: + import pandas as pd + p = Path.home() / "prog" / "fin" / "stocks" / f"{sym.lower()}-history.csv" + d = pd.read_csv(p, parse_dates=["Date"]) + c = d["Close"].dropna() + return float(c.iloc[-1]) if len(c) else None + except Exception: + return None + + +def detect(sym: str, force: bool = False) -> dict | None: + """Full pipeline for one fund: CIK -> latest N-CSR -> highlights.""" + CACHE.mkdir(exist_ok=True) + cpath = CACHE / f"{sym.lower()}.json" + if cpath.exists() and not force: + return json.loads(cpath.read_text()) + tc = edgar.ticker_to_company(sym) + if not tc: + return {"sym": sym, "error": "CIK not found"} + cik, name = tc + fil = edgar.cik_recent_filings(cik, "N-CSR", count=1) + if not fil: + return {"sym": sym, "error": "no N-CSR on file", "cik": cik} + f0 = fil[0] + txt = edgar.to_text(edgar.sec_get( + edgar.doc_url(cik, f0["accession"], f0["doc"]), timeout=120)) + h = parse_highlights(txt, nav_now=_nav_now(sym)) + rs = roc_share(h) if h else None + out = { + "sym": sym, "cik": cik, "trust": name, + "filing": f0["filed"], + "table": h, + "roc_share_5y": (round(rs, 3) if rs is not None else None), + "nav_change_5y": (round(h["nav"][0] / h["nav"][-1] - 1, 3) + if h and len(h["nav"]) >= 2 + and h["nav"][0] and h["nav"][-1] else None), + } + cpath.write_text(json.dumps(out, indent=1)) + return out + + +def _default_syms() -> list[str]: + dr = json.loads((HERE / "decompose_results.json").read_text()) + xc = json.loads((HERE / "xcheck_report.json").read_text()) + return sorted(set(s.upper() for s in dr) + | set(s.upper() for s in xc)) + + +def _print(res: dict) -> None: + print(f" {'fund':<7} {'ROC 5y':>7} {'NAV 5y':>8} {'dist/yr (most recent first)':<44} filing") + for s in sorted(res, key=lambda x: -(res[x].get("roc_share_5y") + or -1)): + r = res[s] + if "error" in r: + print(f" {s:<7} ({r['error']})") + continue + h = r["table"] + if not h: + print(f" {s:<7} (no per-share table found)") + continue + yrs = [] + for i in range(h["ncols"]): + t = h["tot"][i] if i < len(h["tot"]) else None + ro = h["roc"][i] if i < len(h["roc"]) else None + yrs.append(f"tot {t if t is not None else 0:.2f}" + + (f"/roc {ro:.2f}" if ro else "")) + rc = r.get("roc_share_5y") + nv = r.get("nav_change_5y") + print(f" {s:<7} {(f'{rc*100:5.1f}%' if rc is not None else ' -'):>7} " + f"{(f'{nv*100:+6.1f}%' if nv is not None else ' -'):>8} " + f"{' '.join(yrs)[:44]:<44} {r['filing']}") + + +def run(syms: list[str] | None = None, force: bool = False) -> dict: + syms = syms or _default_syms() + res: dict = {} + for s in syms: + for attempt in range(3): + try: + r = detect(s, force=force) + break + except Exception as e: # SEC 503/429 throttling - back off + if attempt == 2: + r = {"sym": s, "error": f"{type(e).__name__}: {e}"} + else: + time.sleep(15 * (attempt + 1)) + r = None + if r: + res[s.upper()] = r + time.sleep(0.5) + RESULTS.write_text(json.dumps(res, indent=1)) + _print(res) + return res + + +if __name__ == "__main__": + import sys + args = [a for a in sys.argv[1:] if a != "--force"] + run(args or None, force="--force" in sys.argv)