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+.
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
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
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.
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)
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.
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.
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
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
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.
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
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
.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-historyuses the snapshots you save. - Not tax or investment advice.