fast-minimum-variance 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_minimum_variance/__init__.py +1 -0
- fast_minimum_variance/cvx.py +37 -0
- fast_minimum_variance/kkt.py +81 -0
- fast_minimum_variance/krylov.py +183 -0
- fast_minimum_variance/random.py +26 -0
- fast_minimum_variance-0.2.0.dist-info/METADATA +117 -0
- fast_minimum_variance-0.2.0.dist-info/RECORD +9 -0
- fast_minimum_variance-0.2.0.dist-info/WHEEL +4 -0
- fast_minimum_variance-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""fast_minimum_variance."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Convex optimization solver for the minimum variance portfolio."""
|
|
2
|
+
|
|
3
|
+
import cvxpy as cp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def minvar_cvxpy(R): # noqa: N803
|
|
7
|
+
"""Solve the minimum variance portfolio via CVXPY.
|
|
8
|
+
|
|
9
|
+
Solves the long-only minimum variance problem::
|
|
10
|
+
|
|
11
|
+
min ||R w||_2^2
|
|
12
|
+
s.t. sum(w) = 1, w >= 0
|
|
13
|
+
|
|
14
|
+
using CVXPY with its default solver.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
R: Return matrix of shape (T, N).
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Weight vector of shape (N,) summing to 1 with all non-negative entries.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> import numpy as np
|
|
24
|
+
>>> from fast_minimum_variance.random import make_returns
|
|
25
|
+
>>> R = make_returns(100, 5, seed=0)
|
|
26
|
+
>>> w = minvar_cvxpy(R)
|
|
27
|
+
>>> w.shape
|
|
28
|
+
(5,)
|
|
29
|
+
>>> float(round(w.sum(), 6))
|
|
30
|
+
1.0
|
|
31
|
+
>>> bool((w >= -1e-6).all())
|
|
32
|
+
True
|
|
33
|
+
"""
|
|
34
|
+
n = R.shape[1]
|
|
35
|
+
w = cp.Variable(n)
|
|
36
|
+
cp.Problem(cp.Minimize(cp.sum_squares(R @ w)), [cp.sum(w) == 1, w >= 0]).solve()
|
|
37
|
+
return w.value
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""KKT system construction for the minimum variance portfolio."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_kkt(R): # noqa: N803
|
|
7
|
+
"""Build the KKT system matrix and RHS for the minimum variance problem.
|
|
8
|
+
|
|
9
|
+
Constructs the (N+1) x (N+1) indefinite KKT system for::
|
|
10
|
+
|
|
11
|
+
min w^T (R^T R) w
|
|
12
|
+
s.t. sum(w) = 1
|
|
13
|
+
|
|
14
|
+
The system has the form::
|
|
15
|
+
|
|
16
|
+
[ 2 R^T R 1 ] [ w ] [ 0 ]
|
|
17
|
+
[ 1^T 0 ] [ λ ] = [ 1 ]
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
R: Return matrix of shape (T, N).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple (A, b) where A is the (N+1) x (N+1) KKT matrix and b is the
|
|
24
|
+
(N+1,) right-hand side vector.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> import numpy as np
|
|
28
|
+
>>> R = np.eye(3)
|
|
29
|
+
>>> A, b = build_kkt(R)
|
|
30
|
+
>>> A.shape
|
|
31
|
+
(4, 4)
|
|
32
|
+
>>> b
|
|
33
|
+
array([0., 0., 0., 1.])
|
|
34
|
+
"""
|
|
35
|
+
n_a = R.shape[1]
|
|
36
|
+
A = np.zeros((n_a + 1, n_a + 1)) # noqa: N806
|
|
37
|
+
A[:n_a, :n_a] = 2 * R.T @ R
|
|
38
|
+
A[:n_a, n_a] = 1
|
|
39
|
+
A[n_a, :n_a] = 1
|
|
40
|
+
b = np.zeros(n_a + 1)
|
|
41
|
+
b[n_a] = 1
|
|
42
|
+
return A, b
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def minvar_kkt(R): # noqa: N803
|
|
46
|
+
"""Solve the minimum variance portfolio via the KKT system with active-set method.
|
|
47
|
+
|
|
48
|
+
Iteratively drops assets with negative weights until all remaining weights
|
|
49
|
+
are non-negative, solving the KKT system exactly at each iteration via
|
|
50
|
+
``numpy.linalg.solve``.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
R: Return matrix of shape (T, N).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Weight vector of shape (N,) summing to 1 with all non-negative entries.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
>>> import numpy as np
|
|
60
|
+
>>> from fast_minimum_variance.random import make_returns
|
|
61
|
+
>>> R = make_returns(100, 5, seed=0)
|
|
62
|
+
>>> w = minvar_kkt(R)
|
|
63
|
+
>>> w.shape
|
|
64
|
+
(5,)
|
|
65
|
+
>>> float(round(w.sum(), 10))
|
|
66
|
+
1.0
|
|
67
|
+
>>> bool((w >= 0).all())
|
|
68
|
+
True
|
|
69
|
+
"""
|
|
70
|
+
n = R.shape[1]
|
|
71
|
+
active = np.ones(n, dtype=bool)
|
|
72
|
+
while True:
|
|
73
|
+
A, b = build_kkt(R[:, active]) # noqa: N806
|
|
74
|
+
sol = np.linalg.solve(A, b)
|
|
75
|
+
w_a = sol[: active.sum()]
|
|
76
|
+
if np.all(w_a >= -1e-10):
|
|
77
|
+
break
|
|
78
|
+
active[np.where(active)[0][w_a < 0]] = False
|
|
79
|
+
w = np.zeros(n)
|
|
80
|
+
w[active] = np.maximum(w_a, 0)
|
|
81
|
+
return w
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Krylov subspace solvers for the minimum variance portfolio."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.sparse.linalg import LinearOperator, cg, minres
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def minvar_minres(R, c=1.0, gamma=0.0): # noqa: N803
|
|
8
|
+
"""Solve the minimum variance portfolio via MINRES with active-set method.
|
|
9
|
+
|
|
10
|
+
Applies the active-set method, dropping assets with negative weights, and
|
|
11
|
+
solves the KKT saddle-point system at each iteration using MINRES. The KKT
|
|
12
|
+
matrix is applied as a LinearOperator — no explicit matrix is ever formed.
|
|
13
|
+
The matvec for x = [v; mu] is::
|
|
14
|
+
|
|
15
|
+
out[:n_a] = 2 (c R^T(Rv) + gamma v) + mu * 1
|
|
16
|
+
out[n_a] = 1^T v
|
|
17
|
+
|
|
18
|
+
With the defaults ``c=1, gamma=0`` this solves the standard sample-covariance
|
|
19
|
+
problem. To apply dimension-based Ledoit-Wolf shrinkage without materialising
|
|
20
|
+
a stacked return matrix, compute::
|
|
21
|
+
|
|
22
|
+
T, N = R.shape
|
|
23
|
+
frob_sq = (R * R).sum()
|
|
24
|
+
alpha = N / (N + T) # shrinkage intensity
|
|
25
|
+
c = 1.0 - alpha # = T / (N + T)
|
|
26
|
+
gamma = frob_sq / (N + T) # diagonal regularisation (= alpha * frob_sq / N)
|
|
27
|
+
|
|
28
|
+
and call ``minvar_minres(R, c=c, gamma=gamma)``.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
R: Return matrix of shape (T, N).
|
|
32
|
+
c: Scaling factor for R^T R (default 1.0).
|
|
33
|
+
gamma: Diagonal regularisation added to the (1,1) block (default 0.0).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple (w, n_iters) where w is the weight vector of shape (N,) summing
|
|
37
|
+
to 1 with all non-negative entries and n_iters is the total number of
|
|
38
|
+
MINRES iterations across all active-set steps.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
>>> import numpy as np
|
|
42
|
+
>>> from fast_minimum_variance.random import make_returns
|
|
43
|
+
>>> R = make_returns(100, 5, seed=0)
|
|
44
|
+
>>> w, iters = minvar_minres(R)
|
|
45
|
+
>>> w.shape
|
|
46
|
+
(5,)
|
|
47
|
+
>>> float(round(w.sum(), 6))
|
|
48
|
+
1.0
|
|
49
|
+
>>> bool((w >= 0).all())
|
|
50
|
+
True
|
|
51
|
+
>>> iters > 0
|
|
52
|
+
True
|
|
53
|
+
"""
|
|
54
|
+
n = R.shape[1]
|
|
55
|
+
active = np.ones(n, dtype=bool)
|
|
56
|
+
total_iters = 0
|
|
57
|
+
while True:
|
|
58
|
+
r_a = R[:, active]
|
|
59
|
+
n_a = r_a.shape[1]
|
|
60
|
+
|
|
61
|
+
def _matvec(x, ra=r_a, na=n_a, cc=c, gam=gamma):
|
|
62
|
+
"""Apply KKT operator [[2(c R^TR + gamma I), 1],[1^T, 0]] to x."""
|
|
63
|
+
out = np.empty(na + 1)
|
|
64
|
+
out[:na] = 2.0 * (cc * (ra.T @ (ra @ x[:na])) + gam * x[:na]) + x[na]
|
|
65
|
+
out[na] = x[:na].sum()
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
b = np.zeros(n_a + 1)
|
|
69
|
+
b[n_a] = 1.0
|
|
70
|
+
kkt = LinearOperator(shape=(n_a + 1, n_a + 1), matvec=_matvec) # type: ignore[call-arg]
|
|
71
|
+
iters = [0]
|
|
72
|
+
sol, _ = minres(kkt, b, callback=lambda _x: iters.__setitem__(0, iters[0] + 1)) # noqa: B023
|
|
73
|
+
total_iters += iters[0]
|
|
74
|
+
w_a = sol[:n_a]
|
|
75
|
+
if np.all(w_a >= -1e-10):
|
|
76
|
+
break
|
|
77
|
+
active[np.where(active)[0][w_a < 0]] = False
|
|
78
|
+
w = np.zeros(n)
|
|
79
|
+
w[active] = np.maximum(w_a, 0)
|
|
80
|
+
w /= w.sum()
|
|
81
|
+
return w, total_iters
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def minvar_cg(R, c=1.0, gamma=0.0): # noqa: N803
|
|
85
|
+
"""Solve the minimum variance portfolio via CG in the constraint-reduced space.
|
|
86
|
+
|
|
87
|
+
Projects the problem onto the constraint-satisfying subspace using an
|
|
88
|
+
implicit Householder reflector as the null-space basis of the budget
|
|
89
|
+
constraint, then applies CG to the reduced positive-definite system
|
|
90
|
+
``P^T (c R^T R + gamma I) P``. An active-set loop drops assets with
|
|
91
|
+
negative weights until feasibility is reached.
|
|
92
|
+
|
|
93
|
+
The Householder vector ``v = [1+sqrt(n_a), 1, ..., 1]`` with
|
|
94
|
+
``beta = 1/(n_a + sqrt(n_a))`` defines the reflector H = I - beta*v*v^T.
|
|
95
|
+
Its last n_a-1 columns span the null space of 1^T and form the implicit
|
|
96
|
+
basis P. Applying P or P^T costs O(n_a) instead of O(n_a^2) for the
|
|
97
|
+
explicit QR matrix, and no O(n_a^2) matrix is ever formed. Because P has
|
|
98
|
+
orthonormal columns, ``P^T (gamma I) P = gamma I``, so the gamma term
|
|
99
|
+
adds only a scalar shift to each matvec.
|
|
100
|
+
|
|
101
|
+
With the defaults ``c=1, gamma=0`` this solves the standard sample-covariance
|
|
102
|
+
problem. See ``minvar_minres`` for the Ledoit-Wolf shrinkage recipe.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
R: Return matrix of shape (T, N).
|
|
106
|
+
c: Scaling factor for R^T R (default 1.0).
|
|
107
|
+
gamma: Diagonal regularisation added to the objective (default 0.0).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple (w, n_iters) where w is the weight vector of shape (N,) summing
|
|
111
|
+
to 1 with all non-negative entries and n_iters is the total number of
|
|
112
|
+
CG iterations across all active-set steps.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
>>> import numpy as np
|
|
116
|
+
>>> from fast_minimum_variance.random import make_returns
|
|
117
|
+
>>> R = make_returns(100, 5, seed=0)
|
|
118
|
+
>>> w, iters = minvar_cg(R)
|
|
119
|
+
>>> w.shape
|
|
120
|
+
(5,)
|
|
121
|
+
>>> float(round(w.sum(), 6))
|
|
122
|
+
1.0
|
|
123
|
+
>>> bool((w >= 0).all())
|
|
124
|
+
True
|
|
125
|
+
>>> iters > 0
|
|
126
|
+
True
|
|
127
|
+
"""
|
|
128
|
+
n = R.shape[1]
|
|
129
|
+
active = np.ones(n, dtype=bool)
|
|
130
|
+
total_iters = 0
|
|
131
|
+
while True:
|
|
132
|
+
r_a = R[:, active]
|
|
133
|
+
n_a = r_a.shape[1]
|
|
134
|
+
|
|
135
|
+
if n_a == 1:
|
|
136
|
+
w_a = np.array([1.0])
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
# Implicit Householder basis for null(1^T): v = [1+sqrt(n_a), 1,...,1]
|
|
140
|
+
sqrt_na = np.sqrt(float(n_a))
|
|
141
|
+
beta = 1.0 / (n_a + sqrt_na)
|
|
142
|
+
v0 = 1.0 + sqrt_na
|
|
143
|
+
|
|
144
|
+
def _p_apply(y, b=beta, vv=v0, na=n_a):
|
|
145
|
+
"""Apply implicit P (n_a x n_a-1) to y: O(n_a)."""
|
|
146
|
+
s = y.sum()
|
|
147
|
+
out = np.empty(na)
|
|
148
|
+
out[0] = -b * vv * s
|
|
149
|
+
out[1:] = y - (b * s)
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
def _pt_apply(u, b=beta, vv=v0):
|
|
153
|
+
"""Apply implicit P^T (n_a-1 x n_a) to u: O(n_a)."""
|
|
154
|
+
s = vv * u[0] + u[1:].sum()
|
|
155
|
+
return u[1:] - (b * s)
|
|
156
|
+
|
|
157
|
+
w0 = np.ones(n_a) / n_a
|
|
158
|
+
r0 = r_a @ w0
|
|
159
|
+
|
|
160
|
+
def _matvec(y, ra=r_a, b=beta, vv=v0, na=n_a, cc=c, gam=gamma):
|
|
161
|
+
"""Apply P^T (c R^T R + gamma I) P to y via implicit Householder."""
|
|
162
|
+
s = y.sum()
|
|
163
|
+
pv = np.empty(na)
|
|
164
|
+
pv[0] = -b * vv * s
|
|
165
|
+
pv[1:] = y - (b * s)
|
|
166
|
+
rpv = cc * (ra.T @ (ra @ pv)) + gam * pv
|
|
167
|
+
sv = vv * rpv[0] + rpv[1:].sum()
|
|
168
|
+
return rpv[1:] - (b * sv)
|
|
169
|
+
|
|
170
|
+
g0 = c * (r_a.T @ r0) + gamma * w0
|
|
171
|
+
rhs = -_pt_apply(g0)
|
|
172
|
+
op = LinearOperator(shape=(n_a - 1, n_a - 1), matvec=_matvec) # type: ignore[call-arg]
|
|
173
|
+
iters = [0]
|
|
174
|
+
sol, _ = cg(op, rhs, callback=lambda _x: iters.__setitem__(0, iters[0] + 1)) # noqa: B023
|
|
175
|
+
total_iters += iters[0]
|
|
176
|
+
w_a = w0 + _p_apply(sol)
|
|
177
|
+
if np.all(w_a >= -1e-10):
|
|
178
|
+
break
|
|
179
|
+
active[np.where(active)[0][w_a < 0]] = False
|
|
180
|
+
w = np.zeros(n)
|
|
181
|
+
w[active] = np.maximum(w_a, 0)
|
|
182
|
+
w /= w.sum()
|
|
183
|
+
return w, total_iters
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Return matrix generation utilities."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def make_returns(T, N, seed=42): # noqa: N803
|
|
7
|
+
"""Generate a T x N matrix of standard normal returns.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
T: Number of time steps (rows).
|
|
11
|
+
N: Number of assets (columns).
|
|
12
|
+
seed: Random seed for reproducibility.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Array of shape (T, N) with i.i.d. standard normal entries.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
>>> R = make_returns(100, 5, seed=0)
|
|
19
|
+
>>> R.shape
|
|
20
|
+
(100, 5)
|
|
21
|
+
>>> import numpy as np
|
|
22
|
+
>>> np.allclose(R.mean(axis=0), np.zeros(5), atol=0.3)
|
|
23
|
+
True
|
|
24
|
+
"""
|
|
25
|
+
rng = np.random.default_rng(seed)
|
|
26
|
+
return rng.standard_normal((T, N))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fast-minimum-variance
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: cvxpy>=1.0
|
|
8
|
+
Requires-Dist: numpy>=2.0.0
|
|
9
|
+
Requires-Dist: scipy>=1.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# [fast-minimum-variance](https://jebel-quant.github.io/fast_minimum_variance): Solving Minimum Variance Portfolios Fast
|
|
13
|
+
|
|
14
|
+
[](https://pypi.org/project/fast-minimum-variance/)
|
|
15
|
+
[](https://github.com/Jebel-Quant/fast_minimum_variance/blob/main/LICENSE)
|
|
16
|
+
[](https://github.com/jebel-quant/rhiza)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
**fast-minimum-variance** is a Python library for computing long-only minimum variance
|
|
21
|
+
portfolios without ever forming the sample covariance matrix. By operating directly on
|
|
22
|
+
the returns matrix $R \in \mathbb{R}^{T \times N}$, it exposes a clean hierarchy of
|
|
23
|
+
solvers — from an exact direct KKT solve to matrix-free Krylov methods — that scale
|
|
24
|
+
gracefully as $N$ grows.
|
|
25
|
+
|
|
26
|
+
The core insight is that minimising portfolio variance is equivalent to minimising
|
|
27
|
+
$\|Rw\|^2$, which can be evaluated using two matrix-vector products $w \mapsto R^\top(Rw)$
|
|
28
|
+
without constructing $R^\top R$ explicitly. This reframing connects the portfolio
|
|
29
|
+
optimisation literature directly to Krylov subspace methods.
|
|
30
|
+
|
|
31
|
+
The long-only constraint $w \geq 0$ is handled throughout via an **active-set method**:
|
|
32
|
+
solve the unconstrained problem on the current active set, drop assets with negative
|
|
33
|
+
weights, and repeat. The process terminates in at most $N$ iterations.
|
|
34
|
+
|
|
35
|
+
## Solvers
|
|
36
|
+
|
|
37
|
+
| Solver | Module | Method | Notes |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `minvar_kkt` | `kkt` | Direct KKT via `numpy.linalg.solve` | Exact; baseline for accuracy comparisons |
|
|
40
|
+
| `minvar_minres` | `krylov` | MINRES on the indefinite KKT system | Matrix-free capable; handles indefiniteness correctly |
|
|
41
|
+
| `minvar_cg` | `krylov` | CG in the constraint-reduced space | Positive-definite reduced system; no indefinite solver needed |
|
|
42
|
+
| `minvar_cvxpy` | `cvx` | General-purpose convex solver via CVXPY | Reference implementation; slowest but most flexible |
|
|
43
|
+
|
|
44
|
+
All solvers return a weight vector $w \in \mathbb{R}^N$ satisfying $\sum_i w_i = 1$ and $w_i \geq 0$.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fast_minimum_variance.random import make_returns
|
|
50
|
+
from fast_minimum_variance.kkt import minvar_kkt
|
|
51
|
+
from fast_minimum_variance.krylov import minvar_cg, minvar_minres
|
|
52
|
+
from fast_minimum_variance.cvx import minvar_cvxpy
|
|
53
|
+
|
|
54
|
+
# Generate a synthetic return matrix: 500 daily returns, 20 assets
|
|
55
|
+
R = make_returns(T=500, N=20, seed=42)
|
|
56
|
+
|
|
57
|
+
# Solve with any of the available solvers
|
|
58
|
+
w_kkt = minvar_kkt(R) # exact KKT solve
|
|
59
|
+
w_minres = minvar_minres(R) # MINRES on the indefinite KKT system
|
|
60
|
+
w_cg = minvar_cg(R) # CG in the constraint-reduced space
|
|
61
|
+
w_cvxpy = minvar_cvxpy(R) # CVXPY reference
|
|
62
|
+
|
|
63
|
+
# All solutions satisfy the portfolio constraints
|
|
64
|
+
assert abs(w_kkt.sum() - 1.0) < 1e-8
|
|
65
|
+
assert (w_kkt >= 0).all()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## The KKT System
|
|
69
|
+
|
|
70
|
+
The equality-constrained minimum variance problem yields the $(N+1) \times (N+1)$ KKT system:
|
|
71
|
+
|
|
72
|
+
$$\begin{pmatrix} 2R^\top R & \mathbf{1} \cr \mathbf{1}^\top & 0 \end{pmatrix} \begin{pmatrix} w \cr \lambda \end{pmatrix} = \begin{pmatrix} \mathbf{0} \cr 1 \end{pmatrix}$$
|
|
73
|
+
|
|
74
|
+
This system is **symmetric but indefinite** — the zero in the bottom-right corner of the
|
|
75
|
+
KKT matrix introduces a negative eigenvalue. This rules out standard CG on the full system,
|
|
76
|
+
but it opens the door to MINRES. Alternatively, the CG solver eliminates the constraint
|
|
77
|
+
entirely by parameterising $w = w_0 + Pv$ where $P$ spans the null space of
|
|
78
|
+
$\mathbf{1}^\top$, yielding a positive-definite reduced system of size $(N-1) \times (N-1)$.
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install fast-minimum-variance
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For development:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/Jebel-Quant/fast_minimum_variance
|
|
90
|
+
cd fast_minimum_variance
|
|
91
|
+
make install
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Requirements
|
|
95
|
+
|
|
96
|
+
- Python 3.11+
|
|
97
|
+
- numpy
|
|
98
|
+
- scipy
|
|
99
|
+
- cvxpy
|
|
100
|
+
|
|
101
|
+
## Citing
|
|
102
|
+
|
|
103
|
+
If you use this library in academic work or research, please cite:
|
|
104
|
+
|
|
105
|
+
```bibtex
|
|
106
|
+
@software{fast_minimum_variance,
|
|
107
|
+
author = {Schmelzer, Thomas},
|
|
108
|
+
title = {fast-minimum-variance: Solving Minimum Variance Portfolios Fast},
|
|
109
|
+
url = {https://github.com/Jebel-Quant/fast_minimum_variance},
|
|
110
|
+
year = {2026},
|
|
111
|
+
license = {MIT}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT License — see [LICENSE](https://github.com/Jebel-Quant/fast_minimum_variance/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fast_minimum_variance/__init__.py,sha256=TFCNorOmpOBZIzu8rVZgpAH9LPlw-4YDsQeDsV50BsU,29
|
|
2
|
+
fast_minimum_variance/cvx.py,sha256=bXtYiJaJtGHdhcsv3MAj-c9Vg5fknGXxPSbMcgkA5sw,957
|
|
3
|
+
fast_minimum_variance/kkt.py,sha256=BYjr4HkpV3_b2jJ0j5oX1l5_3XN7vFw6BHadOqYAWo8,2139
|
|
4
|
+
fast_minimum_variance/krylov.py,sha256=lxmWNnhLwcG_QTTv5zEJt5xTwJ0CHtLAJyghZjxM5Pw,6665
|
|
5
|
+
fast_minimum_variance/random.py,sha256=76M-13ExNggETggw44CuQ43JTARpOkCA8u-03-LzME0,677
|
|
6
|
+
fast_minimum_variance-0.2.0.dist-info/METADATA,sha256=DRqIAS7_Y046DODPpYAF6qUEQgiIgB6_YuQry51oGeM,4672
|
|
7
|
+
fast_minimum_variance-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
fast_minimum_variance-0.2.0.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
9
|
+
fast_minimum_variance-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jebel Quant Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|