Tax-Aware Direct Indexing
Track a benchmark. Harvest losses lot by lot. Respect wash sales.
The dark track is the strategy's after-tax NAV. The grey track is the benchmark NAV. Both start at $1M on the start date, rebalanced daily under the CLARABEL solver.
Numbers are plausible placeholders, not a real backtest. Replace by running taxview-runner over the same window. Daily rebalance, CLARABEL solver. Marginal-rate assumptions: short-term 37%, long-term 20%, NIIT 3.8%.
An index fund holds the index opaquely. Direct indexing holds the same names transparently — so every name that drops can be sold for a tax loss against your other gains, then replaced inside the tracking-error budget.
| Σ wᵢ = 1 | Fully invested — weights sum to 1 |
| 0 ≤ wᵢ ≤ c_max | Long-only with per-name cap |
| TE(w) ≤ TE_max | Tracking error stays inside the budget |
Minimize the weighted sum of two things: (i) the portfolio's factor distance from the benchmark, measured under the risk model Σ, and (ii) the tax cost on realized gains. The two weights λ_te and λ_tax are the conductor's batons — push λ_tax up and the optimizer harvests harder at the cost of wider tracking error; push λ_te up and the portfolio hugs the index more tightly.
We solve this as a portfolio-optimization problem each day, using CVXPY with the CLARABEL conic solver. The solver searches the feasible set defined by the constraints and returns the weight vector that minimises (or maximises) the objective — typically in tens of milliseconds for a 500-name universe.
- Holdings + lots
Per-account lot-level positions with acquisition date, cost basis, and quantity.
- Benchmark weights
Target weights from the chosen index (S&P 100, Russell 1000, MSCI World, etc.).
- Prices + risk model
Daily prices and the factor risk model that drives the tracking-error metric.
- Account constraints
TE budget, single-name cap, exclusions, and any other per-account knobs.
- Trade list
Lot-level buys and sells for the day, each identified by the lot it touches.
- New weight vector
Target weights w* after the solve — what the portfolio should hold tomorrow.
- Realized P/L
Per-lot realized gain/loss for the day, split into short-term and long-term buckets.
Factor tilt
A factor tilt lets the optimizer hold more of the names that score well on a chosen factor — quality, value, momentum, or low-volatility — and less of the names that score poorly. The portfolio still tracks the benchmark, but with a measurable lean toward the chosen factor.
B_f is the column of factor loadings for the chosen factor from the risk model. The constraint forces the portfolio's active exposure to that factor to be at least t_f standard deviations above the benchmark. The optimizer redistributes weight within the tracking-error budget to satisfy it — buying high-scoring names, underweighting low-scoring ones.
You consume part of your tracking-error budget on the tilt. Less budget remains for tax-loss harvesting, so factor tilts typically reduce expected harvest activity slightly. The factor's own active return is the offset.
The console's risk panel shows the current active factor exposure next to its target. Trade tickets annotate names whose factor score drove the buy or sell.
| Long/Short Tax-Aware | Market-Neutral Pair Sleeve | Tax-Aware Direct Indexing | |
|---|---|---|---|
| Net exposure | 100% | 0% | 100% |
| Factor exposure | Tracked | Pinned to zero | Tracked |
| Source of return | Index + tax + active | Cross-sectional alpha | Index + tax alpha |
| Role | Standalone book | Companion sleeve | Standalone book |