fast-minimum-variance: Solving Minimum Variance Portfolios Fast
fast-minimum-variance solves the global (equality-constrained) minimum variance
portfolio. The key observation is that the KKT stationarity condition
The sample covariance
import numpy as np
from fast_minimum_variance import Problem
# 500 daily returns, 20 assets
X = np.random.default_rng(42).standard_normal((500, 20))
w, outer, inner = Problem(X).solve_cg() # conjugate gradients on a dense Sigma
assert abs(w.sum() - 1.0) < 1e-8 # budget holds exactly; weights may be negativeLedoit-Wolf shrinkage plays a dual role: statistically it reduces estimation error; numerically
it compresses the eigenvalue spectrum and directly cuts CG iteration counts. Use
alpha = N / (N + T) as a simple analytical estimate of the optimal shrinkage intensity:
T, N = X.shape
w, outer, inner = Problem(X, alpha=N / (N + T)).solve_cg()On S&P 500 equity data (495 assets, 1192 days), shrinkage cuts CG iterations from 685 to 205 — the entire solve runs in under 10 ms (see Benchmarks).
Problem.solve_cg() runs conjugate gradients on the dense SPD system and
returns (w, outer_steps, inner_iters) where outer_steps is always 1 (there is no outer loop; the field is retained for API
compatibility).
The solve forms the dense system matrix
as a NumPy array and hands it to scipy.sparse.linalg.cg, which solves
Because weights are sign-unconstrained, the KKT system is linear and a single CG
solve suffices — no active-set iteration. If you need a long-only (
The same solver handles a range of portfolio construction problems by choosing
| Problem | alpha |
rho |
mu |
|---|---|---|---|
| Minimum variance | — | ||
| Mean-variance (Markowitz) | any | expected returns | |
| Minimum tracking error to benchmark |
any | X.T @ (X @ b) |
|
| LW-regularised minimum variance | — |
# Mean-variance
mu = np.random.default_rng(0).standard_normal(N) # expected returns, shape (N,)
w, *_ = Problem(X, rho=1.0, mu=mu).solve_cg()
# Minimum tracking error to benchmark b
b = np.ones(N) / N # equal-weight benchmark
mu_te = X.T @ (X @ b)
w, *_ = Problem(X, rho=2.0, mu=mu_te).solve_cg()When rho != 0, two SPD solves are performed:
To replace the default budget constraint (B, c):
B = np.zeros((2, N)); B[0, :N // 2] = 1.0; B[1, N // 2:] = 1.0 # each half holds...
c = np.array([0.5, 0.5]) # ...half of the budget
w, *_ = Problem(X, B=B, c=c).solve_cg()B must have full row rank. Weights remain sign-unconstrained; the multiplier for the
p constraints is recovered from a small
All timings on Apple M4 Pro, Python 3.12, NumPy 2.4, SciPy 1.17.
| Universe |
solve_cg time (s) |
||
|---|---|---|---|
| Synthetic i.i.d. Gaussian | 1000 | 2000 | 0.019 |
| S&P 500 (Jul 2021–Apr 2026) | 495 | 1192 | 0.0091 |
Both with Ledoit-Wolf shrinkage ($\alpha = 0.333$ synthetic / $0.293$ S&P), 56 and 205 CG iterations respectively.
pip install fast-minimum-varianceFor development:
git clone https://github.com/Jebel-Quant/fast_minimum_variance
cd fast_minimum_variance
make install- Python 3.11+
- numpy
- scipy
If you use this library in academic work or research, please cite:
@software{fast_minimum_variance,
author = {Schmelzer, Thomas},
title = {fast-minimum-variance: Solving Minimum Variance Portfolios Fast},
url = {https://github.com/Jebel-Quant/fast_minimum_variance},
year = {2026},
license = {MIT}
}MIT License — see LICENSE for details.