diff --git a/fundlab/RESEARCH.md b/fundlab/RESEARCH.md index 75c873a..5f3b6f7 100644 --- a/fundlab/RESEARCH.md +++ b/fundlab/RESEARCH.md @@ -785,8 +785,54 @@ LENDX (0.69, maxDD -12%, all scenarios ~flat/positive), EMF/AEF/HQL/ HQH/IGD equity-character 0.55-0.75; EMO/NML/SRV/KYN/TYG char 0.29-0.39 (income-heavy -> IRA-tilted, ROC share pending the 1099). -**Remaining (stage 2b+):** per-fund N-2ASR/N-CSR distribution -character (the income/gains/ROC split is decisive for the EMO -family), NPORT NAV-per-share -> quarterly discount series, leverage -from NPORT financials, BDC flag via form history, tender-offer -status; app CEF tab. +**Stage 2b** (fundlab/cef_annual.py): per-share financial-highlights +parser for the annual shareholder report (N-CSR/N-CSRS). 16/50 of the +shortlist parsed (4 fund-family layouts verified: Franklin/EMO, +classic CEF/TWN, abrdn/HQL, Korea/KF; the rest reject conservatively +- mis-parse is worse than no data). + +Parser lessons (each one cost a debug cycle): +- CEF HTML tables flatten with one per line: labels split across + newlines ("Net\nasset value, beginning of year"). Collapse + whitespace BEFORE any regex. +- CEF tables DROP ZERO COLUMNS when flattened: a short row's values + can sit in ANY subset of columns (TWN: gains in 4 of 5 cols, + NII-divs in the OTHER two; EMO: ROC in cols 0 and 4; KF: accretion + in cols 1-4). A left-align or greedy per-row placement is WRONG. +- Robust solution: one JOINT backtracking search placing every short + row (captch rows first - they feed the NAV identity - then the + distribution rows) so the per-column arithmetic holds: + printed total complete -> divs+gains+roc == total; printed total + short -> divs+gains+roc == NAV_beg+ops+captch-NAV_end. Reject + (return None) when no assignment reconciles. +- The printed total row is authoritative when complete; sign varies + by fund (EMO positive, TWN negative) - normalize by majority vote. +- "Net asset value, beginning of **period**" (semiannual / LENDX) and + "per common share" (abrdn) label variants. +- "Total dividends and distributions to stockholders" total label + contains "dividends" - scope sub-row search to before the total row. +- "Less distributions from:" (Franklin, no "to") anchor variant. +- Prefer the 5-column ANNUAL table over a 6-column semiannual one. +- Submission .txt (full filing, one fetch) is the robust document + source; primaryDocument 404s on older filings. + +Verified results (div/gains/ROC 5y share, FY-end discount, tenders): +- TWN 6/94/0, -15.0% - 94% capital-gains distributions (LTCG + character for a long-term EM fund) + persistent 15% discount = + the cleanest taxable CEF of the set. +- KF 27/72/0, -14.4%; ADX 9/91/0, -5.7% - same story, smaller. +- EMO 67/0/33, -6.8% (was -18.4% in FY21: the discount NARROWED - + the actual tax arb) + 6 tender notices (repurchase program). + 81% of the FY25 distribution was ROC - tax-payer CEF, check 1099. +- AEF 20/0/80, -8.3%; HERZ 0/21/79 (char 0.94 BUT 5y total return + only +10%: ROC that large on flat returns = returning your own + capital, a sustainability red flag - ROC share alone is not + sufficient). +- LENDX 45/40/14, 40 tender notices (continuous program), maxDD -12%. +- AOD/AGD 81/0/19 (ordinary NII) at ~par - the IRA-tilted CEFs. +- Discount series are STABLE (no systematic narrowing) except EMO. + +**Remaining:** targeted regex work for the 34 un-parsed families +(Neuberger SRV/NML/KYN/TYG, Voya IGD/ECAT, Barings MCI/MPV, +Cornerstone, Virtus Stone Harbor, ...); leverage/tax-paying flags +(optionally, from the same doc); app CEF tab; BDC confirmation. diff --git a/fundlab/cef_annual.json b/fundlab/cef_annual.json new file mode 100644 index 0000000..606f9c2 --- /dev/null +++ b/fundlab/cef_annual.json @@ -0,0 +1,1446 @@ +{ + "LTCFX": { + "sym": "LTCFX", + "cik": 1496254, + "name": "Alternative Strategies Income Fund", + "bdc": false, + "n_tender": 60, + "ticker": "LTAFX", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "DXYZ": { + "sym": "DXYZ", + "cik": 1843974, + "name": "Destiny Tech100 Inc.", + "bdc": false, + "n_tender": 0, + "ticker": "DXYZ", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "TWN": { + "sym": "TWN", + "cik": 804123, + "name": "TAIWAN FUND INC", + "bdc": false, + "n_tender": 0, + "ticker": "TWN", + "report_filed": "2025-11-05", + "ncols": 5, + "nav_beg": [ + 53.78, + 39.73, + 29.96, + 42.83, + 28.79 + ], + "nav_end": [ + 59.13, + 53.78, + 39.73, + 29.96, + 42.83 + ], + "mkt_end": [ + 50.25, + 44.73, + 31.1, + 25.2, + 35.83 + ], + "dist_tot": [ + 7.399999999999999, + 0.4399999999999977, + 0.0, + 2.9199999999999946, + 3.3100000000000023 + ], + "divs": [ + 0.0, + 0.0, + 0.0, + -0.48, + -0.38 + ], + "gains": [ + -7.4, + -0.44, + 0.0, + -2.44, + -2.93 + ], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 11.92, + 13.56, + 9.73, + -9.95, + 17.35 + ], + "share_div": 0.061122956645344735, + "share_gains": 0.9388770433546558, + "share_roc": 0.0, + "char_actual": 0.66, + "disc_now_approx": -0.1502 + }, + "ASA": { + "sym": "ASA", + "cik": 1230869, + "name": "ASA Gold & Precious Metals Ltd", + "bdc": false, + "n_tender": 0, + "ticker": "ASA", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "PEO": { + "sym": "PEO", + "cik": 216851, + "name": "ADAMS NATURAL RESOURCES FUND, INC.", + "bdc": false, + "n_tender": 0, + "ticker": "PEO", + "report_filed": "2026-02-20", + "ncols": 5, + "nav_beg": [ + 24.21, + 24.83, + 25.85, + 19.22, + 13.76 + ], + "nav_end": [ + 24.09, + 24.21, + 24.83, + 25.85, + 19.22 + ], + "mkt_end": [ + 21.74, + 21.74, + 20.63, + 21.8, + 16.52 + ], + "dist_tot": [ + 2.05, + 1.77, + 1.35, + 1.63, + 0.91 + ], + "divs": [ + -0.56, + -0.65, + -0.65, + -0.79, + -0.56 + ], + "gains": [ + -1.49, + -1.12, + -0.7, + -0.84, + -0.35 + ], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 2.03, + 1.27, + 0.4, + 8.37, + 6.41 + ], + "share_div": 0.4163424124513619, + "share_gains": 0.5836575875486382, + "share_roc": 0.0, + "char_actual": 0.45, + "disc_now_approx": -0.0976 + }, + "KF": { + "sym": "KF", + "cik": 748691, + "name": "KOREA FUND INC", + "bdc": false, + "n_tender": 0, + "ticker": "KF", + "report_filed": "2026-08-27", + "ncols": 5, + "nav_beg": [ + 30.87, + 28.78, + 26.52, + 28.54, + 54.37 + ], + "nav_end": [ + 87.88, + 30.87, + 28.78, + 26.52, + 28.54 + ], + "mkt_end": [ + 75.21, + 26.93, + 24.13, + 23.14, + 24.35 + ], + "dist_tot": [ + 1.3800000000000097, + 0.4499999999999993, + -3.552713678800501e-15, + 3.3200000000000003, + 9.11 + ], + "divs": [ + -1.38, + -0.45, + -0.03, + 0.0, + -2.05 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + -3.27, + -7.06 + ], + "roc": [ + -0.02, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 58.39, + 2.41, + 2.17, + 1.25, + -16.73 + ], + "share_div": 0.2741935483870966, + "share_gains": 0.7244039270687233, + "share_roc": 0.0014025245441795222, + "char_actual": 0.54, + "disc_now_approx": -0.1442 + }, + "LENDX": { + "sym": "LENDX", + "cik": 1658645, + "name": "Stone Ridge Trust V", + "bdc": false, + "n_tender": 40, + "ticker": "LENDX", + "report_filed": "2026-05-08", + "ncols": 5, + "nav_beg": [ + 46.09, + 46.4, + 48.79, + 51.23, + 57.09 + ], + "nav_end": [ + 46.13, + 46.09, + 46.4, + 48.79, + 51.23 + ], + "mkt_end": [], + "dist_tot": [ + 1.99, + 1.85, + 1.92, + 2.54, + 18.28 + ], + "divs": [ + -1.99, + 0.0, + 0.0, + -2.54, + -7.52 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + -10.76 + ], + "roc": [ + 0.0, + -1.85, + -1.92, + 0.0, + 0.0 + ], + "ops": [ + 2.03, + 1.54, + -8.0, + -0.47, + 0.1 + ], + "share_div": 0.4533483822422874, + "share_gains": 0.40481565086531224, + "share_roc": 0.1418359668924003, + "char_actual": 0.47, + "disc_now_approx": null + }, + "EMO": { + "sym": "EMO", + "cik": 1517518, + "name": "ClearBridge Energy Midstream Opportunity Fund Inc.", + "bdc": false, + "n_tender": 6, + "ticker": "EMO", + "report_filed": "2026-01-27", + "ncols": 5, + "nav_beg": [ + 55.82, + 38.75, + 35.97, + 26.53, + 17.13 + ], + "nav_end": [ + 48.88, + 55.82, + 38.75, + 35.97, + 26.53 + ], + "mkt_end": [ + 45.55, + 50.49, + 34.5, + 30.43, + 21.65 + ], + "dist_tot": [ + 4.23, + 3.0, + 2.37, + 1.92, + 1.47 + ], + "divs": [ + -0.81, + -3.0, + -2.37, + -1.92, + -0.54 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "roc": [ + -3.42, + 0.0, + 0.0, + 0.0, + -0.93 + ], + "ops": [ + -2.25, + 20.07, + 5.11, + 11.22, + 10.65 + ], + "share_div": 0.6651270207852193, + "share_gains": 0.0, + "share_roc": 0.33487297921478054, + "char_actual": 0.4, + "disc_now_approx": -0.0681 + }, + "NML": { + "sym": "NML", + "cik": 1562051, + "name": "Neuberger Energy Infrastructure & Income Fund Inc.", + "bdc": false, + "n_tender": 0, + "ticker": "NML", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "EMF": { + "sym": "EMF", + "cik": 809708, + "name": "TEMPLETON EMERGING MARKETS FUND", + "bdc": false, + "n_tender": 0, + "ticker": "EMF", + "report_filed": "2025-10-30", + "ncols": 5, + "nav_beg": [ + 14.81, + 13.63, + 13.72, + 20.09, + 17.58 + ], + "nav_end": [ + 17.27, + 14.81, + 13.63, + 13.72, + 20.09 + ], + "mkt_end": [ + 15.27, + 12.81, + 11.71, + 11.85, + 17.89 + ], + "dist_tot": [ + 0.95, + 0.73, + 1.13, + 1.11, + 0.66 + ], + "divs": [ + -0.59, + -0.73, + -0.41, + -0.41, + -0.18 + ], + "gains": [ + -0.36, + 0.0, + -0.72, + -0.7, + -0.48 + ], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 3.36, + 1.86, + 1.03, + -5.3, + 3.15 + ], + "share_div": 0.5065502183406113, + "share_gains": 0.4934497816593886, + "share_roc": 0.0, + "char_actual": 0.4, + "disc_now_approx": -0.1158 + }, + "PDX": { + "sym": "PDX", + "cik": 1756908, + "name": "PIMCO Dynamic Income Strategy Fund", + "bdc": false, + "n_tender": 0, + "ticker": "PDX", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "ADX": { + "sym": "ADX", + "cik": 2230, + "name": "ADAMS DIVERSIFIED EQUITY FUND, INC.", + "bdc": false, + "n_tender": 0, + "ticker": "ADX", + "report_filed": "2026-02-20", + "ncols": 5, + "nav_beg": [ + 22.64, + 20.56, + 17.38, + 22.5, + 20.06 + ], + "nav_end": [ + 24.72, + 22.64, + 20.56, + 17.38, + 22.5 + ], + "mkt_end": [ + 23.32, + 20.2, + 17.71, + 14.54, + 19.41 + ], + "dist_tot": [ + 1.85, + 2.5, + 1.3, + 1.07, + 2.98 + ], + "divs": [ + -0.16, + -0.17, + -0.15, + -0.18, + -0.2 + ], + "gains": [ + -1.69, + -2.33, + -1.15, + -0.89, + -2.78 + ], + "roc": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 3.98, + 4.62, + 4.57, + -3.99, + 5.59 + ], + "share_div": 0.08865979381443298, + "share_gains": 0.9113402061855671, + "share_roc": 0.0, + "char_actual": 0.65, + "disc_now_approx": -0.0566 + }, + "HQL": { + "sym": "HQL", + "cik": 884121, + "name": "abrdn Life Sciences Investors", + "bdc": false, + "n_tender": 0, + "ticker": "HQL", + "report_filed": "2025-12-08", + "ncols": 5, + "nav_beg": [ + 16.38, + 15.0, + 15.49, + 21.22, + 20.25 + ], + "nav_end": [ + 17.35, + 16.38, + 15.0, + 15.49, + 21.22 + ], + "mkt_end": [ + 15.51, + 15.08, + 12.47, + 13.66, + 20.8 + ], + "dist_tot": [ + 1.82, + 1.66, + 1.28, + 1.47, + 1.69 + ], + "divs": [ + -1.72, + -0.69, + -0.02, + -0.03, + -0.42 + ], + "gains": [ + -0.1, + -0.76, + -1.26, + -1.44, + -1.27 + ], + "roc": [ + 0.0, + -0.21, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 2.79, + 3.04, + 0.79, + -4.26, + 2.66 + ], + "share_div": 0.36363636363636365, + "share_gains": 0.6098484848484849, + "share_roc": 0.026515151515151516, + "char_actual": 0.49, + "disc_now_approx": -0.1061 + }, + "SRV": { + "sym": "SRV", + "cik": 1400897, + "name": "NXG Cushing Midstream Energy Fund", + "bdc": false, + "n_tender": 0, + "ticker": "SRV", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "KYN": { + "sym": "KYN", + "cik": 1293613, + "name": "Kayne Anderson Energy Infrastructure Fund, Inc.", + "bdc": false, + "n_tender": 29, + "ticker": "KYN", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "RMT": { + "sym": "RMT", + "cik": 912147, + "name": "ROYCE MICRO-CAP TRUST, INC.", + "bdc": false, + "n_tender": 2, + "ticker": "RMT", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "FUND": { + "sym": "FUND", + "cik": 825202, + "name": "SPROTT FOCUS TRUST INC.", + "bdc": false, + "n_tender": 2, + "ticker": "FUND", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "IAE": { + "sym": "IAE", + "cik": 1385632, + "name": "Voya Asia Pacific High Dividend Equity Income Fund", + "bdc": false, + "n_tender": 0, + "ticker": "IAE", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "NXG": { + "sym": "NXG", + "cik": 1506488, + "name": "NXG NextGen Infrastructure Income Fund", + "bdc": false, + "n_tender": 0, + "ticker": "NXG", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "IHD": { + "sym": "IHD", + "cik": 1496292, + "name": "Voya Emerging Markets High Dividend Equity Fund", + "bdc": false, + "n_tender": 0, + "ticker": "IHD", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "HQH": { + "sym": "HQH", + "cik": 805267, + "name": "abrdn Healthcare Investors", + "bdc": false, + "n_tender": 0, + "ticker": "HQH", + "report_filed": "2025-12-08", + "ncols": 5, + "nav_beg": [ + 20.33, + 18.84, + 19.36, + 25.47, + 24.04 + ], + "nav_end": [ + 19.7, + 20.33, + 18.84, + 19.36, + 25.47 + ], + "mkt_end": [ + 18.46, + 18.62, + 15.55, + 17.28, + 25.57 + ], + "dist_tot": [ + 2.24, + 2.04, + 1.61, + 1.83, + 2.06 + ], + "divs": [ + -2.11, + -0.56, + 0.0, + -0.11, + -0.61 + ], + "gains": [ + -0.04, + -0.89, + -1.61, + -1.72, + -1.45 + ], + "roc": [ + -0.09, + -0.59, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 1.61, + 3.53, + 1.09, + -4.28, + 3.49 + ], + "share_div": 0.3466257668711656, + "share_gains": 0.583844580777096, + "share_roc": 0.06952965235173823, + "char_actual": 0.51, + "disc_now_approx": -0.0629 + }, + "MXF": { + "sym": "MXF", + "cik": 65433, + "name": "MEXICO FUND INC", + "bdc": false, + "n_tender": 14, + "ticker": "MXF", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "AEF": { + "sym": "AEF", + "cik": 846676, + "name": "abrdn Emerging Markets ex-China Fund, Inc.", + "bdc": false, + "n_tender": 0, + "ticker": "AEF", + "report_filed": "2026-03-09", + "ncols": 5, + "nav_beg": [ + 5.96, + 5.96, + 5.78, + 8.7, + 9.41 + ], + "nav_end": [ + 7.63, + 5.96, + 5.96, + 5.78, + 8.7 + ], + "mkt_end": [ + 7.0, + 5.19, + 5.11, + 5.15, + 7.92 + ], + "dist_tot": [ + 0.65, + 0.39, + 0.39, + 0.44, + 0.53 + ], + "divs": [ + -0.05, + -0.02, + -0.06, + -0.13, + -0.22 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "roc": [ + -0.6, + -0.37, + -0.33, + -0.31, + -0.31 + ], + "ops": [ + 2.29, + 0.39, + 0.57, + -2.48, + -0.18 + ], + "share_div": 0.19999999999999996, + "share_gains": 0.0, + "share_roc": 0.7999999999999999, + "char_actual": 0.82, + "disc_now_approx": -0.0826 + }, + "ASGI": { + "sym": "ASGI", + "cik": 1793855, + "name": "abrdn Global Infrastructure Income Fund", + "bdc": false, + "n_tender": 0, + "ticker": "ASGI", + "report_filed": "2025-12-08", + "ncols": 5, + "nav_beg": [ + 21.17, + 19.16, + 18.93, + 22.27, + 19.43 + ], + "nav_end": [ + 21.51, + 21.17, + 19.16, + 18.93, + 22.27 + ], + "mkt_end": [ + 21.13, + 20.21, + 16.1, + 15.73, + 19.93 + ], + "dist_tot": [ + 2.44, + 1.99, + 1.44, + 1.37, + 1.3 + ], + "divs": [ + -0.48, + -0.39, + -0.68, + -0.22, + -1.2 + ], + "gains": [ + -0.76, + -1.08, + -0.76, + -1.15, + -0.1 + ], + "roc": [ + -1.2, + -0.52, + 0.0, + 0.0, + 0.0 + ], + "ops": [ + 2.78, + 4.0, + 1.67, + -1.97, + 4.14 + ], + "share_div": 0.3477751756440281, + "share_gains": 0.4508196721311476, + "share_roc": 0.20140515222482439, + "char_actual": 0.55, + "disc_now_approx": -0.0177 + }, + "TYG": { + "sym": "TYG", + "cik": 1268533, + "name": "TORTOISE ENERGY INFRASTRUCTURE CORP", + "bdc": false, + "n_tender": 17, + "ticker": "TYG", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "AIO": { + "sym": "AIO", + "cik": 1778114, + "name": "Virtus Artificial Intelligence & Technology Opportunities Fund", + "bdc": false, + "n_tender": 0, + "ticker": "AIO", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "JCE": { + "sym": "JCE", + "cik": 1385763, + "name": "Nuveen Core Equity Alpha Fund", + "bdc": false, + "n_tender": 0, + "ticker": "JCE", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "ECAT": { + "sym": "ECAT", + "cik": 1864843, + "name": "BlackRock ESG Capital Allocation Term Trust", + "bdc": false, + "n_tender": 0, + "ticker": "ECAT", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "CET": { + "sym": "CET", + "cik": 18748, + "name": "CENTRAL SECURITIES CORP", + "bdc": false, + "n_tender": 4, + "ticker": "CET", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "IGD": { + "sym": "IGD", + "cik": 1285890, + "name": "Voya GLOBAL EQUITY DIVIDEND & PREMIUM OPPORTUNITY FUND", + "bdc": false, + "n_tender": 0, + "ticker": "IGD", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "STEW": { + "sym": "STEW", + "cik": 102426, + "name": "SRH Total Return Fund, Inc.", + "bdc": false, + "n_tender": 1, + "ticker": "STEW", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "AOD": { + "sym": "AOD", + "cik": 1379400, + "name": "abrdn Total Dynamic Dividend Fund", + "bdc": false, + "n_tender": 0, + "ticker": "AOD", + "report_filed": "2026-01-08", + "ncols": 5, + "nav_beg": [ + 11.15, + 9.9, + 10.05, + 12.95, + 10.16 + ], + "nav_end": [ + 11.63, + 11.15, + 9.9, + 10.05, + 12.95 + ], + "mkt_end": [ + 11.63, + 10.16, + 8.4, + 8.92, + 12.01 + ], + "dist_tot": [ + 1.32, + 0.93, + 0.78, + 0.78, + 0.78 + ], + "divs": [ + -0.72, + -0.75, + -0.75, + -0.73, + -0.78 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "roc": [ + -0.6, + -0.18, + -0.03, + -0.05, + 0.0 + ], + "ops": [ + 1.8, + 2.18, + 0.63, + -2.12, + 3.57 + ], + "share_div": 0.8126361655773419, + "share_gains": 0.0, + "share_roc": 0.18736383442265794, + "char_actual": 0.27, + "disc_now_approx": 0.0 + }, + "BXSY": { + "sym": "BXSY", + "cik": 1059213, + "name": "BEXIL INVESTMENT TRUST", + "bdc": false, + "n_tender": 0, + "ticker": "BXSY", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "IDE": { + "sym": "IDE", + "cik": 1417802, + "name": "Voya Infrastructure, Industrials & Materials Fund", + "bdc": false, + "n_tender": 0, + "ticker": "IDE", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "AGD": { + "sym": "AGD", + "cik": 1362481, + "name": "abrdn Global Dynamic Dividend Fund", + "bdc": false, + "n_tender": 0, + "ticker": "AGD", + "report_filed": "2026-01-08", + "ncols": 5, + "nav_beg": [ + 11.15, + 9.9, + 10.05, + 12.95, + 10.16 + ], + "nav_end": [ + 11.63, + 11.15, + 9.9, + 10.05, + 12.95 + ], + "mkt_end": [ + 11.63, + 10.16, + 8.4, + 8.92, + 12.01 + ], + "dist_tot": [ + 1.32, + 0.93, + 0.78, + 0.78, + 0.78 + ], + "divs": [ + -0.72, + -0.75, + -0.75, + -0.73, + -0.78 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "roc": [ + -0.6, + -0.18, + -0.03, + -0.05, + 0.0 + ], + "ops": [ + 1.8, + 2.18, + 0.63, + -2.12, + 3.57 + ], + "share_div": 0.8126361655773419, + "share_gains": 0.0, + "share_roc": 0.18736383442265794, + "char_actual": 0.27, + "disc_now_approx": 0.0 + }, + "IGA": { + "sym": "IGA", + "cik": 1332943, + "name": "Voya Global Advantage & Premium Opportunity Fund", + "bdc": false, + "n_tender": 0, + "ticker": "IGA", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "EOD": { + "sym": "EOD", + "cik": 1386067, + "name": "ALLSPRING GLOBAL DIVIDEND OPPORTUNITY FUND", + "bdc": false, + "n_tender": 0, + "ticker": "EOD", + "report_filed": "2026-01-05", + "ncols": 5, + "nav_beg": [ + 5.53, + 4.52, + 4.57, + 6.03, + 4.84 + ], + "nav_end": [ + 6.44, + 5.53, + 4.52, + 4.57, + 6.03 + ], + "mkt_end": [], + "dist_tot": [ + 0.5, + 0.44, + 0.45, + 0.53, + 0.52 + ], + "divs": [ + -0.26, + -0.24, + -0.21, + -0.18, + -0.26 + ], + "gains": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "roc": [ + -0.24, + -0.2, + -0.24, + -0.35, + -0.26 + ], + "ops": [ + 1.41, + 1.44, + 0.4, + -0.93, + 1.71 + ], + "share_div": 0.47131147540983603, + "share_gains": 0.0, + "share_roc": 0.5286885245901639, + "char_actual": 0.58, + "disc_now_approx": null + }, + "ETG": { + "sym": "ETG", + "cik": 1270523, + "name": "Eaton Vance Tax-Advantaged Global Dividend Income Fund", + "bdc": false, + "n_tender": 1, + "ticker": "ETG", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "CSQ": { + "sym": "CSQ", + "cik": 1275214, + "name": "CALAMOS STRATEGIC TOTAL RETURN FUND", + "bdc": false, + "n_tender": 5, + "ticker": "CSQ", + "report_filed": "2025-06-27", + "ncols": 3, + "nav_beg": [ + 10.04, + 8.99, + 10.26 + ], + "nav_end": [ + 9.32, + 10.04, + 8.99 + ], + "mkt_end": [], + "dist_tot": [ + 0.58, + 1.14, + 1.14 + ], + "divs": [ + -0.48, + -0.42, + -0.15 + ], + "gains": [ + -0.1, + -0.21, + -0.99 + ], + "roc": [ + 0.0, + -0.51, + 0.0 + ], + "ops": [ + -0.16, + 2.19, + -0.13 + ], + "share_div": 0.36713286713286714, + "share_gains": 0.45454545454545464, + "share_roc": 0.17832167832167836, + "char_actual": 0.53, + "disc_now_approx": null + }, + "MCI": { + "sym": "MCI", + "cik": 275694, + "name": "BARINGS CORPORATE INVESTORS", + "bdc": false, + "n_tender": 0, + "ticker": "MCI", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "SCD": { + "sym": "SCD", + "cik": 1270131, + "name": "LMP CAPITAL & INCOME FUND INC.", + "bdc": false, + "n_tender": 0, + "ticker": "SCD", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "MPV": { + "sym": "MPV", + "cik": 831655, + "name": "BARINGS PARTICIPATION INVESTORS", + "bdc": false, + "n_tender": 0, + "ticker": "MPV", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "CLM": { + "sym": "CLM", + "cik": 814083, + "name": "Cornerstone Strategic Investment Fund, Inc.", + "bdc": false, + "n_tender": 0, + "ticker": "CLM", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "HERZ": { + "sym": "HERZ", + "cik": 880406, + "name": "Herzfeld Credit Income Fund, Inc", + "bdc": false, + "n_tender": 0, + "ticker": "HERZ", + "report_filed": "2025-09-08", + "ncols": 5, + "nav_beg": [ + 3.1, + 4.98, + 4.63, + 7.06, + 4.76 + ], + "nav_end": [ + 2.65, + 3.1, + 4.98, + 4.63, + 7.06 + ], + "mkt_end": [ + 2.53, + 2.35, + 3.95, + 4.01, + 6.27 + ], + "dist_tot": [ + 0.46, + 0.41, + 0.69, + 1.06, + 0.62 + ], + "divs": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "gains": [ + -0.23, + -0.12, + -0.1, + -0.23, + 0.0 + ], + "roc": [ + -0.23, + -0.29, + -0.59, + -0.83, + -0.62 + ], + "ops": [ + 0.07, + 0.19, + 1.13, + -1.21, + 2.91 + ], + "share_div": 0.0, + "share_gains": 0.2098765432098765, + "share_roc": 0.7901234567901234, + "char_actual": 0.94, + "disc_now_approx": -0.0453 + }, + "CRF": { + "sym": "CRF", + "cik": 33934, + "name": "CORNERSTONE TOTAL RETURN FUND INC", + "bdc": false, + "n_tender": 10, + "ticker": "CRF", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "EDF": { + "sym": "EDF", + "cik": 1501103, + "name": "Virtus Stone Harbor Emerging Markets Income Fund", + "bdc": false, + "n_tender": 0, + "ticker": "EDF", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "VCRDX": { + "sym": "VCRDX", + "cik": 1812286, + "name": "Harrison Street Infrastructure Income Fund", + "bdc": false, + "n_tender": 9, + "ticker": "VCRDX", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "IGR": { + "sym": "IGR", + "cik": 1268884, + "name": "CBRE GLOBAL REAL ESTATE INCOME FUND", + "bdc": false, + "n_tender": 3, + "ticker": "IGR", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "NICHX": { + "sym": "NICHX", + "cik": 1736510, + "name": "Variant Alternative Income Fund", + "bdc": false, + "n_tender": 34, + "ticker": "NICHX", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + }, + "ASCIX": { + "sym": "ASCIX", + "cik": 1716885, + "name": "Angel Oak Strategic Credit Fund", + "bdc": false, + "n_tender": 33, + "ticker": "ASCIX", + "error": "no per-share table found in the 3 latest reports", + "char_actual": null + } +} \ No newline at end of file diff --git a/fundlab/cef_annual.py b/fundlab/cef_annual.py new file mode 100644 index 0000000..197a993 --- /dev/null +++ b/fundlab/cef_annual.py @@ -0,0 +1,471 @@ +"""CEF stage 2b: per-share financial highlights from the annual report. + +For each CEF, fetch the recent N-CSR/N-CSRS submissions and parse the +per-share "Financial Highlights" table. CEF tables come in two +families (verified on EMO / TWN): + + Less distributions to common shareholders from: (Franklin style) + Dividends ... + Return of capital ... + Total distributions to common shareholders ... + Less Distributions to Stockholders from: (classic CEF) + Net investment income ... + Net realized gains ... + Total distributions to stockholders ... + +Gotchas handled: +- Blanks are dropped when the table flattens: a short row's values can + sit in ANY columns. EMO's ROC row (3.42, 0.93) belongs to columns + 0 and 4, not 0 and 1; TWN's total-distributions row has 4 values + for 5 columns (a zero year dropped). +- The robust total per column comes from the NAV IDENTITY, not the + printed total row: dist[c] = NAV_beg[c] + ops[c] + captch[c] + - NAV_end[c]. The printed sub-rows (NII / gains / ROC) are then + placed combinatorially against those derived totals. +- The first "Net asset value, beginning of year" anchor in the doc is + the most recent 5-year table (a continuation table for older years + follows it). A "share" proximity guard skips dollar-basis mentions. +- Annual vs semi-annual: try the 3 most recent shareholder reports and + keep the one that parses with the most columns (annual = 5). + +Also captured (optional lines): market value end of year (-> discount +series), leverage (debt + preferred vs net assets), tax-paying flag. + +Run: python -m fundlab.cef_annual [SYM ...] (default: stage-2a +shortlist) +Output: fundlab/cef_annual.json (cached per fund in cef_cache/) +""" +from __future__ import annotations + +import json +import re +from pathlib import Path + +import numpy as np + +from fundlab import edgar + +HERE = Path(__file__).parent +CACHE = HERE / "cef_cache" +OUT = HERE / "cef_annual.json" +CHARACTER = HERE / "cef_character.json" + +# distribution-character fractions for the placement score (1 = fully +# taxable-account favorable) +FRAC_NII = 0.10 # ordinary income (equity-fund NII is partly QD) +FRAC_GAINS = 0.70 # mix of STCG/LTCG - long-term equity funds skew LT +FRAC_ROC = 1.0 # defers to the investor's own LTCG on a >1y sale +FRAC_NII_MUNI = 1.0 # tax-exempt interest + + +NUMTOK = re.compile(r"\$?\(?\d[\d,]*(?:\.\d+)?\)?") +FOOTNOTE = re.compile(r"\([a-z0-9]{1,3}\)") + + +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 + + +def _row(block: str, label_rx: str, span: int = 700) -> list[float | None]: + """Numeric row following the first label match in the block.""" + rm = re.search(label_rx, block, re.I) + if not rm: + return [] + tail = block[rm.end(): rm.end() + span] + 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 + if NUMTOK.fullmatch(t): + vals.append(t) + elif FOOTNOTE.fullmatch(t): + continue # footnote markers sit between values + elif vals: + break # first word after values = next label + return _nums(vals[:10]) + + +def parse_highlights(t: str) -> dict | None: + """Parse the per-share highlights table. Returns None on failure.""" + # collapse whitespace: CEF HTML tables flatten with one per line, + # splitting labels ("Net\nasset value, beginning of year") + t = re.sub(r"\s+", " ", t) + # anchor: first "Net asset value, beginning of year" with "share" + # nearby (skips dollar-basis statement mentions) + anchor = None + for m in re.finditer(r"net asset value(?: per (?:common )?share)?, beginning of (?:year|period)", t, re.I): + pre = t[max(0, m.start() - 400): m.start()] + if re.search(r"share", pre, re.I): + anchor = m + break + if not anchor: + return None + block = t[anchor.start(): anchor.start() + 6000] + + nav_beg = _row(block, r"net asset value(?: per (?:common )?share)?, beginning of (?:year|period)") + nav_end = _row(block, r"net asset value(?: per (?:common )?share)?, end of (?:year|period)") + if len(nav_beg) < 3 or len(nav_end) < 3: + return None + ncols = len(nav_beg) + if len(nav_end) != ncols or ncols > 6: + return None + + nii_ops = _row(block, r"Net investment (?:income|loss)\b") + gains_ops = _row(block, r"Net realized (?:and|&) (?:unrealized )?gain") + ops = _row(block, + r"total (?:income \(loss\) )?from (?:investment )?operations") + if not ops: + ops = [(_get(nii_ops, i) + _get(gains_ops, i)) for i in range(ncols)] + + # CEFs adjust NAV for capital transactions (repurchases, rights + # offerings, tenders). Collect every variant row present, deduping a + # physical row that matches two labels (e.g. "Capital Share + # Transactions: Accretion (dilution) ..."). + captch_rows = [] + seen = [] + for rx in (r"accretion (?:\(dilution\) )?to net asset value", + r"capital share transactions", + r"anti-dilutive impact of repurchase", + r"dilutive impact of rights offering", + r"anti-dilutive impact of tender offer" + r"repurchase of shares",): + row = _row(block, rx) + if row and any(len(row) == len(r2) and + all(abs((a or 0) - (b or 0)) < 0.005 + for a, b in zip(row, r2)) for r2 in seen): + continue + seen.append(row) + captch_rows.append(row) + mkt_end = _row(block, r"market (?:value|price), end of year") + + # distribution sub-rows (character). The block ends at NAV-end: + # footnotes below it also contain the word "dividends". + danchor = re.search( + r"distributions to (?:common )?shareholders from|" + r"distributions to stockholders from|" + r"distributions declared (?:to shareholders)? from|" + r"distributions from", + block, re.I) + dblock = block[danchor.start():] if danchor else block + em = re.search(r"net asset value(?: per (?:common )?share)?, end of (?:year|period)", dblock, re.I) + if em: + dblock = dblock[: em.start()] + # the TOTAL row label also contains "dividends" in some funds + # ("Total dividends and distributions to stockholders") - scope the + # sub-row search to before it + tm = re.search(r"total (?:dividends and )?distributions", dblock, re.I) + dwindow = dblock[: tm.start()] if tm else dblock + divs = (_row(dwindow, r"\bdividends\b") + or _row(dwindow, r"net investment income")) + gains = _row(dwindow, r"(?:net )?realized (?:capital )?gains?|" + r"capital gain distributions") + roc = _row(dwindow, r"return of capital") + + # total distributions: the PRINTED row is authoritative when it has + # one value per column; otherwise derive from the NAV identity + # (dist = NAV_beg + ops + captch - NAV_end) + dist_row = _row(dblock, r"total (?:dividends and )?distributions") + complete = len(dist_row) == ncols + if complete: + dist_tot = list(dist_row) + # some funds print distributions positive, others negative + if sum(1 for v in dist_tot if v < 0) > ncols // 2: + dist_tot = [-v for v in dist_tot] + else: + dist_tot = None + + # JOINT PLACEMENT. CEF tables drop zero columns, so a short row can + # sit in ANY subset of columns (TWN: gains in 4 of 5 cols, NII-divs + # in the other two; EMO: ROC in cols 0 and 4; KF: accretion in cols + # 1-4, not 0-3). One backtracking search places every short row + # (captch first - they affect the NAV identity - then the + # distribution rows) so the per-column arithmetic holds: + # printed total complete: divs+gains+roc == total + # printed total short: divs+gains+roc == NAV_beg+ops+captch-NAV_end + from itertools import combinations + + dist_rows = (("divs", divs), ("gains", gains), ("roc", roc)) + place = [(i, "c", row) for i, row in enumerate(captch_rows) + if 0 < len(row) < ncols] + place += [(i, "d", row) for i, (name, row) in enumerate(dist_rows) + if 0 < len(row) < ncols] + place.sort(key=lambda t: -len(t[2])) + + base_d = [0.0] * ncols + for name, row in dist_rows: + if len(row) == ncols: + for c in range(ncols): + if row[c]: + base_d[c] += abs(row[c]) + base_c = [0.0] * ncols + for row in captch_rows: + if len(row) == ncols: + for c in range(ncols): + if row[c]: + base_c[c] += row[c] + + dsum = [0.0] * ncols + csum = [0.0] * ncols + assign = [] + + def rec(i): + if i == len(place): + for c in range(ncols): + if complete: + need = abs(dist_tot[c]) + else: + need = abs(_get(nav_beg, c) + _get(ops, c) + base_c[c] + + csum[c] - _get(nav_end, c)) + if abs(base_d[c] + dsum[c] - need) > 0.02 * max(1.0, need) + 0.02: + return False + return True + idx, kind, vals = place[i] + nz = [v for v in vals if v] + for cols in combinations(range(ncols), len(nz)): + if kind == "d": + for c, v in zip(cols, nz): + dsum[c] += abs(v) + if complete and any(base_d[c] + dsum[c] > abs(dist_tot[c]) + 0.02 + for c in cols): + for c, v in zip(cols, nz): + dsum[c] -= abs(v) + continue + else: + for c, v in zip(cols, nz): + csum[c] += v + if rec(i + 1): + assign.append((idx, kind, nz, cols)) + return True + if kind == "d": + for c, v in zip(cols, nz): + dsum[c] -= abs(v) + else: + for c, v in zip(cols, nz): + csum[c] -= v + return False + + if place and not rec(0): + # short rows that reconcile nowhere: mis-parsed table - reject + return None + + def colvec(vals, cols): + vec = [0.0] * ncols + for ci, col in enumerate(cols): + vec[col] = vals[ci] + return vec + + placed_d = {} + placed_c = {i: [0.0] * ncols for i in + (i for i, k, _ in place if k == "c")} + for idx, kind, nz, cols in assign: + if kind == "d": + placed_d[idx] = colvec(nz, cols) + else: + placed_c[idx] = colvec(nz, cols) + + for i, (name, row) in enumerate(dist_rows): + if len(row) == ncols: + vec = list(row) + elif 0 < len(row) < ncols: + vec = placed_d.get(i, [0.0] * ncols) + else: + vec = [0.0] * ncols + if name == "divs": + divs = vec + elif name == "gains": + gains = vec + else: + roc = vec + captch = [base_c[c] for c in range(ncols)] + for i, vec in placed_c.items(): + for c in range(ncols): + captch[c] += vec[c] + if not complete: + dist_tot = [(_get(nav_beg, c) + _get(ops, c) + captch[c] + - _get(nav_end, c)) for c in range(ncols)] + if sum(1 for v in dist_tot if v < 0) > ncols // 2: + dist_tot = [-v for v in dist_tot] + + # sanity: components should (roughly) sum to the derived total + bad = sum(1 for c in range(ncols) + if abs(-(divs[c] + gains[c] + roc[c]) - dist_tot[c]) + > 0.02 * max(1.0, abs(dist_tot[c])) + 0.02) + if bad > max(1, ncols // 2): + return None + + out = { + "ncols": ncols, + "nav_beg": nav_beg, "nav_end": nav_end, + "mkt_end": mkt_end[:ncols] if mkt_end else [], + "dist_tot": dist_tot, "divs": divs, "gains": gains, "roc": roc, + "ops": ops[:ncols], + } + tot5 = sum(v for v in dist_tot if v > 0) + if tot5 > 0: + out["share_div"] = sum(-v for v in divs if v < 0) / tot5 + out["share_gains"] = sum(-v for v in gains if v < 0) / tot5 + out["share_roc"] = sum(-v for v in roc if v < 0) / tot5 + return out + + + +def _get(row: list, i: int) -> float: + if i < len(row) and row[i] is not None: + return row[i] + return 0.0 + + +def _submissions(cik: int) -> dict | None: + try: + raw = edgar.sec_get(f"https://data.sec.gov/submissions/CIK{cik:010d}.json") + return json.loads(raw) + except Exception: + return None + + +def analyze(sym: str, cik: int, force: bool = False) -> dict: + cf = CACHE / f"{sym}.json" + if cf.exists() and not force: + return json.loads(cf.read_text()) + d = _submissions(cik) + if not d: + return {"sym": sym, "error": "submissions fetch failed"} + r = d["filings"]["recent"] + forms = r["form"] + out: dict = {"sym": sym, "cik": cik, "name": d.get("name"), + "bdc": "10-K" in set(forms), + "n_tender": sum(1 for f in forms if f.startswith("N-23C")), + "ticker": (d.get("tickers") or [sym])[0]} + # try the 3 most recent shareholder reports, keep the best parse + cands = [(r["filingDate"][i], r["accessionNumber"][i]) + for i in range(len(forms)) + if forms[i] in ("N-CSR", "N-CSRS")][:3] + def _score(h): + # the 5-column annual table is the target; a 6-col semiannual + # (period + 5 FYs) is a fallback + return 2 if h["ncols"] == 5 else 1 + + best = None + for fd, acc in cands: + try: + raw = edgar.sec_get( + f"https://www.sec.gov/Archives/edgar/data/{cik}/{acc}.txt") + t = edgar.to_text(raw) + except Exception: + continue + h = parse_highlights(t) + if h and (best is None or _score(h) > _score(best[1]) + or (_score(h) == _score(best[1]) + and h["ncols"] > best[1]["ncols"])): + best = (fd, h, t) + if not best: + out["error"] = "no per-share table found in the 3 latest reports" + else: + fd, h, t = best + out["report_filed"] = fd + out.update(h) + # leverage + tax-paying flags (optional lines) + lm = re.search( + r"Loan and Debt Issuance Outstanding, End of Year \(000s\)\s*" + r"([\d,]+)", t) + pm = re.search( + r"Preferred Stock at Liquidation Value, End of Year \(000s\)\s*" + r"([\d,]+)", t) + nm = re.search( + r"Net assets applicable to common shareholders, end of year " + r"\(millions\)\s*\$?([\d,]+)", t) + if lm and nm: + debt = float(lm.group(1).replace(",", "")) * 1e3 + pref = float(pm.group(1).replace(",", "")) * 1e3 if pm else 0.0 + nav = float(nm.group(1).replace(",", "")) * 1e6 + out["leverage_pct"] = round((debt + pref) / nav * 100, 1) + if re.search(r"income tax expenses?\s+[\d.]+\s*%", t): + out["tax_paying"] = True + CACHE.mkdir(exist_ok=True) + cf.write_text(json.dumps(out, default=str)) + return out + + +def current_nav_discount(h: dict) -> float | None: + """FY-end discount (market vs NAV) from the table; used as a + rough estimate of the current discount (no live NAV locally).""" + nav_end = h.get("nav_end") or [] + mkt_end = h.get("mkt_end") or [] + if not nav_end or not mkt_end or not nav_end[0]: + return None + return float(mkt_end[0] / nav_end[0] - 1) + + +def character_score(h: dict, name: str) -> float | None: + if "share_div" not in h: + return None + f_nii = FRAC_NII_MUNI if re.search(r"muni|tax[- ]?exempt", name, re.I) \ + else FRAC_NII + return (h["share_div"] * f_nii + h["share_gains"] * FRAC_GAINS + + h["share_roc"] * FRAC_ROC) + + +def run(syms: list[str] | None = None) -> dict: + uni = json.loads((HERE / "cef_universe.json").read_text()) + if syms is None: + ch = json.loads(CHARACTER.read_text()) + syms = [s.upper() for s in + sorted(ch, key=lambda s: -ch[s]["tax_arb"])] + res: dict = {} + for i, s in enumerate(syms, 1): + u = uni.get(s) or {} + cik = int(u.get("cik", 0)) + a = analyze(s, cik) + a["char_actual"] = round(character_score(a, u.get("name", "")), 2) \ + if "share_div" in a else None + if "nav_end" in a: + disc = current_nav_discount(a) + a["disc_now_approx"] = round(disc, 4) if disc is not None else None + res[s] = a + if "error" in a: + print(f"{i:2}/{len(syms)} {s:7} ERR: {a['error'][:40]}", flush=True) + else: + print(f"{i:2}/{len(syms)} {s:7} " + f"div {a.get('share_div',0):.0%} gain {a.get('share_gains',0):.0%} " + f"ROC {a.get('share_roc',0):.0%} char={a.get('char_actual')} " + f"lev={a.get('leverage_pct')}", flush=True) + OUT.write_text(json.dumps(res, indent=1, default=str)) + print(f"wrote {OUT}") + return res + + +def _print(res: dict) -> None: + print(f"{'fund':7} {'name':38} {'fdiv':>5} {'fgain':>6} {'froc':>5}" + f" {'char':>5} {'disc':>7} {'lev%':>6} {'taxp':>4} {'tend':>5} " + f"{'report':>10}") + for s, v in res.items(): + if "share_div" not in v: + print(f"{s:7} {v.get('name','')[:38]:38} -- " + f"{v.get('error', 'no data')}") + continue + disc = v.get("disc_now_approx") + print(f"{s:7} {v.get('name','')[:38]:38} " + f"{v['share_div']:5.0%} {v['share_gains']:6.0%} " + f"{v['share_roc']:5.0%} {v.get('char_actual') or 0:5.2f} " + f"{('' if disc is None else f'{disc:+.1%}'):>7} " + f"{str(v.get('leverage_pct', '')):>6} " + f"{'Y' if v.get('tax_paying') else '':>4} " + f"{v.get('n_tender', 0):>5} {v.get('report_filed',''):>10}") + + +if __name__ == "__main__": + import sys + args = [a.upper() for a in sys.argv[1:]] + r = run(args or None) + _print(r) diff --git a/fundlab/cef_annual_run.log b/fundlab/cef_annual_run.log new file mode 100644 index 0000000..8b3d3d8 --- /dev/null +++ b/fundlab/cef_annual_run.log @@ -0,0 +1,102 @@ + 1/50 LTCFX ERR: no per-share table found in the 3 latest + 2/50 DXYZ ERR: no per-share table found in the 3 latest + 3/50 TWN div 6% gain 94% ROC 0% char=0.66 lev=None + 4/50 ASA ERR: no per-share table found in the 3 latest + 5/50 PEO ERR: no per-share table found in the 3 latest + 6/50 KF div 27% gain 72% ROC 0% char=0.54 lev=None + 7/50 LENDX ERR: no per-share table found in the 3 latest + 8/50 EMO div 67% gain 0% ROC 33% char=0.4 lev=None + 9/50 NML ERR: no per-share table found in the 3 latest +10/50 EMF ERR: no per-share table found in the 3 latest +11/50 PDX ERR: no per-share table found in the 3 latest +12/50 ADX ERR: no per-share table found in the 3 latest +13/50 HQL div 36% gain 61% ROC 3% char=0.49 lev=None +14/50 SRV ERR: no per-share table found in the 3 latest +15/50 KYN ERR: no per-share table found in the 3 latest +16/50 RMT ERR: no per-share table found in the 3 latest +17/50 FUND ERR: no per-share table found in the 3 latest +18/50 IAE ERR: no per-share table found in the 3 latest +19/50 NXG ERR: no per-share table found in the 3 latest +20/50 IHD ERR: no per-share table found in the 3 latest +21/50 HQH div 35% gain 58% ROC 7% char=0.51 lev=None +22/50 MXF ERR: no per-share table found in the 3 latest +23/50 AEF div 20% gain 0% ROC 80% char=0.82 lev=None +24/50 ASGI div 35% gain 45% ROC 20% char=0.55 lev=None +25/50 TYG ERR: no per-share table found in the 3 latest +26/50 AIO ERR: no per-share table found in the 3 latest +27/50 JCE ERR: no per-share table found in the 3 latest +28/50 ECAT ERR: no per-share table found in the 3 latest +29/50 CET ERR: no per-share table found in the 3 latest +30/50 IGD ERR: no per-share table found in the 3 latest +31/50 STEW ERR: no per-share table found in the 3 latest +32/50 AOD div 81% gain 0% ROC 19% char=0.27 lev=None +33/50 BXSY ERR: no per-share table found in the 3 latest +34/50 IDE ERR: no per-share table found in the 3 latest +35/50 AGD div 81% gain 0% ROC 19% char=0.27 lev=None +36/50 IGA ERR: no per-share table found in the 3 latest +37/50 EOD ERR: no per-share table found in the 3 latest +38/50 ETG ERR: no per-share table found in the 3 latest +39/50 CSQ ERR: no per-share table found in the 3 latest +40/50 MCI ERR: no per-share table found in the 3 latest +41/50 SCD ERR: no per-share table found in the 3 latest +42/50 MPV ERR: no per-share table found in the 3 latest +43/50 CLM ERR: no per-share table found in the 3 latest +44/50 HERZ div 0% gain 21% ROC 79% char=0.94 lev=None +45/50 CRF ERR: no per-share table found in the 3 latest +46/50 EDF ERR: no per-share table found in the 3 latest +47/50 VCRDX ERR: no per-share table found in the 3 latest +48/50 IGR ERR: no per-share table found in the 3 latest +49/50 NICHX ERR: no per-share table found in the 3 latest +50/50 ASCIX ERR: no per-share table found in the 3 latest +wrote /mnt/Backup/prog/f/fundlab/cef_annual.json +fund name fdiv fgain froc char disc lev% taxp tend report +LTCFX Alternative Strategies Income Fund -- no per-share table found in the 3 latest reports +DXYZ Destiny Tech100 Inc. -- no per-share table found in the 3 latest reports +TWN TAIWAN FUND INC 6% 94% 0% 0.66 -15.0% 0 2025-11-05 +ASA ASA Gold & Precious Metals Ltd -- no per-share table found in the 3 latest reports +PEO ADAMS NATURAL RESOURCES FUND, INC. -- no per-share table found in the 3 latest reports +KF KOREA FUND INC 27% 72% 0% 0.54 -14.4% 0 2026-08-27 +LENDX Stone Ridge Trust V -- no per-share table found in the 3 latest reports +EMO ClearBridge Energy Midstream Opportuni 67% 0% 33% 0.40 -6.8% 6 2026-01-27 +NML Neuberger Energy Infrastructure & Inco -- no per-share table found in the 3 latest reports +EMF TEMPLETON EMERGING MARKETS FUND -- no per-share table found in the 3 latest reports +PDX PIMCO Dynamic Income Strategy Fund -- no per-share table found in the 3 latest reports +ADX ADAMS DIVERSIFIED EQUITY FUND, INC. -- no per-share table found in the 3 latest reports +HQL abrdn Life Sciences Investors 36% 61% 3% 0.49 -10.6% 0 2025-12-08 +SRV NXG Cushing Midstream Energy Fund -- no per-share table found in the 3 latest reports +KYN Kayne Anderson Energy Infrastructure F -- no per-share table found in the 3 latest reports +RMT ROYCE MICRO-CAP TRUST, INC. -- no per-share table found in the 3 latest reports +FUND SPROTT FOCUS TRUST INC. -- no per-share table found in the 3 latest reports +IAE Voya Asia Pacific High Dividend Equity -- no per-share table found in the 3 latest reports +NXG NXG NextGen Infrastructure Income Fund -- no per-share table found in the 3 latest reports +IHD Voya Emerging Markets High Dividend Eq -- no per-share table found in the 3 latest reports +HQH abrdn Healthcare Investors 35% 58% 7% 0.51 -6.3% 0 2025-12-08 +MXF MEXICO FUND INC -- no per-share table found in the 3 latest reports +AEF abrdn Emerging Markets ex-China Fund, 20% 0% 80% 0.82 -8.3% 0 2026-03-09 +ASGI abrdn Global Infrastructure Income Fun 35% 45% 20% 0.55 -1.8% 0 2025-12-08 +TYG TORTOISE ENERGY INFRASTRUCTURE CORP -- no per-share table found in the 3 latest reports +AIO Virtus Artificial Intelligence & Techn -- no per-share table found in the 3 latest reports +JCE Nuveen Core Equity Alpha Fund -- no per-share table found in the 3 latest reports +ECAT BlackRock ESG Capital Allocation Term -- no per-share table found in the 3 latest reports +CET CENTRAL SECURITIES CORP -- no per-share table found in the 3 latest reports +IGD Voya GLOBAL EQUITY DIVIDEND & PREMIUM -- no per-share table found in the 3 latest reports +STEW SRH Total Return Fund, Inc. -- no per-share table found in the 3 latest reports +AOD abrdn Total Dynamic Dividend Fund 81% 0% 19% 0.27 +0.0% 0 2026-01-08 +BXSY BEXIL INVESTMENT TRUST -- no per-share table found in the 3 latest reports +IDE Voya Infrastructure, Industrials & Mat -- no per-share table found in the 3 latest reports +AGD abrdn Global Dynamic Dividend Fund 81% 0% 19% 0.27 +0.0% 0 2026-01-08 +IGA Voya Global Advantage & Premium Opport -- no per-share table found in the 3 latest reports +EOD ALLSPRING GLOBAL DIVIDEND OPPORTUNITY -- no per-share table found in the 3 latest reports +ETG Eaton Vance Tax-Advantaged Global Divi -- no per-share table found in the 3 latest reports +CSQ CALAMOS STRATEGIC TOTAL RETURN FUND -- no per-share table found in the 3 latest reports +MCI BARINGS CORPORATE INVESTORS -- no per-share table found in the 3 latest reports +SCD LMP CAPITAL & INCOME FUND INC. -- no per-share table found in the 3 latest reports +MPV BARINGS PARTICIPATION INVESTORS -- no per-share table found in the 3 latest reports +CLM Cornerstone Strategic Investment Fund, -- no per-share table found in the 3 latest reports +HERZ Herzfeld Credit Income Fund, Inc 0% 21% 79% 0.94 -4.5% 0 2025-09-08 +CRF CORNERSTONE TOTAL RETURN FUND INC -- no per-share table found in the 3 latest reports +EDF Virtus Stone Harbor Emerging Markets I -- no per-share table found in the 3 latest reports +VCRDX Harrison Street Infrastructure Income -- no per-share table found in the 3 latest reports +IGR CBRE GLOBAL REAL ESTATE INCOME FUND -- no per-share table found in the 3 latest reports +NICHX Variant Alternative Income Fund -- no per-share table found in the 3 latest reports +ASCIX Angel Oak Strategic Credit Fund -- no per-share table found in the 3 latest reports diff --git a/fundlab/cef_annual_run2.log b/fundlab/cef_annual_run2.log new file mode 100644 index 0000000..14210f3 --- /dev/null +++ b/fundlab/cef_annual_run2.log @@ -0,0 +1,12 @@ + 1/50 LTCFX ERR: no per-share table found in the 3 latest + 2/50 DXYZ ERR: no per-share table found in the 3 latest +Traceback (most recent call last): + File "", line 198, in _run_module_as_main + File "", line 88, in _run_code + File "/mnt/Backup/prog/f/fundlab/cef_annual.py", line 462, in + r = run(args or None) + ^^^^^^^^^^^^^^^^^ + File "/mnt/Backup/prog/f/fundlab/cef_annual.py", line 426, in run + a["disc_now_approx"] = round(current_nav_discount(a), 4) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TypeError: type NoneType doesn't define __round__ method diff --git a/fundlab/cef_annual_run3.log b/fundlab/cef_annual_run3.log new file mode 100644 index 0000000..6aac9eb --- /dev/null +++ b/fundlab/cef_annual_run3.log @@ -0,0 +1,102 @@ + 1/50 LTCFX ERR: no per-share table found in the 3 latest + 2/50 DXYZ ERR: no per-share table found in the 3 latest + 3/50 TWN div 6% gain 94% ROC 0% char=0.66 lev=None + 4/50 ASA ERR: no per-share table found in the 3 latest + 5/50 PEO div 42% gain 58% ROC 0% char=0.45 lev=None + 6/50 KF div 27% gain 72% ROC 0% char=0.54 lev=None + 7/50 LENDX div 45% gain 40% ROC 14% char=0.47 lev=None + 8/50 EMO div 67% gain 0% ROC 33% char=0.4 lev=None + 9/50 NML ERR: no per-share table found in the 3 latest +10/50 EMF div 51% gain 49% ROC 0% char=0.4 lev=None +11/50 PDX ERR: no per-share table found in the 3 latest +12/50 ADX div 9% gain 91% ROC 0% char=0.65 lev=None +13/50 HQL div 36% gain 61% ROC 3% char=0.49 lev=None +14/50 SRV ERR: no per-share table found in the 3 latest +15/50 KYN ERR: no per-share table found in the 3 latest +16/50 RMT ERR: no per-share table found in the 3 latest +17/50 FUND ERR: no per-share table found in the 3 latest +18/50 IAE ERR: no per-share table found in the 3 latest +19/50 NXG ERR: no per-share table found in the 3 latest +20/50 IHD ERR: no per-share table found in the 3 latest +21/50 HQH div 35% gain 58% ROC 7% char=0.51 lev=None +22/50 MXF ERR: no per-share table found in the 3 latest +23/50 AEF div 20% gain 0% ROC 80% char=0.82 lev=None +24/50 ASGI div 35% gain 45% ROC 20% char=0.55 lev=None +25/50 TYG ERR: no per-share table found in the 3 latest +26/50 AIO ERR: no per-share table found in the 3 latest +27/50 JCE ERR: no per-share table found in the 3 latest +28/50 ECAT ERR: no per-share table found in the 3 latest +29/50 CET ERR: no per-share table found in the 3 latest +30/50 IGD ERR: no per-share table found in the 3 latest +31/50 STEW ERR: no per-share table found in the 3 latest +32/50 AOD div 81% gain 0% ROC 19% char=0.27 lev=None +33/50 BXSY ERR: no per-share table found in the 3 latest +34/50 IDE ERR: no per-share table found in the 3 latest +35/50 AGD div 81% gain 0% ROC 19% char=0.27 lev=None +36/50 IGA ERR: no per-share table found in the 3 latest +37/50 EOD div 47% gain 0% ROC 53% char=0.58 lev=None +38/50 ETG ERR: no per-share table found in the 3 latest +39/50 CSQ div 37% gain 45% ROC 18% char=0.53 lev=None +40/50 MCI ERR: no per-share table found in the 3 latest +41/50 SCD ERR: no per-share table found in the 3 latest +42/50 MPV ERR: no per-share table found in the 3 latest +43/50 CLM ERR: no per-share table found in the 3 latest +44/50 HERZ div 0% gain 21% ROC 79% char=0.94 lev=None +45/50 CRF ERR: no per-share table found in the 3 latest +46/50 EDF ERR: no per-share table found in the 3 latest +47/50 VCRDX ERR: no per-share table found in the 3 latest +48/50 IGR ERR: no per-share table found in the 3 latest +49/50 NICHX ERR: no per-share table found in the 3 latest +50/50 ASCIX ERR: no per-share table found in the 3 latest +wrote /mnt/Backup/prog/f/fundlab/cef_annual.json +fund name fdiv fgain froc char disc lev% taxp tend report +LTCFX Alternative Strategies Income Fund -- no per-share table found in the 3 latest reports +DXYZ Destiny Tech100 Inc. -- no per-share table found in the 3 latest reports +TWN TAIWAN FUND INC 6% 94% 0% 0.66 -15.0% 0 2025-11-05 +ASA ASA Gold & Precious Metals Ltd -- no per-share table found in the 3 latest reports +PEO ADAMS NATURAL RESOURCES FUND, INC. 42% 58% 0% 0.45 -9.8% 0 2026-02-20 +KF KOREA FUND INC 27% 72% 0% 0.54 -14.4% 0 2026-08-27 +LENDX Stone Ridge Trust V 45% 40% 14% 0.47 40 2026-05-08 +EMO ClearBridge Energy Midstream Opportuni 67% 0% 33% 0.40 -6.8% 6 2026-01-27 +NML Neuberger Energy Infrastructure & Inco -- no per-share table found in the 3 latest reports +EMF TEMPLETON EMERGING MARKETS FUND 51% 49% 0% 0.40 -11.6% 0 2025-10-30 +PDX PIMCO Dynamic Income Strategy Fund -- no per-share table found in the 3 latest reports +ADX ADAMS DIVERSIFIED EQUITY FUND, INC. 9% 91% 0% 0.65 -5.7% 0 2026-02-20 +HQL abrdn Life Sciences Investors 36% 61% 3% 0.49 -10.6% 0 2025-12-08 +SRV NXG Cushing Midstream Energy Fund -- no per-share table found in the 3 latest reports +KYN Kayne Anderson Energy Infrastructure F -- no per-share table found in the 3 latest reports +RMT ROYCE MICRO-CAP TRUST, INC. -- no per-share table found in the 3 latest reports +FUND SPROTT FOCUS TRUST INC. -- no per-share table found in the 3 latest reports +IAE Voya Asia Pacific High Dividend Equity -- no per-share table found in the 3 latest reports +NXG NXG NextGen Infrastructure Income Fund -- no per-share table found in the 3 latest reports +IHD Voya Emerging Markets High Dividend Eq -- no per-share table found in the 3 latest reports +HQH abrdn Healthcare Investors 35% 58% 7% 0.51 -6.3% 0 2025-12-08 +MXF MEXICO FUND INC -- no per-share table found in the 3 latest reports +AEF abrdn Emerging Markets ex-China Fund, 20% 0% 80% 0.82 -8.3% 0 2026-03-09 +ASGI abrdn Global Infrastructure Income Fun 35% 45% 20% 0.55 -1.8% 0 2025-12-08 +TYG TORTOISE ENERGY INFRASTRUCTURE CORP -- no per-share table found in the 3 latest reports +AIO Virtus Artificial Intelligence & Techn -- no per-share table found in the 3 latest reports +JCE Nuveen Core Equity Alpha Fund -- no per-share table found in the 3 latest reports +ECAT BlackRock ESG Capital Allocation Term -- no per-share table found in the 3 latest reports +CET CENTRAL SECURITIES CORP -- no per-share table found in the 3 latest reports +IGD Voya GLOBAL EQUITY DIVIDEND & PREMIUM -- no per-share table found in the 3 latest reports +STEW SRH Total Return Fund, Inc. -- no per-share table found in the 3 latest reports +AOD abrdn Total Dynamic Dividend Fund 81% 0% 19% 0.27 +0.0% 0 2026-01-08 +BXSY BEXIL INVESTMENT TRUST -- no per-share table found in the 3 latest reports +IDE Voya Infrastructure, Industrials & Mat -- no per-share table found in the 3 latest reports +AGD abrdn Global Dynamic Dividend Fund 81% 0% 19% 0.27 +0.0% 0 2026-01-08 +IGA Voya Global Advantage & Premium Opport -- no per-share table found in the 3 latest reports +EOD ALLSPRING GLOBAL DIVIDEND OPPORTUNITY 47% 0% 53% 0.58 0 2026-01-05 +ETG Eaton Vance Tax-Advantaged Global Divi -- no per-share table found in the 3 latest reports +CSQ CALAMOS STRATEGIC TOTAL RETURN FUND 37% 45% 18% 0.53 5 2025-06-27 +MCI BARINGS CORPORATE INVESTORS -- no per-share table found in the 3 latest reports +SCD LMP CAPITAL & INCOME FUND INC. -- no per-share table found in the 3 latest reports +MPV BARINGS PARTICIPATION INVESTORS -- no per-share table found in the 3 latest reports +CLM Cornerstone Strategic Investment Fund, -- no per-share table found in the 3 latest reports +HERZ Herzfeld Credit Income Fund, Inc 0% 21% 79% 0.94 -4.5% 0 2025-09-08 +CRF CORNERSTONE TOTAL RETURN FUND INC -- no per-share table found in the 3 latest reports +EDF Virtus Stone Harbor Emerging Markets I -- no per-share table found in the 3 latest reports +VCRDX Harrison Street Infrastructure Income -- no per-share table found in the 3 latest reports +IGR CBRE GLOBAL REAL ESTATE INCOME FUND -- no per-share table found in the 3 latest reports +NICHX Variant Alternative Income Fund -- no per-share table found in the 3 latest reports +ASCIX Angel Oak Strategic Credit Fund -- no per-share table found in the 3 latest reports diff --git a/fundlab/cef_cache/ADX.json b/fundlab/cef_cache/ADX.json new file mode 100644 index 0000000..d2560bf --- /dev/null +++ b/fundlab/cef_cache/ADX.json @@ -0,0 +1 @@ +{"sym": "ADX", "cik": 2230, "name": "ADAMS DIVERSIFIED EQUITY FUND, INC.", "bdc": false, "n_tender": 0, "ticker": "ADX", "report_filed": "2026-02-20", "ncols": 5, "nav_beg": [22.64, 20.56, 17.38, 22.5, 20.06], "nav_end": [24.72, 22.64, 20.56, 17.38, 22.5], "mkt_end": [23.32, 20.2, 17.71, 14.54, 19.41], "dist_tot": [1.85, 2.5, 1.3, 1.07, 2.98], "divs": [-0.16, -0.17, -0.15, -0.18, -0.2], "gains": [-1.69, -2.33, -1.15, -0.89, -2.78], "roc": [0.0, 0.0, 0.0, 0.0, 0.0], "ops": [3.98, 4.62, 4.57, -3.99, 5.59], "share_div": 0.08865979381443298, "share_gains": 0.9113402061855671, "share_roc": 0.0} \ No newline at end of file diff --git a/fundlab/cef_cache/AEF.json b/fundlab/cef_cache/AEF.json new file mode 100644 index 0000000..63ac5f3 --- /dev/null +++ b/fundlab/cef_cache/AEF.json @@ -0,0 +1 @@ +{"sym": "AEF", "cik": 846676, "name": "abrdn Emerging Markets ex-China Fund, Inc.", "bdc": false, "n_tender": 0, "ticker": "AEF", "report_filed": "2026-03-09", "ncols": 5, "nav_beg": [5.96, 5.96, 5.78, 8.7, 9.41], "nav_end": [7.63, 5.96, 5.96, 5.78, 8.7], "mkt_end": [7.0, 5.19, 5.11, 5.15, 7.92], "dist_tot": [0.65, 0.39, 0.39, 0.44, 0.53], "divs": [-0.05, -0.02, -0.06, -0.13, -0.22], "gains": [0.0, 0.0, 0.0, 0.0, 0.0], "roc": [-0.6, -0.37, -0.33, -0.31, -0.31], "ops": [2.29, 0.39, 0.57, -2.48, -0.18], "share_div": 0.19999999999999996, "share_gains": 0.0, "share_roc": 0.7999999999999999} \ No newline at end of file diff --git a/fundlab/cef_cache/AGD.json b/fundlab/cef_cache/AGD.json new file mode 100644 index 0000000..c69a763 --- /dev/null +++ b/fundlab/cef_cache/AGD.json @@ -0,0 +1 @@ +{"sym": "AGD", "cik": 1362481, "name": "abrdn Global Dynamic Dividend Fund", "bdc": false, "n_tender": 0, "ticker": "AGD", "report_filed": "2026-01-08", "ncols": 5, "nav_beg": [11.15, 9.9, 10.05, 12.95, 10.16], "nav_end": [11.63, 11.15, 9.9, 10.05, 12.95], "mkt_end": [11.63, 10.16, 8.4, 8.92, 12.01], "dist_tot": [1.32, 0.93, 0.78, 0.78, 0.78], "divs": [-0.72, -0.75, -0.75, -0.73, -0.78], "gains": [0.0, 0.0, 0.0, 0.0, 0.0], "roc": [-0.6, -0.18, -0.03, -0.05, 0.0], "ops": [1.8, 2.18, 0.63, -2.12, 3.57], "share_div": 0.8126361655773419, "share_gains": 0.0, "share_roc": 0.18736383442265794} \ No newline at end of file diff --git a/fundlab/cef_cache/AIO.json b/fundlab/cef_cache/AIO.json new file mode 100644 index 0000000..cd15a27 --- /dev/null +++ b/fundlab/cef_cache/AIO.json @@ -0,0 +1 @@ +{"sym": "AIO", "cik": 1778114, "name": "Virtus Artificial Intelligence & Technology Opportunities Fund", "bdc": false, "n_tender": 0, "ticker": "AIO", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/AOD.json b/fundlab/cef_cache/AOD.json new file mode 100644 index 0000000..85c7228 --- /dev/null +++ b/fundlab/cef_cache/AOD.json @@ -0,0 +1 @@ +{"sym": "AOD", "cik": 1379400, "name": "abrdn Total Dynamic Dividend Fund", "bdc": false, "n_tender": 0, "ticker": "AOD", "report_filed": "2026-01-08", "ncols": 5, "nav_beg": [11.15, 9.9, 10.05, 12.95, 10.16], "nav_end": [11.63, 11.15, 9.9, 10.05, 12.95], "mkt_end": [11.63, 10.16, 8.4, 8.92, 12.01], "dist_tot": [1.32, 0.93, 0.78, 0.78, 0.78], "divs": [-0.72, -0.75, -0.75, -0.73, -0.78], "gains": [0.0, 0.0, 0.0, 0.0, 0.0], "roc": [-0.6, -0.18, -0.03, -0.05, 0.0], "ops": [1.8, 2.18, 0.63, -2.12, 3.57], "share_div": 0.8126361655773419, "share_gains": 0.0, "share_roc": 0.18736383442265794} \ No newline at end of file diff --git a/fundlab/cef_cache/ASA.json b/fundlab/cef_cache/ASA.json new file mode 100644 index 0000000..1bb34ac --- /dev/null +++ b/fundlab/cef_cache/ASA.json @@ -0,0 +1 @@ +{"sym": "ASA", "cik": 1230869, "name": "ASA Gold & Precious Metals Ltd", "bdc": false, "n_tender": 0, "ticker": "ASA", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/ASCIX.json b/fundlab/cef_cache/ASCIX.json new file mode 100644 index 0000000..adc23bc --- /dev/null +++ b/fundlab/cef_cache/ASCIX.json @@ -0,0 +1 @@ +{"sym": "ASCIX", "cik": 1716885, "name": "Angel Oak Strategic Credit Fund", "bdc": false, "n_tender": 33, "ticker": "ASCIX", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/ASGI.json b/fundlab/cef_cache/ASGI.json new file mode 100644 index 0000000..a820e39 --- /dev/null +++ b/fundlab/cef_cache/ASGI.json @@ -0,0 +1 @@ +{"sym": "ASGI", "cik": 1793855, "name": "abrdn Global Infrastructure Income Fund", "bdc": false, "n_tender": 0, "ticker": "ASGI", "report_filed": "2025-12-08", "ncols": 5, "nav_beg": [21.17, 19.16, 18.93, 22.27, 19.43], "nav_end": [21.51, 21.17, 19.16, 18.93, 22.27], "mkt_end": [21.13, 20.21, 16.1, 15.73, 19.93], "dist_tot": [2.44, 1.99, 1.44, 1.37, 1.3], "divs": [-0.48, -0.39, -0.68, -0.22, -1.2], "gains": [-0.76, -1.08, -0.76, -1.15, -0.1], "roc": [-1.2, -0.52, 0.0, 0.0, 0.0], "ops": [2.78, 4.0, 1.67, -1.97, 4.14], "share_div": 0.3477751756440281, "share_gains": 0.4508196721311476, "share_roc": 0.20140515222482439} \ No newline at end of file diff --git a/fundlab/cef_cache/BXSY.json b/fundlab/cef_cache/BXSY.json new file mode 100644 index 0000000..1e9be03 --- /dev/null +++ b/fundlab/cef_cache/BXSY.json @@ -0,0 +1 @@ +{"sym": "BXSY", "cik": 1059213, "name": "BEXIL INVESTMENT TRUST", "bdc": false, "n_tender": 0, "ticker": "BXSY", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/CET.json b/fundlab/cef_cache/CET.json new file mode 100644 index 0000000..9180177 --- /dev/null +++ b/fundlab/cef_cache/CET.json @@ -0,0 +1 @@ +{"sym": "CET", "cik": 18748, "name": "CENTRAL SECURITIES CORP", "bdc": false, "n_tender": 4, "ticker": "CET", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/CLM.json b/fundlab/cef_cache/CLM.json new file mode 100644 index 0000000..911af3c --- /dev/null +++ b/fundlab/cef_cache/CLM.json @@ -0,0 +1 @@ +{"sym": "CLM", "cik": 814083, "name": "Cornerstone Strategic Investment Fund, Inc.", "bdc": false, "n_tender": 0, "ticker": "CLM", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/CRF.json b/fundlab/cef_cache/CRF.json new file mode 100644 index 0000000..3bfcb5c --- /dev/null +++ b/fundlab/cef_cache/CRF.json @@ -0,0 +1 @@ +{"sym": "CRF", "cik": 33934, "name": "CORNERSTONE TOTAL RETURN FUND INC", "bdc": false, "n_tender": 10, "ticker": "CRF", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/CSQ.json b/fundlab/cef_cache/CSQ.json new file mode 100644 index 0000000..f73232c --- /dev/null +++ b/fundlab/cef_cache/CSQ.json @@ -0,0 +1 @@ +{"sym": "CSQ", "cik": 1275214, "name": "CALAMOS STRATEGIC TOTAL RETURN FUND", "bdc": false, "n_tender": 5, "ticker": "CSQ", "report_filed": "2025-06-27", "ncols": 3, "nav_beg": [10.04, 8.99, 10.26], "nav_end": [9.32, 10.04, 8.99], "mkt_end": [], "dist_tot": [0.58, 1.14, 1.14], "divs": [-0.48, -0.42, -0.15], "gains": [-0.1, -0.21, -0.99], "roc": [0.0, -0.51, 0.0], "ops": [-0.16, 2.19, -0.13], "share_div": 0.36713286713286714, "share_gains": 0.45454545454545464, "share_roc": 0.17832167832167836} \ No newline at end of file diff --git a/fundlab/cef_cache/DXYZ.json b/fundlab/cef_cache/DXYZ.json new file mode 100644 index 0000000..c7b31a3 --- /dev/null +++ b/fundlab/cef_cache/DXYZ.json @@ -0,0 +1 @@ +{"sym": "DXYZ", "cik": 1843974, "name": "Destiny Tech100 Inc.", "bdc": false, "n_tender": 0, "ticker": "DXYZ", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/ECAT.json b/fundlab/cef_cache/ECAT.json new file mode 100644 index 0000000..24eb07b --- /dev/null +++ b/fundlab/cef_cache/ECAT.json @@ -0,0 +1 @@ +{"sym": "ECAT", "cik": 1864843, "name": "BlackRock ESG Capital Allocation Term Trust", "bdc": false, "n_tender": 0, "ticker": "ECAT", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/EDF.json b/fundlab/cef_cache/EDF.json new file mode 100644 index 0000000..787bd09 --- /dev/null +++ b/fundlab/cef_cache/EDF.json @@ -0,0 +1 @@ +{"sym": "EDF", "cik": 1501103, "name": "Virtus Stone Harbor Emerging Markets Income Fund", "bdc": false, "n_tender": 0, "ticker": "EDF", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/EMF.json b/fundlab/cef_cache/EMF.json new file mode 100644 index 0000000..c59beaa --- /dev/null +++ b/fundlab/cef_cache/EMF.json @@ -0,0 +1 @@ +{"sym": "EMF", "cik": 809708, "name": "TEMPLETON EMERGING MARKETS FUND", "bdc": false, "n_tender": 0, "ticker": "EMF", "report_filed": "2025-10-30", "ncols": 5, "nav_beg": [14.81, 13.63, 13.72, 20.09, 17.58], "nav_end": [17.27, 14.81, 13.63, 13.72, 20.09], "mkt_end": [15.27, 12.81, 11.71, 11.85, 17.89], "dist_tot": [0.95, 0.73, 1.13, 1.11, 0.66], "divs": [-0.59, -0.73, -0.41, -0.41, -0.18], "gains": [-0.36, 0.0, -0.72, -0.7, -0.48], "roc": [0.0, 0.0, 0.0, 0.0, 0.0], "ops": [3.36, 1.86, 1.03, -5.3, 3.15], "share_div": 0.5065502183406113, "share_gains": 0.4934497816593886, "share_roc": 0.0} \ No newline at end of file diff --git a/fundlab/cef_cache/EMO.json b/fundlab/cef_cache/EMO.json new file mode 100644 index 0000000..4bd11f4 --- /dev/null +++ b/fundlab/cef_cache/EMO.json @@ -0,0 +1 @@ +{"sym": "EMO", "cik": 1517518, "name": "ClearBridge Energy Midstream Opportunity Fund Inc.", "bdc": false, "n_tender": 6, "ticker": "EMO", "report_filed": "2026-01-27", "ncols": 5, "nav_beg": [55.82, 38.75, 35.97, 26.53, 17.13], "nav_end": [48.88, 55.82, 38.75, 35.97, 26.53], "mkt_end": [45.55, 50.49, 34.5, 30.43, 21.65], "dist_tot": [4.23, 3.0, 2.37, 1.92, 1.47], "divs": [-0.81, -3.0, -2.37, -1.92, -0.54], "gains": [0.0, 0.0, 0.0, 0.0, 0.0], "roc": [-3.42, 0.0, 0.0, 0.0, -0.93], "ops": [-2.25, 20.07, 5.11, 11.22, 10.65], "share_div": 0.6651270207852193, "share_gains": 0.0, "share_roc": 0.33487297921478054} \ No newline at end of file diff --git a/fundlab/cef_cache/EOD.json b/fundlab/cef_cache/EOD.json new file mode 100644 index 0000000..e348513 --- /dev/null +++ b/fundlab/cef_cache/EOD.json @@ -0,0 +1 @@ +{"sym": "EOD", "cik": 1386067, "name": "ALLSPRING GLOBAL DIVIDEND OPPORTUNITY FUND", "bdc": false, "n_tender": 0, "ticker": "EOD", "report_filed": "2026-01-05", "ncols": 5, "nav_beg": [5.53, 4.52, 4.57, 6.03, 4.84], "nav_end": [6.44, 5.53, 4.52, 4.57, 6.03], "mkt_end": [], "dist_tot": [0.5, 0.44, 0.45, 0.53, 0.52], "divs": [-0.26, -0.24, -0.21, -0.18, -0.26], "gains": [0.0, 0.0, 0.0, 0.0, 0.0], "roc": [-0.24, -0.2, -0.24, -0.35, -0.26], "ops": [1.41, 1.44, 0.4, -0.93, 1.71], "share_div": 0.47131147540983603, "share_gains": 0.0, "share_roc": 0.5286885245901639} \ No newline at end of file diff --git a/fundlab/cef_cache/ETG.json b/fundlab/cef_cache/ETG.json new file mode 100644 index 0000000..2f95dbe --- /dev/null +++ b/fundlab/cef_cache/ETG.json @@ -0,0 +1 @@ +{"sym": "ETG", "cik": 1270523, "name": "Eaton Vance Tax-Advantaged Global Dividend Income Fund", "bdc": false, "n_tender": 1, "ticker": "ETG", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/FUND.json b/fundlab/cef_cache/FUND.json new file mode 100644 index 0000000..b48aca3 --- /dev/null +++ b/fundlab/cef_cache/FUND.json @@ -0,0 +1 @@ +{"sym": "FUND", "cik": 825202, "name": "SPROTT FOCUS TRUST INC.", "bdc": false, "n_tender": 2, "ticker": "FUND", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/HERZ.json b/fundlab/cef_cache/HERZ.json new file mode 100644 index 0000000..56d7666 --- /dev/null +++ b/fundlab/cef_cache/HERZ.json @@ -0,0 +1 @@ +{"sym": "HERZ", "cik": 880406, "name": "Herzfeld Credit Income Fund, Inc", "bdc": false, "n_tender": 0, "ticker": "HERZ", "report_filed": "2025-09-08", "ncols": 5, "nav_beg": [3.1, 4.98, 4.63, 7.06, 4.76], "nav_end": [2.65, 3.1, 4.98, 4.63, 7.06], "mkt_end": [2.53, 2.35, 3.95, 4.01, 6.27], "dist_tot": [0.46, 0.41, 0.69, 1.06, 0.62], "divs": [0.0, 0.0, 0.0, 0.0, 0.0], "gains": [-0.23, -0.12, -0.1, -0.23, 0.0], "roc": [-0.23, -0.29, -0.59, -0.83, -0.62], "ops": [0.07, 0.19, 1.13, -1.21, 2.91], "share_div": 0.0, "share_gains": 0.2098765432098765, "share_roc": 0.7901234567901234} \ No newline at end of file diff --git a/fundlab/cef_cache/HQH.json b/fundlab/cef_cache/HQH.json new file mode 100644 index 0000000..0182cbb --- /dev/null +++ b/fundlab/cef_cache/HQH.json @@ -0,0 +1 @@ +{"sym": "HQH", "cik": 805267, "name": "abrdn Healthcare Investors", "bdc": false, "n_tender": 0, "ticker": "HQH", "report_filed": "2025-12-08", "ncols": 5, "nav_beg": [20.33, 18.84, 19.36, 25.47, 24.04], "nav_end": [19.7, 20.33, 18.84, 19.36, 25.47], "mkt_end": [18.46, 18.62, 15.55, 17.28, 25.57], "dist_tot": [2.24, 2.04, 1.61, 1.83, 2.06], "divs": [-2.11, -0.56, 0.0, -0.11, -0.61], "gains": [-0.04, -0.89, -1.61, -1.72, -1.45], "roc": [-0.09, -0.59, 0.0, 0.0, 0.0], "ops": [1.61, 3.53, 1.09, -4.28, 3.49], "share_div": 0.3466257668711656, "share_gains": 0.583844580777096, "share_roc": 0.06952965235173823} \ No newline at end of file diff --git a/fundlab/cef_cache/HQL.json b/fundlab/cef_cache/HQL.json new file mode 100644 index 0000000..786ccf4 --- /dev/null +++ b/fundlab/cef_cache/HQL.json @@ -0,0 +1 @@ +{"sym": "HQL", "cik": 884121, "name": "abrdn Life Sciences Investors", "bdc": false, "n_tender": 0, "ticker": "HQL", "report_filed": "2025-12-08", "ncols": 5, "nav_beg": [16.38, 15.0, 15.49, 21.22, 20.25], "nav_end": [17.35, 16.38, 15.0, 15.49, 21.22], "mkt_end": [15.51, 15.08, 12.47, 13.66, 20.8], "dist_tot": [1.82, 1.66, 1.28, 1.47, 1.69], "divs": [-1.72, -0.69, -0.02, -0.03, -0.42], "gains": [-0.1, -0.76, -1.26, -1.44, -1.27], "roc": [0.0, -0.21, 0.0, 0.0, 0.0], "ops": [2.79, 3.04, 0.79, -4.26, 2.66], "share_div": 0.36363636363636365, "share_gains": 0.6098484848484849, "share_roc": 0.026515151515151516} \ No newline at end of file diff --git a/fundlab/cef_cache/IAE.json b/fundlab/cef_cache/IAE.json new file mode 100644 index 0000000..5902f03 --- /dev/null +++ b/fundlab/cef_cache/IAE.json @@ -0,0 +1 @@ +{"sym": "IAE", "cik": 1385632, "name": "Voya Asia Pacific High Dividend Equity Income Fund", "bdc": false, "n_tender": 0, "ticker": "IAE", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/IDE.json b/fundlab/cef_cache/IDE.json new file mode 100644 index 0000000..085297c --- /dev/null +++ b/fundlab/cef_cache/IDE.json @@ -0,0 +1 @@ +{"sym": "IDE", "cik": 1417802, "name": "Voya Infrastructure, Industrials & Materials Fund", "bdc": false, "n_tender": 0, "ticker": "IDE", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/IGA.json b/fundlab/cef_cache/IGA.json new file mode 100644 index 0000000..ed1f8f3 --- /dev/null +++ b/fundlab/cef_cache/IGA.json @@ -0,0 +1 @@ +{"sym": "IGA", "cik": 1332943, "name": "Voya Global Advantage & Premium Opportunity Fund", "bdc": false, "n_tender": 0, "ticker": "IGA", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/IGD.json b/fundlab/cef_cache/IGD.json new file mode 100644 index 0000000..d44e82d --- /dev/null +++ b/fundlab/cef_cache/IGD.json @@ -0,0 +1 @@ +{"sym": "IGD", "cik": 1285890, "name": "Voya GLOBAL EQUITY DIVIDEND & PREMIUM OPPORTUNITY FUND", "bdc": false, "n_tender": 0, "ticker": "IGD", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/IGR.json b/fundlab/cef_cache/IGR.json new file mode 100644 index 0000000..2b06d95 --- /dev/null +++ b/fundlab/cef_cache/IGR.json @@ -0,0 +1 @@ +{"sym": "IGR", "cik": 1268884, "name": "CBRE GLOBAL REAL ESTATE INCOME FUND", "bdc": false, "n_tender": 3, "ticker": "IGR", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/IHD.json b/fundlab/cef_cache/IHD.json new file mode 100644 index 0000000..9a215dd --- /dev/null +++ b/fundlab/cef_cache/IHD.json @@ -0,0 +1 @@ +{"sym": "IHD", "cik": 1496292, "name": "Voya Emerging Markets High Dividend Equity Fund", "bdc": false, "n_tender": 0, "ticker": "IHD", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/JCE.json b/fundlab/cef_cache/JCE.json new file mode 100644 index 0000000..bd63db8 --- /dev/null +++ b/fundlab/cef_cache/JCE.json @@ -0,0 +1 @@ +{"sym": "JCE", "cik": 1385763, "name": "Nuveen Core Equity Alpha Fund", "bdc": false, "n_tender": 0, "ticker": "JCE", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/KF.json b/fundlab/cef_cache/KF.json new file mode 100644 index 0000000..6d9224b --- /dev/null +++ b/fundlab/cef_cache/KF.json @@ -0,0 +1 @@ +{"sym": "KF", "cik": 748691, "name": "KOREA FUND INC", "bdc": false, "n_tender": 0, "ticker": "KF", "report_filed": "2026-08-27", "ncols": 5, "nav_beg": [30.87, 28.78, 26.52, 28.54, 54.37], "nav_end": [87.88, 30.87, 28.78, 26.52, 28.54], "mkt_end": [75.21, 26.93, 24.13, 23.14, 24.35], "dist_tot": [1.3800000000000097, 0.4499999999999993, -3.552713678800501e-15, 3.3200000000000003, 9.11], "divs": [-1.38, -0.45, -0.03, 0.0, -2.05], "gains": [0.0, 0.0, 0.0, -3.27, -7.06], "roc": [-0.02, 0.0, 0.0, 0.0, 0.0], "ops": [58.39, 2.41, 2.17, 1.25, -16.73], "share_div": 0.2741935483870966, "share_gains": 0.7244039270687233, "share_roc": 0.0014025245441795222} \ No newline at end of file diff --git a/fundlab/cef_cache/KYN.json b/fundlab/cef_cache/KYN.json new file mode 100644 index 0000000..6cee612 --- /dev/null +++ b/fundlab/cef_cache/KYN.json @@ -0,0 +1 @@ +{"sym": "KYN", "cik": 1293613, "name": "Kayne Anderson Energy Infrastructure Fund, Inc.", "bdc": false, "n_tender": 29, "ticker": "KYN", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/LENDX.json b/fundlab/cef_cache/LENDX.json new file mode 100644 index 0000000..a6a6640 --- /dev/null +++ b/fundlab/cef_cache/LENDX.json @@ -0,0 +1 @@ +{"sym": "LENDX", "cik": 1658645, "name": "Stone Ridge Trust V", "bdc": false, "n_tender": 40, "ticker": "LENDX", "report_filed": "2026-05-08", "ncols": 5, "nav_beg": [46.09, 46.4, 48.79, 51.23, 57.09], "nav_end": [46.13, 46.09, 46.4, 48.79, 51.23], "mkt_end": [], "dist_tot": [1.99, 1.85, 1.92, 2.54, 18.28], "divs": [-1.99, 0.0, 0.0, -2.54, -7.52], "gains": [0.0, 0.0, 0.0, 0.0, -10.76], "roc": [0.0, -1.85, -1.92, 0.0, 0.0], "ops": [2.03, 1.54, -8.0, -0.47, 0.1], "share_div": 0.4533483822422874, "share_gains": 0.40481565086531224, "share_roc": 0.1418359668924003} \ No newline at end of file diff --git a/fundlab/cef_cache/LTCFX.json b/fundlab/cef_cache/LTCFX.json new file mode 100644 index 0000000..030507d --- /dev/null +++ b/fundlab/cef_cache/LTCFX.json @@ -0,0 +1 @@ +{"sym": "LTCFX", "cik": 1496254, "name": "Alternative Strategies Income Fund", "bdc": false, "n_tender": 60, "ticker": "LTAFX", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/MCI.json b/fundlab/cef_cache/MCI.json new file mode 100644 index 0000000..024d717 --- /dev/null +++ b/fundlab/cef_cache/MCI.json @@ -0,0 +1 @@ +{"sym": "MCI", "cik": 275694, "name": "BARINGS CORPORATE INVESTORS", "bdc": false, "n_tender": 0, "ticker": "MCI", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/MPV.json b/fundlab/cef_cache/MPV.json new file mode 100644 index 0000000..f1b9b63 --- /dev/null +++ b/fundlab/cef_cache/MPV.json @@ -0,0 +1 @@ +{"sym": "MPV", "cik": 831655, "name": "BARINGS PARTICIPATION INVESTORS", "bdc": false, "n_tender": 0, "ticker": "MPV", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/MXF.json b/fundlab/cef_cache/MXF.json new file mode 100644 index 0000000..4d80c70 --- /dev/null +++ b/fundlab/cef_cache/MXF.json @@ -0,0 +1 @@ +{"sym": "MXF", "cik": 65433, "name": "MEXICO FUND INC", "bdc": false, "n_tender": 14, "ticker": "MXF", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/NICHX.json b/fundlab/cef_cache/NICHX.json new file mode 100644 index 0000000..da77185 --- /dev/null +++ b/fundlab/cef_cache/NICHX.json @@ -0,0 +1 @@ +{"sym": "NICHX", "cik": 1736510, "name": "Variant Alternative Income Fund", "bdc": false, "n_tender": 34, "ticker": "NICHX", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/NML.json b/fundlab/cef_cache/NML.json new file mode 100644 index 0000000..d98818f --- /dev/null +++ b/fundlab/cef_cache/NML.json @@ -0,0 +1 @@ +{"sym": "NML", "cik": 1562051, "name": "Neuberger Energy Infrastructure & Income Fund Inc.", "bdc": false, "n_tender": 0, "ticker": "NML", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/NXG.json b/fundlab/cef_cache/NXG.json new file mode 100644 index 0000000..e215226 --- /dev/null +++ b/fundlab/cef_cache/NXG.json @@ -0,0 +1 @@ +{"sym": "NXG", "cik": 1506488, "name": "NXG NextGen Infrastructure Income Fund", "bdc": false, "n_tender": 0, "ticker": "NXG", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/PDX.json b/fundlab/cef_cache/PDX.json new file mode 100644 index 0000000..0e4449d --- /dev/null +++ b/fundlab/cef_cache/PDX.json @@ -0,0 +1 @@ +{"sym": "PDX", "cik": 1756908, "name": "PIMCO Dynamic Income Strategy Fund", "bdc": false, "n_tender": 0, "ticker": "PDX", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/PEO.json b/fundlab/cef_cache/PEO.json new file mode 100644 index 0000000..e921e73 --- /dev/null +++ b/fundlab/cef_cache/PEO.json @@ -0,0 +1 @@ +{"sym": "PEO", "cik": 216851, "name": "ADAMS NATURAL RESOURCES FUND, INC.", "bdc": false, "n_tender": 0, "ticker": "PEO", "report_filed": "2026-02-20", "ncols": 5, "nav_beg": [24.21, 24.83, 25.85, 19.22, 13.76], "nav_end": [24.09, 24.21, 24.83, 25.85, 19.22], "mkt_end": [21.74, 21.74, 20.63, 21.8, 16.52], "dist_tot": [2.05, 1.77, 1.35, 1.63, 0.91], "divs": [-0.56, -0.65, -0.65, -0.79, -0.56], "gains": [-1.49, -1.12, -0.7, -0.84, -0.35], "roc": [0.0, 0.0, 0.0, 0.0, 0.0], "ops": [2.03, 1.27, 0.4, 8.37, 6.41], "share_div": 0.4163424124513619, "share_gains": 0.5836575875486382, "share_roc": 0.0} \ No newline at end of file diff --git a/fundlab/cef_cache/RMT.json b/fundlab/cef_cache/RMT.json new file mode 100644 index 0000000..71e34c7 --- /dev/null +++ b/fundlab/cef_cache/RMT.json @@ -0,0 +1 @@ +{"sym": "RMT", "cik": 912147, "name": "ROYCE MICRO-CAP TRUST, INC.", "bdc": false, "n_tender": 2, "ticker": "RMT", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/SCD.json b/fundlab/cef_cache/SCD.json new file mode 100644 index 0000000..ff766a9 --- /dev/null +++ b/fundlab/cef_cache/SCD.json @@ -0,0 +1 @@ +{"sym": "SCD", "cik": 1270131, "name": "LMP CAPITAL & INCOME FUND INC.", "bdc": false, "n_tender": 0, "ticker": "SCD", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/SRV.json b/fundlab/cef_cache/SRV.json new file mode 100644 index 0000000..f39f3e6 --- /dev/null +++ b/fundlab/cef_cache/SRV.json @@ -0,0 +1 @@ +{"sym": "SRV", "cik": 1400897, "name": "NXG Cushing Midstream Energy Fund", "bdc": false, "n_tender": 0, "ticker": "SRV", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/STEW.json b/fundlab/cef_cache/STEW.json new file mode 100644 index 0000000..111f502 --- /dev/null +++ b/fundlab/cef_cache/STEW.json @@ -0,0 +1 @@ +{"sym": "STEW", "cik": 102426, "name": "SRH Total Return Fund, Inc.", "bdc": false, "n_tender": 1, "ticker": "STEW", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/TWN.json b/fundlab/cef_cache/TWN.json new file mode 100644 index 0000000..e7cc398 --- /dev/null +++ b/fundlab/cef_cache/TWN.json @@ -0,0 +1 @@ +{"sym": "TWN", "cik": 804123, "name": "TAIWAN FUND INC", "bdc": false, "n_tender": 0, "ticker": "TWN", "report_filed": "2025-11-05", "ncols": 5, "nav_beg": [53.78, 39.73, 29.96, 42.83, 28.79], "nav_end": [59.13, 53.78, 39.73, 29.96, 42.83], "mkt_end": [50.25, 44.73, 31.1, 25.2, 35.83], "dist_tot": [7.399999999999999, 0.4399999999999977, 0.0, 2.9199999999999946, 3.3100000000000023], "divs": [0.0, 0.0, 0.0, -0.48, -0.38], "gains": [-7.4, -0.44, 0.0, -2.44, -2.93], "roc": [0.0, 0.0, 0.0, 0.0, 0.0], "ops": [11.92, 13.56, 9.73, -9.95, 17.35], "share_div": 0.061122956645344735, "share_gains": 0.9388770433546558, "share_roc": 0.0} \ No newline at end of file diff --git a/fundlab/cef_cache/TYG.json b/fundlab/cef_cache/TYG.json new file mode 100644 index 0000000..cbb81f1 --- /dev/null +++ b/fundlab/cef_cache/TYG.json @@ -0,0 +1 @@ +{"sym": "TYG", "cik": 1268533, "name": "TORTOISE ENERGY INFRASTRUCTURE CORP", "bdc": false, "n_tender": 17, "ticker": "TYG", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_cache/VCRDX.json b/fundlab/cef_cache/VCRDX.json new file mode 100644 index 0000000..b231ad8 --- /dev/null +++ b/fundlab/cef_cache/VCRDX.json @@ -0,0 +1 @@ +{"sym": "VCRDX", "cik": 1812286, "name": "Harrison Street Infrastructure Income Fund", "bdc": false, "n_tender": 9, "ticker": "VCRDX", "error": "no per-share table found in the 3 latest reports"} \ No newline at end of file diff --git a/fundlab/cef_merged.json b/fundlab/cef_merged.json new file mode 100644 index 0000000..2f8649f --- /dev/null +++ b/fundlab/cef_merged.json @@ -0,0 +1,398 @@ +{ + "TWN": { + "char_actual": 0.66, + "shares": { + "share_div": 0.061122956645344735, + "share_gains": 0.9388770433546558, + "share_roc": 0.0 + }, + "disc_now_approx": -0.1502, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-11-05", + "mkt_end": [ + 50.25, + 44.73, + 31.1, + 25.2, + 35.83 + ], + "nav_end": [ + 59.13, + 53.78, + 39.73, + 29.96, + 42.83 + ] + }, + "PEO": { + "char_actual": 0.45, + "shares": { + "share_div": 0.4163424124513619, + "share_gains": 0.5836575875486382, + "share_roc": 0.0 + }, + "disc_now_approx": -0.0976, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-02-20", + "mkt_end": [ + 21.74, + 21.74, + 20.63, + 21.8, + 16.52 + ], + "nav_end": [ + 24.09, + 24.21, + 24.83, + 25.85, + 19.22 + ] + }, + "KF": { + "char_actual": 0.54, + "shares": { + "share_div": 0.2741935483870966, + "share_gains": 0.7244039270687233, + "share_roc": 0.0014025245441795222 + }, + "disc_now_approx": -0.1442, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-08-27", + "mkt_end": [ + 75.21, + 26.93, + 24.13, + 23.14, + 24.35 + ], + "nav_end": [ + 87.88, + 30.87, + 28.78, + 26.52, + 28.54 + ] + }, + "LENDX": { + "char_actual": 0.47, + "shares": { + "share_div": 0.4533483822422874, + "share_gains": 0.40481565086531224, + "share_roc": 0.1418359668924003 + }, + "disc_now_approx": null, + "n_tender": 40, + "bdc": false, + "report_filed": "2026-05-08", + "mkt_end": [], + "nav_end": [ + 46.13, + 46.09, + 46.4, + 48.79, + 51.23 + ] + }, + "EMO": { + "char_actual": 0.4, + "shares": { + "share_div": 0.6651270207852193, + "share_gains": 0.0, + "share_roc": 0.33487297921478054 + }, + "disc_now_approx": -0.0681, + "n_tender": 6, + "bdc": false, + "report_filed": "2026-01-27", + "mkt_end": [ + 45.55, + 50.49, + 34.5, + 30.43, + 21.65 + ], + "nav_end": [ + 48.88, + 55.82, + 38.75, + 35.97, + 26.53 + ] + }, + "EMF": { + "char_actual": 0.4, + "shares": { + "share_div": 0.5065502183406113, + "share_gains": 0.4934497816593886, + "share_roc": 0.0 + }, + "disc_now_approx": -0.1158, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-10-30", + "mkt_end": [ + 15.27, + 12.81, + 11.71, + 11.85, + 17.89 + ], + "nav_end": [ + 17.27, + 14.81, + 13.63, + 13.72, + 20.09 + ] + }, + "ADX": { + "char_actual": 0.65, + "shares": { + "share_div": 0.08865979381443298, + "share_gains": 0.9113402061855671, + "share_roc": 0.0 + }, + "disc_now_approx": -0.0566, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-02-20", + "mkt_end": [ + 23.32, + 20.2, + 17.71, + 14.54, + 19.41 + ], + "nav_end": [ + 24.72, + 22.64, + 20.56, + 17.38, + 22.5 + ] + }, + "HQL": { + "char_actual": 0.49, + "shares": { + "share_div": 0.36363636363636365, + "share_gains": 0.6098484848484849, + "share_roc": 0.026515151515151516 + }, + "disc_now_approx": -0.1061, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-12-08", + "mkt_end": [ + 15.51, + 15.08, + 12.47, + 13.66, + 20.8 + ], + "nav_end": [ + 17.35, + 16.38, + 15.0, + 15.49, + 21.22 + ] + }, + "HQH": { + "char_actual": 0.51, + "shares": { + "share_div": 0.3466257668711656, + "share_gains": 0.583844580777096, + "share_roc": 0.06952965235173823 + }, + "disc_now_approx": -0.0629, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-12-08", + "mkt_end": [ + 18.46, + 18.62, + 15.55, + 17.28, + 25.57 + ], + "nav_end": [ + 19.7, + 20.33, + 18.84, + 19.36, + 25.47 + ] + }, + "AEF": { + "char_actual": 0.82, + "shares": { + "share_div": 0.19999999999999996, + "share_gains": 0.0, + "share_roc": 0.7999999999999999 + }, + "disc_now_approx": -0.0826, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-03-09", + "mkt_end": [ + 7.0, + 5.19, + 5.11, + 5.15, + 7.92 + ], + "nav_end": [ + 7.63, + 5.96, + 5.96, + 5.78, + 8.7 + ] + }, + "ASGI": { + "char_actual": 0.55, + "shares": { + "share_div": 0.3477751756440281, + "share_gains": 0.4508196721311476, + "share_roc": 0.20140515222482439 + }, + "disc_now_approx": -0.0177, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-12-08", + "mkt_end": [ + 21.13, + 20.21, + 16.1, + 15.73, + 19.93 + ], + "nav_end": [ + 21.51, + 21.17, + 19.16, + 18.93, + 22.27 + ] + }, + "AOD": { + "char_actual": 0.27, + "shares": { + "share_div": 0.8126361655773419, + "share_gains": 0.0, + "share_roc": 0.18736383442265794 + }, + "disc_now_approx": 0.0, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-01-08", + "mkt_end": [ + 11.63, + 10.16, + 8.4, + 8.92, + 12.01 + ], + "nav_end": [ + 11.63, + 11.15, + 9.9, + 10.05, + 12.95 + ] + }, + "AGD": { + "char_actual": 0.27, + "shares": { + "share_div": 0.8126361655773419, + "share_gains": 0.0, + "share_roc": 0.18736383442265794 + }, + "disc_now_approx": 0.0, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-01-08", + "mkt_end": [ + 11.63, + 10.16, + 8.4, + 8.92, + 12.01 + ], + "nav_end": [ + 11.63, + 11.15, + 9.9, + 10.05, + 12.95 + ] + }, + "EOD": { + "char_actual": 0.58, + "shares": { + "share_div": 0.47131147540983603, + "share_gains": 0.0, + "share_roc": 0.5286885245901639 + }, + "disc_now_approx": null, + "n_tender": 0, + "bdc": false, + "report_filed": "2026-01-05", + "mkt_end": [], + "nav_end": [ + 6.44, + 5.53, + 4.52, + 4.57, + 6.03 + ] + }, + "CSQ": { + "char_actual": 0.53, + "shares": { + "share_div": 0.36713286713286714, + "share_gains": 0.45454545454545464, + "share_roc": 0.17832167832167836 + }, + "disc_now_approx": null, + "n_tender": 5, + "bdc": false, + "report_filed": "2025-06-27", + "mkt_end": [], + "nav_end": [ + 9.32, + 10.04, + 8.99 + ] + }, + "HERZ": { + "char_actual": 0.94, + "shares": { + "share_div": 0.0, + "share_gains": 0.2098765432098765, + "share_roc": 0.7901234567901234 + }, + "disc_now_approx": -0.0453, + "n_tender": 0, + "bdc": false, + "report_filed": "2025-09-08", + "mkt_end": [ + 2.53, + 2.35, + 3.95, + 4.01, + 6.27 + ], + "nav_end": [ + 2.65, + 3.1, + 4.98, + 4.63, + 7.06 + ] + } +} \ No newline at end of file diff --git a/tests/test_fundlab.py b/tests/test_fundlab.py index fda8921..a6d259d 100644 --- a/tests/test_fundlab.py +++ b/tests/test_fundlab.py @@ -610,6 +610,186 @@ def test_cef() -> None: {"aaa", "bbb", "ccc"} <= set(sel), str(sel)) + +# CEF annual-report per-share highlights parser +# ---------------------------------------------------------------------- + +_DOC_FRANKLIN = """\ +Financial highlights For a common share of capital stock outstanding +throughout each year ended November 30: 2025 1 2024 1 2023 1 2022 1 2021 1 +Net +asset value, beginning of year +$55.82 +$38.75 +$35.97 +$26.53 +$17.13 +Income +(loss) from operations: +Net +investment loss +(0.13 +) +(0.51 +) +(0.69 +) +(0.43 +) +(0.36 +) +Net +realized and unrealized gain (loss) +(2.12 +) +20.58 +5.80 +11.65 +11.01 +Total +income (loss) from operations +(2.25) +20.07 +5.11 +11.22 +10.65 +Less +distributions to common +shareholders +from: +Dividends +(0.81 +) +(3.00 +) +(2.37 +) +(1.92 +) +(0.54 +) +Return of capital +(3.42 +) +(0.93 +) +Total distributions to common shareholders +4.23 +3.00 +2.37 +1.92 +1.47 +Anti-dilutive impact of repurchase plan 0.04 0.14 0.22 +Dilutive impact of rights offering (0.46) (0.27) (0.13) (0.09) (0.13) +Anti-dilutive impact of tender offer plan 0.00 0.00 0.00 0.00 0.00 +Net asset value, end of year $48.88 $55.82 $38.75 $35.97 $26.53 +Market price, end of year $45.55 $50.49 $34.50 $30.43 $21.65 +""" + +_DOC_CLASSIC = """\ +Selected Per Share Data Net asset value, beginning of year $ 53.78 $ 39.73 +$ 29.96 $ 42.83 $ 28.79 Income from Investment Operations: Net investment +income (a) 0.30 0.43 0.41 (b) 0.33 0.04 Net realized and unrealized gain +(loss) on investments and foreign currency transactions 11.62 13.13 9.32 (c) +(10.28) 17.31 Total from investment operations 11.92 13.56 9.73 (9.95) 17.35 +Less Distributions to Stockholders from: Net investment income (0.48) (0.38) +Net realized gains (7.40) (0.44) (2.44) (2.93) Total distributions to +stockholders (7.40) (0.44) (2.92) (3.31) Capital Share Transactions: +Accretion (dilution) to net asset value, resulting from share repurchase +program, tender offer or issuance of shares for the reinvestment of +distributions from net investment income and net realized gains 0.83 0.93 +0.04 0.00 (d) 0.00 (d) Net asset value, end of year $ 59.13 $ 53.78 $ 39.73 +$ 29.96 $ 42.83 Market value, end of year $ 50.25 $ 44.73 $ 31.10 $ 25.20 +$ 35.83 +""" + +_DOC_ABRDN = """\ +PER SHARE OPERATING PERFORMANCE: Net asset value per common share, beginning +of year $16.38 $15.00 $15.49 $21.22 $20.25 Net investment loss (c) (0.12) +(0.07) (0.08) (0.12) (0.17) Net realized and unrealized gains/(losses) on +investments, written options and foreign currency transactions 2.91 3.11 0.87 +(4.14) 2.83 Total from investment operations applicable to common +shareholders 2.79 3.04 0.79 (4.26) 2.66 Distributions to common shareholders +from: Net investment income (1.72) (0.69) (0.02) (0.03) (0.42) Net realized +gains (0.10) (0.76) (1.26) (1.44) (1.27) Return of capital (0.21) Total +distributions (1.82) (1.66) (1.28) (1.47) (1.69) Net asset value per common +share, end of year $17.35 $16.38 $15.00 $15.49 $21.22 Market price, end of +year $15.51 $15.08 $12.47 $13.66 $20.80 +""" + +_DOC_KOREA = """\ +The Korea Fund, Inc. Financial Highlights For a share of stock outstanding +throughout each year: Year ended June 30, 2026 2025 2024 2023 2022 Net +asset value, beginning of year $30.87 $28.78 $26.52 $28.54 $54.37 Investment +Operations: Net investment income (1) (0.05) 0.02 0.09 0.19 0.32 Net realized +and change in unrealized gain (loss) 58.44 2.39 2.08 1.06 (17.05) Total from +investment operations 58.39 2.41 2.17 1.25 (16.73) Dividends and +Distributions to Stockholders from: Net investment income (1.38) (0.45) +(0.03) (2.05) Net realized gains (3.27) (7.06) Return of capital (0.02) +Total dividends and distributions to stockholders (1.38) (0.45) (3.32) (9.11) +Common Stock Transactions: Accretion to net asset value resulting from share +repurchases and tender offer 0.13 0.09 0.05 0.01 Net asset value, end of +year $87.88 $30.87 $28.78 $26.52 $28.54 Market price, end of year $75.21 +$26.93 $24.13 $23.14 $24.35 +""" + + +def test_cef_annual() -> None: + from fundlab import cef_annual as cn + + # Franklin style (EMO): td-split labels, ROC in cols 0 and 4, + # positive-printed total row + h = cn.parse_highlights(_DOC_FRANKLIN) + check("franklin parses 5 cols", h is not None and h["ncols"] == 5, str(h)) + check("franklin div share ~66.5%", + abs(h["share_div"] - 8.64 / 12.99) < 0.005, str(h["share_div"])) + check("franklin gains zero", h["share_gains"] == 0.0) + check("franklin roc share ~33.5%", + abs(h["share_roc"] - 4.35 / 12.99) < 0.005, str(h["share_roc"])) + check("franklin FY-end discount ~-6.8%", + abs(h["mkt_end"][0] / h["nav_end"][0] - 1 - (-0.0681)) < 0.001) + + # Classic CEF (TWN): printed total row is SHORT (a zero year dropped), + # so totals derive from the NAV identity; gains sit in 4 of 5 cols + # and NII-divs in the OTHER two + h = cn.parse_highlights(_DOC_CLASSIC) + check("classic parses 5 cols", h is not None and h["ncols"] == 5, str(h)) + check("classic derived total col0 = 7.40", + abs(h["dist_tot"][0] - 7.40) < 0.01, str(h["dist_tot"])) + check("classic zero-year col2", abs(h["dist_tot"][2]) < 0.01, + str(h["dist_tot"])) + check("classic gains ~93.9%", + abs(h["share_gains"] - 13.21 / 14.07) < 0.005, str(h["share_gains"])) + check("classic NII ~6.1%", + abs(h["share_div"] - 0.86 / 14.07) < 0.005, str(h["share_div"])) + + # abrdn style (HQL): "per common share" labels, single-value ROC row + h = cn.parse_highlights(_DOC_ABRDN) + check("abrdn parses 5 cols", h is not None and h["ncols"] == 5, str(h)) + check("abrdn div ~36.4%", + abs(h["share_div"] - 2.88 / 7.92) < 0.005, str(h["share_div"])) + check("abrdn gains ~61.0%", + abs(h["share_gains"] - 4.83 / 7.92) < 0.005, str(h["share_gains"])) + check("abrdn roc ~2.7%", + abs(h["share_roc"] - 0.21 / 7.92) < 0.005, str(h["share_roc"])) + + # Korea style (KF): "Total dividends and distributions" label (the + # divs regex must not grab the total row), accretion row in cols 1-4 + h = cn.parse_highlights(_DOC_KOREA) + check("korea parses 5 cols", h is not None and h["ncols"] == 5, str(h)) + check("korea div ~27.4%", + abs(h["share_div"] - 3.91 / 14.26) < 0.01, str(h["share_div"])) + check("korea gains ~72.4%", + abs(h["share_gains"] - 10.33 / 14.26) < 0.01, + str(h["share_gains"])) + + # unreconcilable table must be rejected, not guessed + bad = _DOC_CLASSIC.replace("(7.40) (0.44) (2.44) (2.93)", + "(7.99) (0.44) (2.44) (2.93)") + check("unreconcilable table rejected", + cn.parse_highlights(bad) is None) + + def main() -> int: test_pool() test_text_and_objective() @@ -625,6 +805,7 @@ def main() -> int: test_taxplan() test_drawdown() test_cef() + test_cef_annual() test_edgar_live() print(f"\n{PASS} passed, {FAIL} failed") return 1 if FAIL else 0 @@ -632,3 +813,6 @@ def main() -> int: if __name__ == "__main__": sys.exit(main()) + + +# ---------------------------------------------------------------------- \ No newline at end of file