# rebalancer

Tax-aware multi-account portfolio rebalancing from the command line. Given several accounts (taxable brokerage, EPF/NPS/401k/IRA, mutual funds), target allocations with drift bands, tax lots and prices, rebalancer solves a linear program (HiGHS) that picks the cheapest set of trades to get back to target: new cash first, lot-level tax cost, transaction costs, account constraints and the wash-sale rule are all part of the optimisation.

## Features

- Allocation, drift and 5/25-style bands (absolute and relative) per asset class, with optional glide path by age.
- Multi-class securities (a 65/35 balanced fund counts for both equity and debt).
- Four strategies, compared side by side: cash-flow-only, threshold, full rebalance, tax-loss-harvest aware.
- Lot-level tax model: STCG/LTCG with holding periods, India preset (12 months, 20% / 12.5%, ₹1.25L LTCG exemption, slab-taxed debt funds) and US preset (1 year, your rates, capped loss deduction).
- Lot selection per account: FIFO (enforced, as Indian demat requires), HIFO, or tax-minimising specific lots.
- Account constraints: no-sell flags (EPF, PPF), fund universes, fractional vs whole-unit securities, per-account cash flows (contributions or withdrawals).
- Costs: fixed fee per trade plus bps, minimum trade size, whole-unit rounding with cash re-checked.
- Wash-sale rule (US): no security is bought while another lot of it is sold at a loss, and loss lots bought within the window cannot be harvested.
- Asset location suggestion with an after-tax terminal wealth estimate.
- Drift history from saved snapshots.
- Output as rich tables, a broker-friendly CSV trade list, or JSON.

## Install

Python 3.12+.

```bash
cd rebalancer
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate   # or call .venv/bin/rebalancer directly
```

## Quickstart

```bash
rebalancer -p examples/india/portfolio.yaml status
rebalancer -p examples/india/portfolio.yaml plan
rebalancer -p examples/us/portfolio.yaml compare
rebalancer -p examples/us/portfolio.yaml plan -s tlh --csv trades.csv
rebalancer -p examples/us/portfolio.yaml location
rebalancer -p examples/india/portfolio.yaml drift-history --dir examples/india/snapshots
```

`-p/--portfolio` defaults to `portfolio.yaml` in the current directory (or `$REBALANCER_PORTFOLIO`). Prices come from the CSV named in the portfolio file unless `--prices` is given.

## Usage

Global options: `-p/--portfolio FILE`, `--prices FILE`, `--age N` (overrides the glide-path age).

| Command | What it does |
| --- | --- |
| `status [--json]` | Current allocation, drift, band breaches, account summary. |
| `plan [-s STRATEGY] [--cash N] [--cash-account ID] [--csv FILE\|-] [--json]` | Trade list for one strategy (default `threshold`). `--cash` adds (or with a negative value withdraws) cash in the first taxable account, or the one given by `--cash-account`. |
| `compare [--cash N] [--json]` | Runs all four strategies and compares trades, tax, costs and residual drift. |
| `location [--json]` | Suggests which classes belong in which account and estimates the after-tax wealth gain. |
| `snapshot [--dir D] [--date YYYY-MM-DD]` | Saves the current allocation as `D/<date>.json`. |
| `drift-history [--dir D] [--json]` | Drift per class across saved snapshots. |

Strategies: `cashflow` (deploy cash, never sell), `threshold` (sell only from classes outside their bands, buy to bring breached classes back; classes inside their band are left alone), `full` (every class back to target), `tlh` (bands as slack, but sells are allowed everywhere so loss lots can be harvested into other securities of the same class).

### Status

