From 6f6d6b8047e49408c8ebde3a27a7f911625b89f5 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Fri, 28 Aug 2026 16:04:59 -0400 Subject: [PATCH] CEF app tab: full 239-fund verified tax-arb ranking --- app.py | 57 +++++++++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/app.py b/app.py index ea550a1..82c21a7 100644 --- a/app.py +++ b/app.py @@ -1110,51 +1110,36 @@ with tab_fundlab: "table is a rough keyword classification. N-PORT holdings are " "quarterly and up to ~60 days stale.") - # --- closed-end funds (tax-arb shortlist) --------------------------- + # --- closed-end funds (tax-arb, actual distribution character) ------ import pandas as _pd _fl_dir = Path(__file__).parent / "fundlab" - _char_path = _fl_dir / "cef_character.json" - if _char_path.exists(): - _char = json.loads(_char_path.read_text()) - _annual = {} - _ann_path = _fl_dir / "cef_annual.json" - if _ann_path.exists(): - _annual = json.loads(_ann_path.read_text()) + _rank_path = _fl_dir / "cef_rank_all.json" + if _rank_path.exists(): + _rank = json.loads(_rank_path.read_text()) + _univ = json.loads((_fl_dir / "cef_universe.json").read_text()) _rows = [] - for _s, _c in _char.items(): - _a = _annual.get(_s.upper(), {}) - _ch = _c.get("character") - _actual = False - if _a.get("char_actual") is not None: - _ch = _a["char_actual"] - _actual = True - _t5, _vol = _c.get("t5"), _c.get("vol5") - _score = (None if _ch is None or _t5 is None or _vol is None - else round(_ch * (_t5 + 0.4 * _vol), 4)) - _disc = _a.get("disc_now_approx") + for _score, _s, _ch, _t5, _vol, _npos, _tend, _disc in _rank: _rows.append({ - "sym": _s.upper(), - "name": (_a.get("name") or _c.get("name") or "")[:44], + "sym": _s, + "name": (_univ.get(_s, {}).get("name") or "")[:44], "char": _ch, - "actual": _actual, "disc%": None if _disc is None else round(100 * _disc, 1), "t5%": None if _t5 is None else round(100 * _t5, 1), "vol5%": None if _vol is None else round(100 * _vol, 1), - "pos_scen": _c.get("n_pos_scen"), - "tenders": _a.get("n_tender"), + "pos_scen": _npos, + "tenders": _tend, "score": _score}) _cef_df = _pd.DataFrame(_rows) st.divider() - st.subheader("Closed-end funds — tax-arb shortlist") - st.dataframe( - _cef_df.sort_values("score", ascending=False, na_position="last"), - width="stretch", height=460) + st.subheader("Closed-end funds — tax-arb ranking") + st.dataframe(_cef_df, width="stretch", height=460) st.caption( - "char = distribution character 0–1 (0 = all ordinary income, 1 = " - "all capital-gains/ROC). actual = verified from the fund's own " - "per-share financial highlights (divs + gains + ROC == total " - "distributions per year, plus the full NAV chain); otherwise the " - "return-sleeve model. disc% = market vs NAV. pos_scen = severe-" - "drawdown scenarios with a positive 5y return. score = char × " - "(5y total return + 0.4 × 5y vol) — harvestable character " - "weighted by damage potential.") + f"{len(_cef_df)} CEFs with VERIFIED per-share distribution " + "character (divs + gains + ROC == total distributions per year, " + "plus the NAV chain, from the fund's own report). char 0–1: " + "0 = all ordinary income, 1 = all capital-gains/ROC (taxable " + "income that defers to the LTCG/ROC rate in a taxable account). " + "score = char × (5y total return + 0.4 × 5y vol): harvestable " + "character weighted by how much damage a forced sale could do. " + "tenders = N-23C repurchase/tender filings (sponsor capital-" + "management pressure). disc% = FY-end market vs NAV.")