Financial analysis tools
Go to file
Greg Pomerantz 328855a926 Fund report: 11 candidates + 13 shortlist funds, self-contained HTML
fundlab/report.py -> reports/fund_report.html (20 MB, plotly inlined,
opens offline). Per fund: max-history equity curve (fund vs fitted
reference vs IVV); performance table (full/5y/1y, the 5 market
episodes, calendar years) with the fund-minus-reference period-alpha
column; drivers (reference-model R²/alpha/t + 34-sleeve signature +
curated decomposition verdict and N-PORT cross-check notes); the
reference mix explained sleeve-by-sleeve (what each exposure actually
is, plus net-cash/net-levered read); tax character + taxable/IRA
placement; and a peer table of the 4 best funds in the same k=30
return-driver cluster with computed advantages/disadvantages.

Weak-fit (R²<0.5) funds anchor their tables to CASH rather than the
statistically-thin forward-selected mix (which can be an offsetting
VIX/duration spec combination whose path is meaningless); the loadings
are still shown with a 'weak fit' caveat.
2026-08-30 17:00:41 -04:00
fundlab Fund report: 11 candidates + 13 shortlist funds, self-contained HTML 2026-08-30 17:00:41 -04:00
reports Fund report: 11 candidates + 13 shortlist funds, self-contained HTML 2026-08-30 17:00:41 -04:00
tests CEF stage 2b: per-share financial-highlights parser (cef_annual.py) 2026-08-28 07:30:16 -04:00
.gitignore N-PORT cross-check: verify top candidates' actual holdings 2026-08-27 12:43:17 -04:00
adx-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
aef-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
app.py Compute all alphas in excess of the 3-mo T-bill rate (BIL) 2026-08-30 15:50:15 -04:00
asa-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
brw-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
bto-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
chart_widget.py Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
clm-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
crf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
data.py app: background cache refresh, per-benchmark stats, correlation tab, global date range 2026-08-25 18:15:47 -04:00
evg-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
families.py Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
fxby-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
grf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
herz-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
iaf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
kf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
mci-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
metrics.py app: background cache refresh, per-benchmark stats, correlation tab, global date range 2026-08-25 18:15:47 -04:00
mxf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
nro-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
peo-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
portfolio.py Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
portfolios.json Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
portfolios.py Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
README.md data: manifest-based incremental refresh of the parquet cache 2026-08-24 17:28:20 -04:00
requirements.txt Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
run_tests.sh data: manifest-based incremental refresh of the parquet cache 2026-08-24 17:28:20 -04:00
run.sh Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
rvt-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
saba-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
swz-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
tax.py Stock & Portfolio Analyzer: full UI rework 2026-08-24 16:05:27 -04:00
tyg-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
utf-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
vlt-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00
ztr-split.csv CEF pass: universe (SEC report, 973 -> 295 listed) + stage 1 screen + stage 2a character 2026-08-27 22:24:12 -04:00

Stock & Portfolio Analyzer

Interactive tool for analyzing individual securities and portfolios against local Yahoo Finance dumps (~/prog/fin/stocks, ~4k symbols).

Quick start

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
./run.sh    # serves the UI on the fixed port 8599 (http://localhost:8599)

First run builds a parquet cache in .cache/ (~1 min for 4k symbols); later runs load in well under a second. The cache tracks the data dir per-file (mtime + size in .cache/manifest.json), so when the download is updated, only the changed/added/removed symbols are re-read — a partial refresh takes seconds instead of a full ~1 min rebuild.

Modules

Module Purpose
data.py Ingest {sym}-history/dividend/capitalGain.csv -> cached parquet panels (date x symbol), with manifest-based incremental refresh when the data dir changes. Adj Close already includes distributions, so it drives pre-tax total returns.
metrics.py Total/annualized return, vol, Sharpe, Sortino, max drawdown, Calmar, CAPM beta/alpha. Pure pandas, all transparent.
portfolio.py Weighted portfolios with drift and periodic rebalancing to target weights (1W/1ME/QE/YE), one-way cost in bps. Spec grammar: commas join the elements of ONE portfolio (SYM or SYM:w, bare = equal weight), spaces separate DISTINCT symbols/portfolios (parse_items).
tax.py Simplified DAS after-tax engine: FIFO lots, 365-day long/short split, separate LT/ST/dividend rates. Headline curve = what you keep if you sell everything today (unrealized gains taxed daily by lot age).
chart_widget.py Self-contained plotly.js chart in an iframe: mouse zoom/pan, x clamped to the data, view edges snapped to first/last data points with day-precise labels, y tight-fit, every line re-based to 1.0 at the left edge.
portfolios.py Saved portfolio definitions in portfolios.json (name, spec, scheme, cost).
settings.json Persisted UI inputs (symbol/benchmark specs, scheme, costs, tax rates, period, curve/window mode) — restored on every page load and server restart; delete to reset.
app.py Streamlit UI: single "symbol or portfolio" spec field (page updates as soon as the input is valid; unknown symbols get click-to-fix "did you mean" suggestions) + a benchmark box with the same grammar (one benchmark per line; a line is a single symbol or a comma-joined portfolio, simulated with the same scheme/cost/tax rules — pre- and after-tax curves, first one drives beta/alpha), scheme/costs/tax rates, save + load/compare/delete portfolios (overlaid pre/after-tax curves), curve toggle (both / pre-tax only / after-tax only), stats table, allocation, per-year tax detail.

Development

  • Run: ./run.shhttp://localhost:8599 (fixed port; no-ops if a server is already running). The chart loads plotly.js from a CDN; for fully offline use set F_INLINE_PLOTLY=1 in run.sh.
  • Test: ./run_tests.sh
    1. tests/test_app.py — app-level tests via Streamlit AppTest (no browser). Memory: one data bundle is ~2.3 GB, so this process keeps at most ONE AppTest alive (see its header comment).
    2. tests/test_e2e_browser.py — Playwright + headless Chromium driving the real page with real keystrokes; needs the server running on 8599. One-time setup: .venv/bin/pip install playwright and .venv/bin/python -m playwright install chromium.
  • Gotchas
    • Streamlit caches imported modules per process: restart the server after editing any .py (kill the old one first — run.sh refuses to double-start).
    • st.cache_data caches the portfolio + tax simulations: they recompute only when symbols/scheme/cost/tax rates change, not on window or curve toggles.
    • settings.json (gitignored) persists UI inputs across reloads and restarts; delete it to reset. Saved portfolios live in portfolios.json.
    • Data cache: .cache/*.parquet; rebuild via the sidebar checkbox (first build ~1 min for ~4k symbols).

Known simplifications (roadmap)

  • No loss carryover or carryforward across years; no wash-sale rules.
  • Distributed capital gains taxed entirely at the long-term rate.
  • Single (federal-like) tax bracket; no state taxes, no AMT.
  • Equal treatment of benchmark for beta/alpha (CAPM, rf = 0 by default).

Ideas: vectorbt sweeps over rebalance schemes, NiceGUI/Textual frontend, empyrical-reloaded metrics, monthly (not yearly) loss netting, tax-loss harvesting simulation.