```text
Indian investor (EPF, PPF, NPS, demat, mutual funds)  as of 2026-10-16  total ₹6,386,920
Allocation vs target                                                  
┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Class  ┃      Value ┃ Actual % ┃ Target % ┃ Drift ┃  Band ┃ Action ┃
┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ equity │ ₹3,985,260 │    62.40 │    55.00 │ +7.40 │ ±5.00 │ sell   │
│ debt   │ ₹2,056,160 │    32.19 │    35.00 │ -2.81 │ ±5.00 │ ok     │
│ gold   │   ₹295,500 │     4.63 │     8.00 │ -3.37 │ ±2.00 │ buy    │
│ cash   │    ₹50,000 │     0.78 │     2.00 │ -1.22 │ ±1.00 │ buy    │
└────────┴────────────┴──────────┴──────────┴───────┴───────┴────────┘
Accounts                                                          
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━┓
┃ Account             ┃ Type         ┃      Value ┃ Cash ┃ Share ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━┩
│ EPF (no sells)      │ tax_free     │   ₹860,000 │   ₹0 │ 13.5% │
│ PPF (no sells)      │ tax_free     │   ₹410,000 │   ₹0 │  6.4% │
│ NPS Tier I          │ tax_deferred │   ₹404,100 │   ₹0 │  6.3% │
│ Demat (Zerodha)     │ taxable      │ ₹2,470,820 │   ₹0 │ 38.7% │
│ Mutual funds (Coin) │ taxable      │ ₹2,242,000 │   ₹0 │ 35.1% │
└─────────────────────┴──────────────┴────────────┴──────┴───────┘
```

### Plan (India: FIFO lots, exemption, no-sell EPF)

Cash flows come from the `cash_flow` fields in the portfolio file. The demat account is FIFO, so the NIFTYBEES sale consumes the oldest lot first; the ₹72,091 long-term gain stays inside the ₹1.25L exemption, so no tax is due. EPF cannot sell and can only buy EPF, so its contribution is deployed there.

```text
Plan: threshold (threshold: act only on classes outside their bands)
Cash flows: epf ₹30,000, nps ₹20,000, demat ₹100,000
Trades                                                                                 
┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
┃ Account ┃ Action ┃ Security  ┃         Qty ┃  Price ┃   Amount ┃    Gain ┃ Tax term ┃
┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
│ demat   │ SELL   │ NIFTYBEES │         653 │ 285.40 │ ₹186,366 │ ₹72,091 │ LT       │
│ demat   │ BUY    │ GOLDBEES  │       2,298 │  98.50 │ ₹226,353 │       - │ -        │
│ epf     │ BUY    │ EPF       │ 29,944.0671 │   1.00 │  ₹29,944 │       - │ -        │
│ nps     │ SELL   │ NPS_E     │       4,200 │  48.50 │ ₹203,700 │       - │ -        │
│ nps     │ BUY    │ NPS_C     │  4,898.4935 │  41.20 │ ₹201,818 │       - │ -        │
└─────────┴────────┴───────────┴─────────────┴────────┴──────────┴─────────┴──────────┘
Before                                                                
┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Class  ┃      Value ┃ Actual % ┃ Target % ┃ Drift ┃  Band ┃ Action ┃
┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ equity │ ₹3,985,260 │    60.97 │    55.00 │ +5.97 │ ±5.00 │ sell   │
│ debt   │ ₹2,056,160 │    31.45 │    35.00 │ -3.55 │ ±5.00 │ ok     │
│ gold   │   ₹295,500 │     4.52 │     8.00 │ -3.48 │ ±2.00 │ buy    │
│ cash   │   ₹200,000 │     3.06 │     2.00 │ +1.06 │ ±1.00 │ sell   │
└────────┴────────────┴──────────┴──────────┴───────┴───────┴────────┘
After                                                                 
┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Class  ┃      Value ┃ Actual % ┃ Target % ┃ Drift ┃  Band ┃ Action ┃
┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ equity │ ₹3,595,194 │    55.01 │    55.00 │ +0.01 │ ±5.00 │ ok     │
│ debt   │ ₹2,287,922 │    35.01 │    35.00 │ +0.01 │ ±5.00 │ ok     │
│ gold   │   ₹521,853 │     7.98 │     8.00 │ -0.02 │ ±2.00 │ ok     │
│ cash   │   ₹130,833 │     2.00 │     2.00 │ +0.00 │ ±1.00 │ ok     │
└────────┴────────────┴──────────┴──────────┴───────┴───────┴────────┘
Summary                                             
┌───────────────────────────────────────┬──────────┐
│ Bought                                │ ₹458,115 │
│ Sold                                  │ ₹390,066 │
│ Transaction costs                     │   ₹1,118 │
│ Estimated tax                         │       ₹0 │
│ Residual drift (sum |drift|, pct pts) │     0.03 │
└───────────────────────────────────────┴──────────┘
```

### Compare strategies (US)

