- No fixed risk-free rate: the statistics page now nets Sharpe, Sortino
and CAPM alpha against BIL (SPDR 1-3 Month T-Bill) daily total
returns from the data bundle — the same reference the Fund Lab uses
for all its alphas (pre-2007 dates fill 0, as in fundlab.decompose).
- metrics: sharpe/sortino/beta_alpha/summary accept a daily rf SERIES
(or scalar annual rate as before) via a shared excess() helper.
- Also fixes a latent double-count: the old scalar-rf alpha subtracted
rf twice (once in the returns, once in the intercept term); alpha is
now mean(excess fund) - beta * mean(excess bench), the standard CAPM
intercept on excess returns.
- Page caption states the T-bill reference (or warns if BIL is absent).
- Tests: series-rf identities (sharpe/sortino/beta/alpha), benchmark
self-row beta 1 / alpha 0, caption check.
- data.py: non-blocking load_bundle(); background watcher thread refreshes
the parquet cache (5s scan, 30s min rebuild cadence); refresh()/
up_to_date()/generation()
- statistics tab: one table per benchmark (vs <label>), plain column names
(beta/alpha/return/vol...), selectable+reorderable stat list in
settings.json
- correlation tab: per-portfolio components-vs-benchmarks +
all-portfolios-vs-benchmarks; numbered columns
- global date range (window radio + start/end boxes) applied to all tabs;
metrics.xcorr(); equity window radio gains YTD/3M/1M
- single spec grammar for symbol and benchmark fields: commas join one
portfolio (MSFT:0.6,V:0.4), spaces separate distinct symbols/portfolios;
both fields accept one or many entries
- benchmarks simulated with the same scheme/cost/tax rules; per-benchmark
beta/alpha columns; after-tax benchmark curves
- global Curve mode (pre/after/both) above the tabs; clean names in
single-curve mode
- live updates: field commits on Enter/blur, page recomputes per rerun;
portfolio+tax sims cached (st.cache_data); plotly.js from CDN (4.6MB ->
browser-cached) with F_INLINE_PLOTLY=1 offline fallback
- chart: legend underneath, solid lines, pan sticks to data edges
(width-preserving), zoom edge-clamped
- inputs persist in settings.json across reloads/restarts/devices
- tests: tests/test_app.py (AppTest) + tests/test_e2e_browser.py
(Playwright) via ./run_tests.sh