# folioperf

Investors rarely know their real return. folioperf reads your transactions and price history (funds, stocks, EPF/PPF, FDs) and computes the money-weighted return (XIRR), the time-weighted return, benchmark-equivalent returns using your own cash flows, attribution of where the return came from, and the cost of fees. It runs as a CLI and writes a self-contained HTML report.

## Features

- Holdings reconstruction: SIPs, sells with average cost, paired switches, splits, bonus, reinvested and cash dividends, balance instruments (EPF, PPF, FD), price forward-fill.
- XIRR for the portfolio, account, instrument, asset class, category or any date window (opening value becomes the first flow). Newton with Brent fallback, multiple-root warning, clear errors for impossible flows.
- TWR (day-linked), Modified Dietz, rolling 1/3/5-year returns with distribution and share of time beating the benchmark, drawdowns with recovery, volatility, Sharpe, Sortino, beta, alpha, up/down capture.
- "Same cash flows into the benchmark": Long-Nickels direct equivalent and Kaplan-Schoar PME, per instrument or whole portfolio.
- Attribution: weight x return contributions with Carino linking, Brinson-Fachler allocation, selection and interaction against a target mix.
- Costs: expense-ratio drag in rupees, brokerage, regular vs direct plan cost.
- Importers for CAS-style mutual fund CSVs and broker tradebooks.
- `report` writes one HTML file with SVG charts (value vs invested, rolling returns, drawdown) and tables.

## Install

```bash
uv venv .venv
uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate
```

## Quickstart

```bash
python examples/generate.py            # regenerate the synthetic data (already checked in)
cd examples/portfolio
folioperf summary
folioperf xirr --by instrument --from 2021-04-01
folioperf twr --rolling 3y
folioperf vs-benchmark
folioperf attribution --period 2025
folioperf drawdowns
folioperf report -o report.html
```

Point at other data with `--data DIR` or `FOLIOPERF_DATA=DIR`.

## Input files

All files live in one directory.

| File | Columns |
|---|---|
| `transactions.csv` | `date, account, instrument, type, units, amount, price` |
| `prices.csv` | `date, instrument, price` (funds, stocks and benchmark indices) |
| `instruments.csv` (optional) | `instrument, name, asset_class, category, expense_ratio, benchmark, direct_equivalent` |
| `riskfree.csv` (optional) | `date, rate` (annual percent) |
| `targets.csv` (optional) | `asset_class, weight, benchmark` |

Transaction types: `buy`, `sip`, `sell`, `switch-in`, `switch-out`, `dividend`, `payout`, `fee`, `deposit`, `withdrawal`, `interest`, plus `split` (ratio in `units`) and `bonus` (free units in `units`). Either `units` or `amount` may be omitted, and it is derived from the price series. Dates may be `YYYY-MM-DD`, `DD-MM-YYYY`, `DD/MM/YYYY` or `DD-Mon-YYYY`. A `dividend` with units is reinvested; without units it is cash received. Instruments with no price history (EPF, PPF, FD) are balances at a price of 1: `deposit`, `interest`, `withdrawal`. Switch-out and switch-in rows on the same date and account are paired so cost basis carries over.

Importers:

```bash
folioperf import cas statement.csv -o transactions.csv            # folio, scheme, date, units, nav, amount[, description]
folioperf import tradebook trades.csv --account zerodha -o t.csv   # trade_date, symbol, trade_type, quantity, price[, brokerage]
```

## Usage with real output

All of the following were run in `examples/portfolio`, an 8-year synthetic portfolio (six fund SIPs, one switch, stock trades with a split and a bonus, EPF, PPF and FDs, seeded GBM prices).

Common options on analysis commands: `--data`, `--as-of`, `--from`, `--to`, `--account`, `--instrument`, `--asset-class`, `--rf`. `twr` and `vs-benchmark` also accept `--benchmark ID`; `xirr` and `vs-benchmark` accept `--by portfolio|account|instrument|asset-class|category`; `attribution` accepts `--period 2025|2025-Q2|2025-04|FY2024-25`.