```text
Strategy comparison                                                                                 
┏━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃           ┃        ┃          ┃         ┃       ┃       ┃             ┃             ┃        Max ┃
┃           ┃        ┃          ┃         ┃       ┃       ┃       Drift ┃             ┃    |drift| ┃
┃ Strategy  ┃ Trades ┃   Bought ┃    Sold ┃   Tax ┃ Costs ┃      before ┃ Drift after ┃      after ┃
┡━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ cashflow  │      3 │  $18,803 │      $0 │    $0 │    $4 │       23.18 │       16.63 │       8.28 │
│ threshold │      6 │  $66,302 │ $47,571 │ -$612 │   $23 │       23.18 │        0.09 │       0.04 │
│ full      │      7 │ $103,145 │ $84,517 │ -$960 │   $38 │       23.18 │        0.12 │       0.06 │
│ tlh       │      7 │ $103,145 │ $84,517 │ -$960 │   $38 │       23.18 │        0.12 │       0.06 │
└───────────┴────────┴──────────┴─────────┴───────┴───────┴─────────────┴─────────────┴────────────┘
Allocation after each strategy (actual %)                               
┏━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ Class       ┃ Target ┃ Before ┃ cashflow ┃ threshold ┃  full ┃   tlh ┃
┡━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
│ us_equity   │  46.75 │  39.58 │    41.04 │     46.72 │ 46.71 │ 46.71 │
│ intl_equity │  19.50 │  27.78 │    27.78 │     19.50 │ 19.50 │ 19.50 │
│ bonds       │  25.40 │  23.59 │    25.40 │     25.40 │ 25.40 │ 25.40 │
│ reit        │   8.35 │   5.75 │     5.75 │      8.34 │  8.34 │  8.34 │
│ cash        │   0.00 │   3.31 │     0.03 │      0.04 │  0.06 │  0.06 │
└─────────────┴────────┴────────┴──────────┴───────────┴───────┴───────┘
```

The negative tax is a loss-harvesting credit: the underwater VXUS lot is sold and the loss deduction (capped at $3,000 times the ordinary rate) is worth $612 to $960.

### Tax-loss-harvest aware plan (US)

The bond lot in the taxable account is sold at a loss while bonds are bought in the 401(k) instead, so the bond allocation is unchanged and no security is both sold at a loss and bought.

```text
Plan: tlh (tax-loss-harvest aware: bands plus loss harvesting swaps)
Cash flows: brokerage $15,000, k401 $1,500
Trades                                                                                
┏━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
┃ Account   ┃ Action ┃ Security ┃        Qty ┃  Price ┃  Amount ┃    Gain ┃ Tax term ┃
┡━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
│ brokerage │ SELL   │ BND      │        400 │  72.10 │ $28,840 │ -$4,360 │ LT       │
│ brokerage │ SELL   │ VXUS     │        250 │  71.85 │ $17,962 │ -$1,913 │ LT       │
│ brokerage │ BUY    │ VNQ      │        158 │  94.30 │ $14,899 │       - │ -        │
│ brokerage │ BUY    │ VTI      │        157 │ 312.40 │ $49,047 │       - │ -        │
│ k401      │ SELL   │ VFIAX    │    13.2409 │ 612.20 │  $8,106 │       - │ -        │
│ k401      │ SELL   │ VTIAX    │   711.7448 │  41.60 │ $29,609 │       - │ -        │
│ k401      │ BUY    │ VBTLX    │ 3,979.6167 │   9.85 │ $39,199 │       - │ -        │
└───────────┴────────┴──────────┴────────────┴────────┴─────────┴─────────┴──────────┘
Before                                                                   
┏━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Class       ┃    Value ┃ Actual % ┃ Target % ┃ Drift ┃  Band ┃ Action ┃
┡━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ us_equity   │ $227,346 │    39.58 │    46.75 │ -7.17 │ ±5.00 │ buy    │
│ intl_equity │ $159,590 │    27.78 │    19.50 │ +8.28 │ ±4.88 │ sell   │
│ bonds       │ $135,515 │    23.59 │    25.40 │ -1.81 │ ±5.00 │ ok     │
│ reit        │  $33,005 │     5.75 │     8.35 │ -2.60 │ ±2.09 │ buy    │
│ cash        │  $19,000 │     3.31 │     0.00 │ +3.31 │ ±0.50 │ sell   │
└─────────────┴──────────┴──────────┴──────────┴───────┴───────┴────────┘
After                                                                    
┏━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Class       ┃    Value ┃ Actual % ┃ Target % ┃ Drift ┃  Band ┃ Action ┃
┡━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ us_equity   │ $268,287 │    46.71 │    46.75 │ -0.04 │ ±5.00 │ ok     │
│ intl_equity │ $112,019 │    19.50 │    19.50 │ +0.00 │ ±4.88 │ ok     │
│ bonds       │ $145,874 │    25.40 │    25.40 │ -0.00 │ ±5.00 │ ok     │
│ reit        │  $47,904 │     8.34 │     8.35 │ -0.01 │ ±2.09 │ ok     │
│ cash        │     $334 │     0.06 │     0.00 │ +0.06 │ ±0.50 │ ok     │
└─────────────┴──────────┴──────────┴──────────┴───────┴───────┴────────┘
Summary                                             
┌───────────────────────────────────────┬──────────┐
│ Bought                                │ $103,145 │
│ Sold                                  │  $84,517 │
│ Transaction costs                     │      $38 │
│ Estimated tax                         │    -$960 │
│ Residual drift (sum |drift|, pct pts) │     0.12 │
└───────────────────────────────────────┴──────────┘
```

