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:
parent
d1e85026bf
commit
6f8bde4c35
|
|
@ -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 <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
1446
fundlab/cef_annual.json
Normal file
File diff suppressed because it is too large
Load Diff
471
fundlab/cef_annual.py
Normal file
471
fundlab/cef_annual.py
Normal 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
102
fundlab/cef_annual_run.log
Normal 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
|
||||
12
fundlab/cef_annual_run2.log
Normal file
12
fundlab/cef_annual_run2.log
Normal 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
102
fundlab/cef_annual_run3.log
Normal 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
|
||||
1
fundlab/cef_cache/ADX.json
Normal file
1
fundlab/cef_cache/ADX.json
Normal 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}
|
||||
1
fundlab/cef_cache/AEF.json
Normal file
1
fundlab/cef_cache/AEF.json
Normal 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}
|
||||
1
fundlab/cef_cache/AGD.json
Normal file
1
fundlab/cef_cache/AGD.json
Normal 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}
|
||||
1
fundlab/cef_cache/AIO.json
Normal file
1
fundlab/cef_cache/AIO.json
Normal 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"}
|
||||
1
fundlab/cef_cache/AOD.json
Normal file
1
fundlab/cef_cache/AOD.json
Normal 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}
|
||||
1
fundlab/cef_cache/ASA.json
Normal file
1
fundlab/cef_cache/ASA.json
Normal 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"}
|
||||
1
fundlab/cef_cache/ASCIX.json
Normal file
1
fundlab/cef_cache/ASCIX.json
Normal 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"}
|
||||
1
fundlab/cef_cache/ASGI.json
Normal file
1
fundlab/cef_cache/ASGI.json
Normal 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}
|
||||
1
fundlab/cef_cache/BXSY.json
Normal file
1
fundlab/cef_cache/BXSY.json
Normal 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"}
|
||||
1
fundlab/cef_cache/CET.json
Normal file
1
fundlab/cef_cache/CET.json
Normal 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"}
|
||||
1
fundlab/cef_cache/CLM.json
Normal file
1
fundlab/cef_cache/CLM.json
Normal 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"}
|
||||
1
fundlab/cef_cache/CRF.json
Normal file
1
fundlab/cef_cache/CRF.json
Normal 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"}
|
||||
1
fundlab/cef_cache/CSQ.json
Normal file
1
fundlab/cef_cache/CSQ.json
Normal 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}
|
||||
1
fundlab/cef_cache/DXYZ.json
Normal file
1
fundlab/cef_cache/DXYZ.json
Normal 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"}
|
||||
1
fundlab/cef_cache/ECAT.json
Normal file
1
fundlab/cef_cache/ECAT.json
Normal 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"}
|
||||
1
fundlab/cef_cache/EDF.json
Normal file
1
fundlab/cef_cache/EDF.json
Normal 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"}
|
||||
1
fundlab/cef_cache/EMF.json
Normal file
1
fundlab/cef_cache/EMF.json
Normal 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}
|
||||
1
fundlab/cef_cache/EMO.json
Normal file
1
fundlab/cef_cache/EMO.json
Normal 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}
|
||||
1
fundlab/cef_cache/EOD.json
Normal file
1
fundlab/cef_cache/EOD.json
Normal 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}
|
||||
1
fundlab/cef_cache/ETG.json
Normal file
1
fundlab/cef_cache/ETG.json
Normal 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"}
|
||||
1
fundlab/cef_cache/FUND.json
Normal file
1
fundlab/cef_cache/FUND.json
Normal 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"}
|
||||
1
fundlab/cef_cache/HERZ.json
Normal file
1
fundlab/cef_cache/HERZ.json
Normal 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}
|
||||
1
fundlab/cef_cache/HQH.json
Normal file
1
fundlab/cef_cache/HQH.json
Normal 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}
|
||||
1
fundlab/cef_cache/HQL.json
Normal file
1
fundlab/cef_cache/HQL.json
Normal 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}
|
||||
1
fundlab/cef_cache/IAE.json
Normal file
1
fundlab/cef_cache/IAE.json
Normal 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"}
|
||||
1
fundlab/cef_cache/IDE.json
Normal file
1
fundlab/cef_cache/IDE.json
Normal 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"}
|
||||
1
fundlab/cef_cache/IGA.json
Normal file
1
fundlab/cef_cache/IGA.json
Normal 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"}
|
||||
1
fundlab/cef_cache/IGD.json
Normal file
1
fundlab/cef_cache/IGD.json
Normal 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"}
|
||||
1
fundlab/cef_cache/IGR.json
Normal file
1
fundlab/cef_cache/IGR.json
Normal 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"}
|
||||
1
fundlab/cef_cache/IHD.json
Normal file
1
fundlab/cef_cache/IHD.json
Normal 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"}
|
||||
1
fundlab/cef_cache/JCE.json
Normal file
1
fundlab/cef_cache/JCE.json
Normal 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"}
|
||||
1
fundlab/cef_cache/KF.json
Normal file
1
fundlab/cef_cache/KF.json
Normal 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}
|
||||
1
fundlab/cef_cache/KYN.json
Normal file
1
fundlab/cef_cache/KYN.json
Normal 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"}
|
||||
1
fundlab/cef_cache/LENDX.json
Normal file
1
fundlab/cef_cache/LENDX.json
Normal 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}
|
||||
1
fundlab/cef_cache/LTCFX.json
Normal file
1
fundlab/cef_cache/LTCFX.json
Normal 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"}
|
||||
1
fundlab/cef_cache/MCI.json
Normal file
1
fundlab/cef_cache/MCI.json
Normal 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"}
|
||||
1
fundlab/cef_cache/MPV.json
Normal file
1
fundlab/cef_cache/MPV.json
Normal 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"}
|
||||
1
fundlab/cef_cache/MXF.json
Normal file
1
fundlab/cef_cache/MXF.json
Normal 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"}
|
||||
1
fundlab/cef_cache/NICHX.json
Normal file
1
fundlab/cef_cache/NICHX.json
Normal 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"}
|
||||
1
fundlab/cef_cache/NML.json
Normal file
1
fundlab/cef_cache/NML.json
Normal 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"}
|
||||
1
fundlab/cef_cache/NXG.json
Normal file
1
fundlab/cef_cache/NXG.json
Normal 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"}
|
||||
1
fundlab/cef_cache/PDX.json
Normal file
1
fundlab/cef_cache/PDX.json
Normal 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"}
|
||||
1
fundlab/cef_cache/PEO.json
Normal file
1
fundlab/cef_cache/PEO.json
Normal 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}
|
||||
1
fundlab/cef_cache/RMT.json
Normal file
1
fundlab/cef_cache/RMT.json
Normal 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"}
|
||||
1
fundlab/cef_cache/SCD.json
Normal file
1
fundlab/cef_cache/SCD.json
Normal 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"}
|
||||
1
fundlab/cef_cache/SRV.json
Normal file
1
fundlab/cef_cache/SRV.json
Normal 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"}
|
||||
1
fundlab/cef_cache/STEW.json
Normal file
1
fundlab/cef_cache/STEW.json
Normal 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"}
|
||||
1
fundlab/cef_cache/TWN.json
Normal file
1
fundlab/cef_cache/TWN.json
Normal 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}
|
||||
1
fundlab/cef_cache/TYG.json
Normal file
1
fundlab/cef_cache/TYG.json
Normal 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"}
|
||||
1
fundlab/cef_cache/VCRDX.json
Normal file
1
fundlab/cef_cache/VCRDX.json
Normal 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
398
fundlab/cef_merged.json
Normal 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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
Loading…
Reference in New Issue
Block a user