CEF stage 2b: per-share financial-highlights parser (cef_annual.py)

Joint backtracking placement for CEF tables that drop zero columns;
NAV-identity fallback when the printed total row is short; 4 fund-
family layouts verified (Franklin/classic/abrdn/Korea). 16/50 of the
CEF shortlist now have a verified div/gains/ROC split + FY-end
discount series. 17 new parser unit tests (123/123 green).
This commit is contained in:
Greg Pomerantz 2026-08-28 07:30:16 -04:00
parent d1e85026bf
commit 6f8bde4c35
58 changed files with 2816 additions and 5 deletions

View File

@ -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 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). 0.29-0.39 (income-heavy -> IRA-tilted, ROC share pending the 1099).
**Remaining (stage 2b+):** per-fund N-2ASR/N-CSR distribution **Stage 2b** (fundlab/cef_annual.py): per-share financial-highlights
character (the income/gains/ROC split is decisive for the EMO parser for the annual shareholder report (N-CSR/N-CSRS). 16/50 of the
family), NPORT NAV-per-share -> quarterly discount series, leverage shortlist parsed (4 fund-family layouts verified: Franklin/EMO,
from NPORT financials, BDC flag via form history, tender-offer classic CEF/TWN, abrdn/HQL, Korea/KF; the rest reject conservatively
status; app CEF tab. - mis-parse is worse than no data).
Parser lessons (each one cost a debug cycle):
- CEF HTML tables flatten with one <td> 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.

1446
fundlab/cef_annual.json Normal file

File diff suppressed because it is too large Load Diff

471
fundlab/cef_annual.py Normal file
View File

@ -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 <td> 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)

102
fundlab/cef_annual_run.log Normal file
View File

@ -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

View File

@ -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 "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/mnt/Backup/prog/f/fundlab/cef_annual.py", line 462, in <module>
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

102
fundlab/cef_annual_run3.log Normal file
View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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"}

View File

@ -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}

View File

@ -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"}

View File

@ -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"}

398
fundlab/cef_merged.json Normal file
View File

@ -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
]
}
}

View File

@ -610,6 +610,186 @@ def test_cef() -> None:
{"aaa", "bbb", "ccc"} <= set(sel), str(sel)) {"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: def main() -> int:
test_pool() test_pool()
test_text_and_objective() test_text_and_objective()
@ -625,6 +805,7 @@ def main() -> int:
test_taxplan() test_taxplan()
test_drawdown() test_drawdown()
test_cef() test_cef()
test_cef_annual()
test_edgar_live() test_edgar_live()
print(f"\n{PASS} passed, {FAIL} failed") print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0 return 1 if FAIL else 0
@ -632,3 +813,6 @@ def main() -> int:
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())
# ----------------------------------------------------------------------