### CSV trade list

One row per lot for taxable sells, so a broker ticket can name the exact lots.

```text
account,action,symbol,quantity,price,amount,fee,lot_date,lot_cost,est_gain,term
brokerage,SELL,VXUS,250,71.8500,17962.50,3.59,2024-01-10,79.5000,-1912.50,LT
brokerage,BUY,VNQ,158,94.3000,14899.40,2.98,,,,
brokerage,BUY,VTI,65,312.4000,20306.00,4.06,,,,
k401,SELL,VTIAX,711.745,41.6000,29608.58,5.92,,,,
k401,BUY,VBTLX,1053.2,9.8500,10374.00,2.07,,,,
k401,BUY,VFIAX,33.8491,612.2000,20722.42,4.14,,,,
```

### Asset location

```text
Current placement                                                              
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┓
┃ Account   ┃ us_equity ┃ intl_equity ┃   bonds ┃    reit ┃   cash ┃    Total ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━┩
│ brokerage │   $89,034 │     $32,332 │ $46,865 │ $14,145 │ $2,500 │ $184,876 │
│ k401      │  $110,196 │    $116,480 │ $88,650 │       - │      - │ $315,326 │
│ roth      │   $28,116 │     $10,778 │       - │ $18,860 │      - │  $57,754 │
└───────────┴───────────┴─────────────┴─────────┴─────────┴────────┴──────────┘
Suggested placement                                                             
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┓
┃ Account   ┃ us_equity ┃ intl_equity ┃    bonds ┃    reit ┃   cash ┃    Total ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━┩
│ brokerage │  $184,876 │           - │        - │       - │      - │ $184,876 │
│ k401      │   $17,721 │    $159,590 │ $135,515 │       - │ $2,500 │ $315,326 │
│ roth      │   $24,748 │           - │        - │ $33,005 │      - │  $57,754 │
└───────────┴───────────┴─────────────┴──────────┴─────────┴────────┴──────────┘
After-tax wealth in 25 years: current $2,825,885, suggested $2,888,019 ($62,134, +2.20%)
```

### Drift history

```text
Drift history (percentage points vs target)                                                    
┏━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ Date       ┃     Total ┃ equity ┃  debt ┃  gold ┃  cash ┃ Worst        ┃ Breached           ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ 2026-01-15 │ 5,717,381 │  +3.55 │ +0.41 │ -2.83 │ -1.13 │ equity +3.55 │ gold, cash         │
│ 2026-04-15 │ 5,968,458 │  +5.09 │ -0.88 │ -3.05 │ -1.16 │ equity +5.09 │ equity, gold, cash │
│ 2026-07-15 │ 6,219,535 │  +6.51 │ -2.07 │ -3.25 │ -1.20 │ equity +6.51 │ equity, gold, cash │
│ 2026-10-16 │ 6,386,920 │  +7.40 │ -2.81 │ -3.37 │ -1.22 │ equity +7.40 │ equity, gold, cash │
└────────────┴───────────┴────────┴───────┴───────┴───────┴──────────────┴────────────────────┘
```

## Input files

A portfolio is one YAML file plus a prices CSV (`symbol,price`). See `examples/*/portfolio.yaml`.