```text
$ folioperf summary
Holdings as of 2026-03-31                                                                  
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┓
┃ Instrument ┃ Class         ┃      Units ┃       Cost ┃      Value ┃ Unrealised ┃   XIRR ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━┩
│ DEBT_DIR   │ Debt          │ 20,198.849 │  ₹6,72,000 │  ₹9,09,510 │  ₹2,37,510 │  7.44% │
│ EPF        │ Debt          │          - │ ₹10,84,860 │ ₹15,31,408 │  ₹4,46,548 │  9.12% │
│ FD_SBI2    │ Debt          │          - │  ₹3,00,000 │  ₹3,44,755 │    ₹44,755 │  6.51% │
│ FLEXI_DIR  │ Equity        │ 20,743.739 │ ₹20,26,442 │ ₹29,33,463 │  ₹9,07,022 │ 11.86% │
│ HDFCBANK   │ Equity        │    235.000 │  ₹4,46,963 │  ₹7,11,489 │  ₹2,64,526 │  9.65% │
│ INDEX_DIR  │ Equity        │ 16,671.610 │  ₹5,50,673 │  ₹7,47,835 │  ₹1,97,162 │  0.77% │
│ INTL_DIR   │ International │ 13,284.448 │  ₹3,45,000 │  ₹4,87,845 │  ₹1,42,845 │ 12.00% │
│ LARGE_DIR  │ Equity        │  6,346.491 │  ₹5,24,327 │  ₹6,59,475 │  ₹1,35,149 │ 11.03% │
│ MID_DIR    │ Equity        │  5,357.199 │  ₹8,15,100 │ ₹10,74,224 │  ₹2,59,124 │  7.95% │
│ PPF        │ Debt          │          - │ ₹12,00,000 │ ₹16,57,941 │  ₹4,57,941 │  7.17% │
│ RELIANCE   │ Equity        │    571.000 │  ₹2,91,539 │  ₹3,05,811 │    ₹14,273 │  4.10% │
│ SMALL_REG  │ Equity        │  6,684.158 │  ₹4,92,000 │  ₹7,24,511 │  ₹2,32,511 │ 11.19% │
│ TCS        │ Equity        │     77.000 │  ₹1,97,138 │  ₹8,84,663 │  ₹6,87,525 │ 31.73% │
└────────────┴───────────────┴────────────┴────────────┴────────────┴────────────┴────────┘

Portfolio                                         
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Metric                          ┃        Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Current value                   │ ₹1,29,72,931 │
│ Net invested (window flows)     │   ₹81,11,823 │
│ Realised gains (all time)       │    ₹6,33,115 │
│ Fees and commissions (all time) │         ₹868 │
│ XIRR                            │       10.61% │
│ TWR (cumulative)                │      162.69% │
│ TWR (annualised)                │       12.85% │
│ Modified Dietz                  │      109.78% │
└─────────────────────────────────┴──────────────┘

$ folioperf xirr --by asset-class
XIRR by asset-class, 2018-04-05 to 2026-03-31              
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓
┃ Asset-Class   ┃   Invested ┃      Value ┃   XIRR ┃ Note ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩
│ Debt          │ ₹37,56,860 │ ₹44,43,614 │  7.41% │      │
│ Equity        │ ₹59,15,934 │ ₹80,41,472 │ 12.80% │      │
│ International │  ₹3,45,000 │  ₹4,87,845 │ 12.00% │      │
└───────────────┴────────────┴────────────┴────────┴──────┘

$ folioperf xirr --by instrument --from 2021-04-01
XIRR by instrument, 2021-04-01 to 2026-03-31            
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓
┃ Instrument ┃   Invested ┃      Value ┃   XIRR ┃ Note ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩
│ DEBT_DIR   │  ₹7,04,381 │  ₹9,09,510 │  7.32% │      │
│ EPF        │ ₹11,43,653 │ ₹15,31,408 │  8.83% │      │
│ FD_SBI     │  ₹5,71,380 │         ₹0 │  4.89% │      │
│ FD_SBI2    │  ₹3,00,000 │  ₹3,44,755 │  6.51% │      │
│ FLEXI_DIR  │ ₹25,66,151 │ ₹29,33,463 │  3.65% │      │
│ HDFCBANK   │  ₹7,20,234 │  ₹7,11,489 │  0.08% │      │
│ INDEX_DIR  │  ₹7,32,871 │  ₹7,47,835 │  0.77% │      │
│ INTL_DIR   │  ₹3,53,407 │  ₹4,87,845 │ 11.20% │      │
│ LARGE_DIR  │ ₹11,29,587 │  ₹6,59,475 │  3.14% │      │
│ MID_DIR    │ ₹10,29,653 │ ₹10,74,224 │  1.20% │      │
│ PPF        │ ₹12,67,472 │ ₹16,57,941 │  7.16% │      │
│ RELIANCE   │  ₹4,09,261 │  ₹3,05,811 │ -1.99% │      │
│ SMALL_REG  │  ₹5,92,758 │  ₹7,24,511 │  5.78% │      │
│ TCS        │ ₹11,15,906 │  ₹8,84,663 │  6.69% │      │
└────────────┴────────────┴────────────┴────────┴──────┘

$ folioperf twr --rolling 1y,3y,5y
Time-weighted return                                    
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Metric                                     ┃   Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ TWR (day-linked), 2018-04-06 to 2026-03-31 │ 162.69% │
│ Annualised                                 │  12.85% │
│ Modified Dietz (cross-check)               │ 109.78% │
│ blended benchmark cumulative               │ 132.51% │
│ blended benchmark annualised               │  11.14% │
│ Volatility                                 │  11.54% │
│ Sharpe                                     │    0.52 │
│ Sortino                                    │    0.75 │
│ Beta                                       │    0.95 │
│ Alpha (annual)                             │   1.31% │
│ Upside capture                             │    0.96 │
│ Downside capture                           │    0.93 │
└────────────────────────────────────────────┴─────────┘

Rolling annualised returns (vs blended benchmark)                                          
┏━━━━━━━━┳━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Window ┃  Obs ┃     Min ┃    P25 ┃ Median ┃    P75 ┃    Max ┃ % negative ┃ % beating bm ┃
┡━━━━━━━━╇━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ 1Y     │ 2553 │ -10.40% │  2.82% │ 14.11% │ 24.23% │ 89.47% │      15.3% │        49.5% │
│ 3Y     │ 1822 │   2.56% │  9.06% │ 13.21% │ 20.27% │ 27.99% │       0.0% │        84.2% │
│ 5Y     │ 1092 │   4.72% │ 13.50% │ 14.66% │ 16.37% │ 20.70% │       0.0% │        97.3% │
└────────┴──────┴─────────┴────────┴────────┴────────┴────────┴────────────┴──────────────┘

$ folioperf vs-benchmark
Direct-equivalent (Long-Nickels PME)                                                       
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Instrument ┃        Value ┃ Benchmark value ┃   XIRR ┃ Benchmark XIRR ┃ Excess ┃ KS-PME ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ DEBT_DIR   │    ₹9,09,510 │       ₹9,23,044 │  7.44% │          7.80% │ -0.36% │   0.99 │
│ EPF        │   ₹15,31,408 │      ₹14,55,874 │  9.12% │          7.80% │  1.32% │   1.05 │
│ FD_SBI     │           ₹0 │         ₹79,377 │  5.48% │          7.58% │ -2.10% │   0.91 │
│ FD_SBI2    │    ₹3,44,755 │       ₹3,57,134 │  6.51% │          8.23% │ -1.72% │   0.97 │
│ FLEXI_DIR  │   ₹29,33,463 │      ₹25,76,965 │ 11.86% │          8.45% │  3.41% │   1.14 │
│ HDFCBANK   │    ₹7,11,489 │       ₹6,25,024 │  9.65% │          7.14% │  2.51% │   1.13 │
│ INDEX_DIR  │    ₹7,47,835 │       ₹7,62,120 │  0.77% │          1.50% │ -0.73% │   0.98 │
│ INTL_DIR   │    ₹4,87,845 │       ₹4,12,769 │ 12.00% │          6.19% │  5.80% │   1.18 │
│ LARGE_DIR  │    ₹6,59,475 │       ₹5,82,145 │ 11.03% │          8.93% │  2.10% │   1.07 │
│ MID_DIR    │   ₹10,74,224 │       ₹9,79,007 │  7.95% │          5.29% │  2.66% │   1.10 │
│ PPF        │   ₹16,57,941 │      ₹17,04,942 │  7.17% │          7.79% │ -0.62% │   0.97 │
│ RELIANCE   │    ₹3,05,811 │       ₹4,37,246 │  4.10% │         11.80% │ -7.70% │   0.75 │
│ SMALL_REG  │    ₹7,24,511 │       ₹9,42,555 │ 11.19% │         18.79% │ -7.60% │   0.77 │
│ TCS        │    ₹8,84,663 │       ₹1,20,528 │ 31.73% │         18.73% │ 13.00% │   2.21 │
│ Portfolio  │ ₹1,29,72,931 │    ₹1,19,58,730 │ 10.61% │          8.78% │  1.82% │   1.07 │
└────────────┴──────────────┴─────────────────┴────────┴────────────────┴────────┴────────┘

$ folioperf attribution --period 2025
Contribution to return by instrument, 2025-01-01 to 2025-12-31
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Instrument            ┃ Avg weight ┃ Return ┃ Contribution ┃
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━┩
│ FLEXI_DIR             │     22.81% │ 11.53% │        2.60% │
│ RELIANCE              │      2.31% │ 51.46% │        1.07% │
│ SMALL_REG             │      6.21% │ 16.95% │        1.04% │
│ HDFCBANK              │      6.45% │ 12.52% │        0.81% │
│ EPF                   │      9.56% │  8.17% │        0.78% │
│ LARGE_DIR             │      4.88% │ 16.07% │        0.78% │
│ PPF                   │     11.15% │  7.15% │        0.78% │
│ DEBT_DIR              │      6.02% │  7.38% │        0.45% │
│ INDEX_DIR             │      6.27% │  5.84% │        0.37% │
│ TCS                   │      8.81% │  4.06% │        0.35% │
│ INTL_DIR              │      3.52% │  9.60% │        0.33% │
│ MID_DIR               │      9.61% │  2.58% │        0.30% │
│ FD_SBI2               │      2.41% │  7.20% │        0.19% │
│ Total (Carino-linked) │    100.00% │        │        9.84% │
└───────────────────────┴────────────┴────────┴──────────────┘

Brinson-Fachler vs target allocation                                                                    
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓
┃ Asset class   ┃ Weight ┃ Target ┃ Return ┃ Benchmark ┃ Allocation ┃ Selection ┃ Interaction ┃  Total ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩
│ Debt          │ 29.13% │ 25.00% │  7.52% │     7.35% │     -0.12% │     0.09% │      -0.03% │ -0.07% │
│ Equity        │ 67.35% │ 65.00% │ 10.91% │     7.55% │     -0.04% │     2.19% │       0.12% │  2.26% │
│ International │  3.52% │ 10.00% │  9.60% │    -5.43% │      0.76% │     1.56% │      -1.01% │  1.30% │
│ Active return │        │        │  9.84% │     6.34% │            │           │             │  3.50% │
└───────────────┴────────┴────────┴────────┴───────────┴────────────┴───────────┴─────────────┴────────┘

$ folioperf drawdowns --top 3
Drawdowns (max -23.95%)                                                            
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Peak       ┃     Trough ┃  Recovered ┃   Depth ┃ Peak to trough ┃ Recovery time ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ 2020-02-23 │ 2020-03-24 │ 2020-06-23 │ -23.95% │           30 d │          91 d │
│ 2025-11-05 │ 2026-03-24 │    not yet │ -18.74% │          139 d │             - │
│ 2021-11-30 │ 2022-06-06 │ 2024-03-25 │ -18.73% │          188 d │         658 d │
└────────────┴────────────┴────────────┴─────────┴────────────────┴───────────────┘

$ folioperf costs
Costs                                            
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Instrument ┃ Expense ratio drag ┃ Commissions ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ DEBT_DIR   │            ₹10,000 │          ₹0 │
│ FLEXI_DIR  │            ₹75,031 │          ₹0 │
│ HDFCBANK   │                 ₹0 │        ₹223 │
│ INDEX_DIR  │             ₹4,331 │          ₹0 │
│ INTL_DIR   │             ₹7,521 │          ₹0 │
│ LARGE_DIR  │            ₹34,849 │          ₹0 │
│ MID_DIR    │            ₹33,602 │          ₹0 │
│ RELIANCE   │                 ₹0 │        ₹211 │
│ SMALL_REG  │            ₹50,270 │          ₹0 │
│ TCS        │                 ₹0 │        ₹433 │
│ Total      │          ₹2,15,603 │        ₹868 │
└────────────┴────────────────────┴─────────────┘

Regular vs direct plan (same cash flows)                                                               
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Regular   ┃    Direct ┃ Regular value ┃ Direct value ┃ Cost of regular ┃ XIRR regular ┃ XIRR direct ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ SMALL_REG │ SMALL_DIR │     ₹7,24,511 │    ₹7,64,270 │         ₹39,760 │       11.19% │      12.73% │
└───────────┴───────────┴───────────────┴──────────────┴─────────────────┴──────────────┴─────────────┘
```