```yaml
as_of: 2026-10-16
prices: prices.csv                  # relative to the YAML file
targets:
  age: 41                           # used with glide_path
  bands: {absolute: 5, relative: 25}   # percentage points / percent of target; the tighter one triggers
  classes:                          # fixed targets (percent) ...
    equity: 60
    cash: {target: 5, absolute: 2, relative: 50}
  glide_path:                       # ... or interpolated by age (class entries then only supply bands)
    - {age: 30, us_equity: 55, bonds: 10}
tax:
  preset: india | us | none
  slab_rate: 30                     # india: rate on non-equity gains
  stcg_rate: 32                     # us
  ltcg_rate: 15                     # us
  loss_deduction_cap: 3000          # us: net losses usable against ordinary income
  wash_sale: true                   # us default true
  categories:                       # optional per-category overrides
    gold: {holding_days: 365, stcg: 30, ltcg: 12.5, exemption: 0}
costs: {fixed: 20, bps: 12, min_trade: 5000}
location:
  tax_efficiency: {equity: 0.85}    # 0..1, higher = more tax efficient
  expected_return: {equity: 12}     # percent
  withdrawal_rate: 12               # tax on tax_deferred balances at withdrawal
  horizon: 20
securities:
  BAF: {name: Balanced fund, classes: {equity: 0.65, debt: 0.35}, tax_category: equity, fractional: true}
accounts:
  - id: demat
    type: taxable                   # taxable | tax_deferred | tax_free
    lot_method: fifo                # fifo | hifo | min_tax
    no_sell: false
    funds: [NIFTYBEES]              # buy universe (omit for everything)
    cash: 0
    cash_flow: 100000               # contribution (+) or withdrawal (-) to apply in this run
    positions:
      - symbol: NIFTYBEES
        lots:                       # taxable accounts need lots; cost is per unit
          - {date: 2021-06-15, qty: 2500, cost: 175.00}
      - {symbol: NPS_E, qty: 4200}  # non-taxable accounts need only qty
```

Rates are in percent, bps in basis points, `cost` is per unit. Idle cash is treated as a class named `cash` (target 0% unless you list it) so uninvested money is pulled into the portfolio.

## Architecture

```text
rebalancer/
  models.py       dataclasses: lots, accounts, targets, tax and cost config
  loader.py       YAML/CSV loading, tax presets, glide path, validation
  drift.py        class values, drift and band checks
  tax.py          holding periods and exact tax (netting, exemption, loss credit)
  lp.py           small sparse LP/MILP builder over scipy.optimize.milp (HiGHS)
  optimizer.py    model construction, min-trade/fixed-fee heuristics, rounding to units
  strategies.py   the four strategies and cash-flow resolution
  location.py     asset location (transportation LP) and after-tax wealth estimate
  snapshots.py    saved snapshots and drift history
  report.py       rich tables, CSV, JSON
  cli.py          click commands
docs/model.md        LP formulation, equations, worked examples
docs/assumptions.md  modelling assumptions and what is not modelled
examples/            India and US portfolios with prices and snapshots
```

## Tests

```bash
.venv/bin/pytest -q
```

221 tests: drift and bands, glide path, tax per lot and netting, hand-checkable strategy results, FIFO/HIFO/min-tax lot choice, cash-flow-only never sells, wash-sale, min trade and fixed-fee pruning, location, CLI end-to-end on both examples, and property tests over 40 random portfolios times 4 strategies (cash never negative, no-sell and fund universe respected, whole units, FIFO/HIFO order, min trade, wash-sale, tax equals recomputed tax).

## Limitations

- The model is flat-rate and single-year: no carried-forward losses, no state tax, no AMT, no surcharge or cess. See `docs/assumptions.md`.
- The LP does not net a long-term loss against a short-term gain at the short-term rate; the reported tax does (the two can differ slightly).
- Tax is assumed to be paid from outside the portfolio; it is reported and optimised but not deducted from cash.
- Wash-sale handling covers the exact same security in any account, not "substantially identical" funds, and ignores dividend reinvestment.
- Minimum trade size and fixed fees are handled with heuristics (re-solve with banned trades), so the result is very good but not guaranteed globally optimal.
- Past price history is not modelled; `drift-history` uses the snapshots you save.
- Not tax or investment advice.