`folioperf report -o report.html` produces the HTML report: headline numbers, value vs invested, rolling returns, drawdown chart, risk, XIRR and benchmark tables, attribution and costs.

## Configuration

| Setting | Effect |
|---|---|
| `--data DIR` / `FOLIOPERF_DATA` | Data directory (default: current directory). |
| `--rf` | Risk-free percent per year when `riskfree.csv` is absent (default 6). |
| `instruments.csv: benchmark` | Series each instrument's cash flows are replayed into. |
| `instruments.csv: direct_equivalent` | NAV series of the direct plan for the regular-vs-direct comparison. |

## Architecture

| Module | Role |
|---|---|
| `models.py` | Dataclasses, `Series` with forward fill. |
| `loaders.py` | CSV parsing, validation messages with line numbers, CAS and tradebook importers. |
| `ledger.py` | Holdings reconstruction, cost basis, daily value and flow arrays. |
| `xirr.py` | Newton plus Brent XIRR, root counting. |
| `returns.py` | TWR, Dietz, rolling, drawdowns, risk statistics. |
| `benchmark.py` | Long-Nickels replay and KS-PME. |
| `attribution.py` | Carino contributions and Brinson-Fachler. |
| `costs.py` | Expense drag, commissions, direct vs regular. |
| `portfolio.py` | Loads a data directory, scopes, windows, flow assembly. |
| `analysis.py` | Glue producing the figures commands show. |
| `charts.py`, `report.py` | SVG charts and the HTML report. |
| `cli.py` | Click commands. |

Design notes and formulas with worked examples: [docs/methodology.md](docs/methodology.md).

## Tests

```bash
pytest
```

72 tests cover XIRR against Excel's documented example and edge cases, TWR against hand calculations, Modified Dietz, rolling windows, drawdowns, PME identities, Carino sums, Brinson identity, ledger reconstruction (switches, splits, bonus, dividends), importers, every CLI command on the example data, and the HTML report.

## Limitations

- Fees are outside the portfolio: XIRR and TWR are quoted before brokerage and stamp duty.
- Cost basis uses average cost, no tax-lot or capital gains tax logic.
- Prices are split-unadjusted raw prices; record `split` and `bonus` rows to match.
- Risk statistics use weekday returns with forward-filled holidays.
- Brinson-Fachler assumes the target mix is rebalanced monthly; multi-currency is not supported.
- Sells are not allowed to exceed holdings; the loader reports the offending line instead.
- Importers cover the common column layouts only; check the converted CSV before relying on it.
