quanttoolbox.optim¶
optim.proximal / optim.projection¶
Python alternatives
Keep — pyproximal covers some overlapping norm/constraint proximal operators, but is oriented toward signal-processing/inverse-problems use cases, not portfolio constraints (turnover, combined Dykstra-projected linear+box systems). No strong off-the-shelf equivalent for this exact operator set.
quanttoolbox.optim.proximal
¶
Proximal operators for L1/L2/Linf norms, box/equality/inequality constraints, and combined ("Dykstra alternating projection") constraint sets.
Ported from QuantToolBox/optim/{proximal_L1,proximal_L2,proximal_Linfinity, proximal_max,proximal_bounds,proximal_equality,proximal_inequality, proximal_linear_constraints,proximal_turnover,soft_thresholding}.m
Translation notes:
proximal_L1andsoft_thresholding(2-argument form) are algebraically identical in the original; onlysoft_thresholdingis kept here (as the more common name), withproximal_l1as an alias.proximal_bounds' original had two branches: a trivial closed-form clip (Proximal_Algorithm == 1) and a redundantquadprogcall for the same box projection (Proximal_Algorithm == 2, which solves the exact same problem -- projecting onto a box has a closed-form solution, so a QP solver is never actually needed). Only the closed-form clip is ported.proximal_equality's Dykstra-loop branch was commented out in the original in favor of the closed-formpinvsolution (which is exact for a single equality-constraint projection); that closed form is what's ported here.proximal_inequality/proximal_linear_constraints/proximal_turnover's Dykstra alternating-projection loops (combining multiple constraint sets) are preserved, since combined constraint projection generally has no closed form.- MATLAB's
global Proximal_MaxItersis replaced byquanttoolbox.config.ProximalConfig.
proximal_bounds(v, lb, ub)
¶
Projection onto a box [lb, ub] (closed-form clip).
Original: optim/proximal_bounds.m
Source code in src/quanttoolbox/optim/proximal.py
97 98 99 100 101 102 103 | |
proximal_equality(v, a_eq, b_eq)
¶
Projection onto the affine subspace {x : A_eq @ x == b_eq}.
Original: optim/proximal_equality.m
Returns (x, retcode) with retcode always 0 (kept for API parity with proximal_inequality/proximal_linear_constraints, which can fail).
Source code in src/quanttoolbox/optim/proximal.py
106 107 108 109 110 111 112 113 114 115 116 117 118 | |
proximal_inequality(v, c_ineq, d_ineq, config=None)
¶
Projection onto a polyhedron {x : C_ineq @ x <= D_ineq} via Dykstra's alternating projection algorithm (cycling through each row/half-space).
Original: optim/proximal_inequality.m
Returns (x, retcode); retcode is -1 if max_iters was reached without convergence, 0 otherwise.
Source code in src/quanttoolbox/optim/proximal.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
proximal_l2(v, lambda_)
¶
Proximal operator of the (scaled) L2 norm: shrinks v toward the origin by at most lambda_ in Euclidean length.
Original: optim/proximal_L2.m
Source code in src/quanttoolbox/optim/proximal.py
60 61 62 63 64 65 66 67 68 | |
proximal_linear_constraints(v, a_eq=None, b_eq=None, c_ineq=None, d_ineq=None, lb=None, ub=None, config=None)
¶
Projection onto the intersection of an affine subspace, a polyhedron, and a box, via Dykstra's alternating projection algorithm. Pass None for any constraint set to omit it.
Original: optim/proximal_linear_constraints.m
Note: like the original, the exact-equality convergence check
(x1 == x4 in MATLAB, np.allclose here) can occasionally exit
early during a temporary plateau in the iterate sequence, before
reaching the true intersection point -- this is most likely for
inputs sitting near a "corner" where the constraint sets meet
tangentially. If a result looks suspicious, verify constraint
satisfaction directly and re-run with a larger max_iters and a
perturbed starting point if needed.
Source code in src/quanttoolbox/optim/proximal.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
proximal_linfinity(v, lambda_)
¶
Proximal operator of lambda * ||v||_infinity.
Original: optim/proximal_Linfinity.m
Source code in src/quanttoolbox/optim/proximal.py
88 89 90 91 92 93 94 | |
proximal_max(v, lambda_)
¶
Proximal operator of lambda * max(v): caps the largest entries of v so that the total amount "shaved off" sums to lambda_ (used inside proximal_Linfinity / projection_L1's simplex-style water-filling step).
Original: optim/proximal_max.m
Source code in src/quanttoolbox/optim/proximal.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
proximal_turnover(v, a_eq, b_eq, c_ineq, d_ineq, lb, ub, x0, tau, config=None)
¶
Projection onto the intersection of an affine subspace, a polyhedron, a box, and a turnover constraint (||x - x0||_1 <= tau), via Dykstra's alternating projection algorithm.
Original: optim/proximal_turnover.m
Source code in src/quanttoolbox/optim/proximal.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
soft_thresholding(v, lambda_minus, lambda_plus=None)
¶
Soft-thresholding / proximal operator of the L1 norm.
One-argument-lambda form: symmetric soft threshold, sign(v) * max(|v|-lambda, 0). Two-argument form: asymmetric threshold with separate positive/negative shrinkage.
Original: optim/soft_thresholding.m (also optim/proximal_L1.m, identical)
Source code in src/quanttoolbox/optim/proximal.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
quanttoolbox.optim.projection
¶
Projection operators onto L1/L2/Linf norm balls and a box-intersect-L2-ball set.
Ported from QuantToolBox/optim/{projection_L1,projection_L2, projection_Linfinity,projection_box_L2}.m
Translation notes:
projection_L2is literallyv - proximal_l2(v, lambda)in the original (projection = identity minus the proximal/shrinkage step); reproduced as-is here rather than re-derived, to stay faithful.projection_box_L2(project onto a box intersected with an L2 ball centered at c) uses Dykstra's alternating projection algorithm, same pattern asproximal_linear_constraints.
projection_box_l2(v, x_minus, x_plus, c, lambda_, config=None)
¶
Projection onto the intersection of a box [x_minus, x_plus] and an L2 ball of radius lambda_ centered at c, via Dykstra's alternating projection algorithm.
Original: optim/projection_box_L2.m
Source code in src/quanttoolbox/optim/projection.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
projection_l1(v, radius, method=1)
¶
Euclidean projection of v onto the L1 ball of the given radius.
method=1 (default): exact sorted-cumsum water-filling algorithm. method=2: equivalent computation via proximal_max.
Original: optim/projection_L1.m
Source code in src/quanttoolbox/optim/projection.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
projection_l2(v, lambda_)
¶
Euclidean projection removing at most lambda_ of v's L2 length (identity minus the L2 proximal/shrinkage step).
Original: optim/projection_L2.m
Source code in src/quanttoolbox/optim/projection.py
49 50 51 52 53 54 55 56 | |
projection_linfinity(v, radius)
¶
Euclidean projection of v onto the L-infinity ball of the given radius (simple elementwise clip to [-radius, radius]).
Original: optim/projection_Linfinity.m
Source code in src/quanttoolbox/optim/projection.py
59 60 61 62 63 64 65 66 | |
Examples¶
Proximal projection under bounds, (in)equalities, and combined constraints — optim/proximal1.py
"""Translated from Examples/optim/proximal1.m -- proximal projections onto
boxes, single/multiple inequality constraints, single/multiple equality
constraints, and combined linear-constraint sets, on a fixed random
starting point.
The original compares `Proximal_Algorithm = 1` (closed-form, where one
exists) against `Proximal_Algorithm = 2` (an alternate/QP-based
computation of the *same* projection) side by side. During porting, the
redundant `Proximal_Algorithm == 2` branches for `proximal_bounds` and
`proximal_equality` were dropped (see `proximal.py`'s module docstring --
they solve the exact same problem the closed form already solves exactly,
so keeping both added no information), and `proximal_inequality`/
`proximal_linear_constraints` only ever had one (Dykstra) implementation.
So there is no second variant left to compare here -- each constraint
type is projected onto once below, rather than twice.
The original draws x from MATLAB's unseeded `rand`; a fixed seed
(`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.optim.proximal import (
proximal_bounds,
proximal_equality,
proximal_inequality,
proximal_linear_constraints,
)
rng = np.random.default_rng(0)
n = 10
x = rng.random(n)
print("x:", np.round(x, 4))
# Lower & upper bounds
lb, ub = np.zeros(n), 0.5 * np.ones(n)
x1 = proximal_bounds(x, lb, ub)
print("\nBounds [0, 0.5]:", np.round(x1, 4))
# One inequality constraint
c_ineq = np.zeros((1, n))
c_ineq[0, :4] = 0.25
d_ineq = np.array([0.5])
x1, rc = proximal_inequality(x, c_ineq, d_ineq)
print("\nInequality (1 constraint):", np.round(x1, 4), "retcode:", rc)
# Two inequality constraints
c_ineq = np.zeros((2, n))
d_ineq = np.zeros(2)
c_ineq[0, :4] = 0.25
c_ineq[1, 3:6] = -np.array([1, 2, 2])
d_ineq[0] = 0.5
d_ineq[1] = -2.0
x1, rc = proximal_inequality(x, c_ineq, d_ineq)
print("\nInequality (2 constraints):", np.round(x1, 4), "retcode:", rc)
# Four inequality constraints
c_ineq = np.zeros((4, n))
d_ineq = np.zeros(4)
c_ineq[0, :4] = 0.25
c_ineq[1, 3:6] = -np.array([1, 2, 2])
c_ineq[2, 9] = 1
c_ineq[3, 9] = -1
d_ineq[0] = 0.5
d_ineq[1] = -2.0
d_ineq[2] = 0.1
d_ineq[3] = -0.1
x1, rc = proximal_inequality(x, c_ineq, d_ineq)
print("\nInequality (4 constraints):", np.round(x1, 4), "retcode:", rc)
# One equality constraint
a_eq = np.ones((1, n))
b_eq = np.array([4.0])
x1, rc = proximal_equality(x, a_eq, b_eq)
print("\nEquality (1 constraint):", np.round(x1, 4), "retcode:", rc)
# Two equality constraints
a_eq = np.zeros((2, n))
b_eq = np.zeros(2)
a_eq[0, :] = 1.0
a_eq[1, [0, 1]] = [1, -1]
b_eq[0] = 4.0
x1, rc = proximal_equality(x, a_eq, b_eq)
print("\nEquality (2 constraints):", np.round(x1, 4), "retcode:", rc)
# Linear constraints: 1 equality + 1 inequality + bounds
a_eq = np.ones((1, n))
b_eq = np.array([4.0])
c_ineq = np.zeros((1, n))
d_ineq = np.array([0.5])
c_ineq[0, :4] = 0.25
lb, ub = np.zeros(n), 0.5 * np.ones(n)
x1, rc = proximal_linear_constraints(
x, a_eq=a_eq, b_eq=b_eq, c_ineq=c_ineq, d_ineq=d_ineq, lb=lb, ub=ub
)
print("\nLinear constraints (1 eq, 1 ineq, bounds):", np.round(x1, 4), "retcode:", rc)
# Linear constraints: 2 equality + 2 inequality + bounds
a_eq = np.zeros((2, n))
b_eq = np.zeros(2)
a_eq[0, :] = 1.0
a_eq[1, [0, 1]] = [1, -1]
b_eq[0] = 4.0
c_ineq = np.zeros((2, n))
d_ineq = np.zeros(2)
c_ineq[0, :4] = 0.25
c_ineq[1, 3:6] = -np.array([1, 2, 2])
d_ineq[0] = 0.5
d_ineq[1] = -2.0
x1, rc = proximal_linear_constraints(
x, a_eq=a_eq, b_eq=b_eq, c_ineq=c_ineq, d_ineq=d_ineq, lb=lb, ub=ub
)
print("\nLinear constraints (2 eq, 2 ineq, bounds):", np.round(x1, 4), "retcode:", rc)
Proximal-L1 vs. two L1-ball-projection turnover algorithms — optim/prox_turnover1.py
"""Translated from Examples/optim/prox_turnover1.m -- compares the L1
proximal (soft-thresholding) operator against the L1-ball projection (both
its default sorted-cumsum algorithm and its `proximal_max`-based
alternative) at two different starting points and lambda values.
The original draws `v` from MATLAB's unseeded `rand`; a fixed seed
(`np.random.default_rng(0)`) is substituted here for reproducibility, same
convention used elsewhere in this port (see matrix1.py, building_blocks.md)."""
import numpy as np
from quanttoolbox.optim.projection import projection_l1
from quanttoolbox.optim.proximal import soft_thresholding
rng = np.random.default_rng(0)
v = 5 * (rng.random(10) - 0.5)
lambda_ = 1.20
x0 = np.zeros(10)
x1 = soft_thresholding(v - x0, lambda_) + x0
x2 = projection_l1(v - x0, lambda_) + x0
x3 = projection_l1(v - x0, lambda_, method=0) + x0
x = np.column_stack([v, x1, x2, x3])
print("lambda =", lambda_, " x0 = 0")
print("columns: v, proximal_L1, projection_L1 (method 1), projection_L1 (method 0)")
print(x)
print("column sums of |x - x0|:", np.sum(np.abs(x - x0[:, None]), axis=0))
lambda_ = 2.00
x0 = np.ones(10)
x0 = x0 / np.sum(x0)
x1 = soft_thresholding(v - x0, lambda_) + x0
x2 = projection_l1(v - x0, lambda_) + x0
x3 = projection_l1(v - x0, lambda_, method=0) + x0
x = np.column_stack([v, x1, x2, x3])
print("\nlambda =", lambda_, " x0 = equal-weight (sums to 1)")
print("columns: v, proximal_L1, projection_L1 (method 1), projection_L1 (method 0)")
print(x)
print("column sums of |x - x0|:", np.sum(np.abs(x - x0[:, None]), axis=0))
optim.quadprog (solve_qp)¶
Python alternatives
Hybrid: for hot loops calling solve_qp many times (e.g. risk-budgeting's inner ADMM loop), calling qpsolvers.solve_qp directly — bypassing cvxpy's DSL-parsing overhead — would likely be faster. Worth profiling if performance matters; keep the cvxpy-based version for its more expressive constraint composition (ridge/lasso penalties, arbitrary constraints).
quanttoolbox.optim.quadprog
¶
Quadratic programming: a general QP solver plus ridge/lasso/turnover penalized variants, and the closed-form QP-on-a-hyperplane solution.
Ported from QuantToolBox/optim/{quadprog_bc_ccd,quadprog_lasso, quadprog_ridge,quadprog_turnover,quadprog_mixed_norm, quadprog_mixed2_norm,qp_hyperplane}.m
Translation notes -- this is the single biggest architectural simplification in the whole port:
MATLAB's Optimization Toolbox quadprog cannot express an L1 penalty
term directly, so the original toolbox works around this with a
"variable-splitting" trick: introduce two extra non-negative variable
blocks per L1 term (x = x+ - x-), turning each lasso/mixed-norm/turnover
problem into a bigger, purely-quadratic QP that quadprog can solve. This
produces four near-duplicate ~100-line MATLAB functions
(quadprog_lasso/ridge/mixed_norm/mixed2_norm), each hand-building the
block matrices for a different combination of penalties.
cvxpy supports L1/L2 norm terms and turnover (L1-ball) constraints
natively in its objective/constraint DSL, so none of that
variable-splitting machinery is needed here. All four MATLAB functions
above collapse into one function, solve_qp, parameterized by which
penalty terms and constraints are supplied:
solve_qp(q, r) # plain QP (replaces quadprog_ridge with no penalty)
solve_qp(q, r, ridge_penalty=(gamma, x_target)) # replaces quadprog_ridge
solve_qp(q, r, lasso_penalty=(gamma, x_target)) # replaces quadprog_lasso
solve_qp(q, r, ridge_penalty=..., lasso_penalty=...) # replaces quadprog_mixed_norm
solve_qp(q, r, turnover=(x0, tau)) # replaces the turnover-constrained
# branch of quadprog_turnover
(quadprog_mixed2_norm's "two separate lasso/ridge penalties toward two
different targets" case is representable by simply adding the two ridge
penalties together algebraically, since ridge penalties toward different
targets combine into a single quadratic form -- not separately exposed as
a distinct code path here.)
The objective throughout is 0.5 * x'Qx - R'x (MATLAB's convention,
note the minus sign on the linear term), matching every one of the
original functions' calling convention.
quadprog_bc_ccd (box-constrained QP via cyclical coordinate descent)
is preserved as a separate, dependency-free NumPy implementation since
it's a genuinely different (iterative, matrix-free-friendly) algorithm
worth keeping available alongside the cvxpy path.
qp_hyperplane (closed-form QP with a single equality constraint) has
an exact closed-form Lagrangian solution and needs no solver at all.
qp_bc_ccd(q, r, x_minus=None, x_plus=None, x_init=None, n_iters=100)
¶
Box-constrained QP (min 0.5*x'Qx - R'x s.t. x_minus <= x <= x_plus) via cyclical coordinate descent -- a lightweight, dependency-free alternative to solve_qp for this specific (box-only) case.
Original: optim/quadprog_bc_ccd.m
Source code in src/quanttoolbox/optim/quadprog.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
qp_hyperplane(q, r, a, b)
¶
Closed-form solution of min 0.5*x'Qx - R'x s.t. a'x = b (single equality constraint), via the Lagrangian stationary conditions.
Original: optim/qp_hyperplane.m
Returns (x, lagrange_multiplier).
Source code in src/quanttoolbox/optim/quadprog.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
solve_qp(q, r, a_eq=None, b_eq=None, c_ineq=None, d_ineq=None, lb=None, ub=None, ridge_penalty=None, lasso_penalty=None, turnover=None, default_budget_constraint=False)
¶
Solve min 0.5*x'Qx - R'x subject to the given constraints and optional ridge/lasso penalty terms and turnover budget.
Original: optim/{quadprog_lasso,quadprog_ridge,quadprog_turnover, quadprog_mixed_norm,quadprog_mixed2_norm}.m (consolidated -- see module docstring)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
q
|
QP objective 0.5*x'Qx - R'x.
|
|
required |
r
|
QP objective 0.5*x'Qx - R'x.
|
|
required |
a_eq
|
equality constraints A_eq @ x == B_eq.
|
|
None
|
b_eq
|
equality constraints A_eq @ x == B_eq.
|
|
None
|
c_ineq
|
inequality constraints C_ineq @ x <= D_ineq.
|
|
None
|
d_ineq
|
inequality constraints C_ineq @ x <= D_ineq.
|
|
None
|
lb
|
box bounds.
|
|
None
|
ub
|
box bounds.
|
|
None
|
ridge_penalty
|
(gamma, x_target) adds gamma * ||x - x_target||_2^2 to
|
the objective. gamma may be a scalar, a vector (diagonal), or a full matrix. |
None
|
lasso_penalty
|
(gamma, x_target) adds gamma' * |x - x_target| to the
|
objective (elementwise L1, gamma may be scalar or vector). |
None
|
turnover
|
(x0, tau) constrains ||x - x0||_1 <= tau.
|
|
None
|
default_budget_constraint
|
if True and a_eq/b_eq are not given, adds
|
the default sum(x) == 1 budget constraint (matches the original's
|
False
|
Source code in src/quanttoolbox/optim/quadprog.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
Examples¶
Mean-variance optimization plus ridge/lasso-penalized portfolios — rpb/test_lasso1.py
"""Translated from Examples/rpb/test_lasso1.m -- Roncalli [2013],
"Introduction to Risk Parity and Budgeting", Example 1 (page 53):
compares an unconstrained gamma-problem MVO portfolio against the same
problem with a budget (sum-to-1) constraint, and ridge/lasso-penalized
variants (toward 0 and toward equal-weight) of the unconstrained
problem.
`compute_mvo_portfolio`/`quadprog_ridge`/`quadprog_lasso` map to
`mvo_portfolio`/`solve_qp(..., ridge_penalty=...)`/`solve_qp(...,
lasso_penalty=...)` as established in test_lasso3.py/test_lasso5.py; the
original's `0, 0` sentinel arguments for "no equality constraint" /
"no bounds" map to simply omitting those keyword arguments."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
x0 = np.full(n, 1 / n)
# Case gamma-problem
gamma_x = 0.5
x1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x).weights
x2 = mvo_portfolio(
mu, cov_matrix, gamma=gamma_x, a_eq=np.ones((1, n)), b_eq=np.array([1.0])
).weights
lambda_ridge = 0.03
s_ridge = lambda_ridge * np.eye(n)
x3 = solve_qp(cov_matrix, gamma_x * mu, ridge_penalty=(s_ridge, np.zeros(n)))
x4 = solve_qp(cov_matrix, gamma_x * mu, ridge_penalty=(s_ridge, x0))
lambda_lasso = 0.03 / 2
s_lasso = lambda_lasso * np.ones(n)
x5 = solve_qp(cov_matrix, gamma_x * mu, lasso_penalty=(s_lasso, np.zeros(n)))
x6 = solve_qp(cov_matrix, gamma_x * mu, lasso_penalty=(s_lasso, x0))
results = 100 * np.column_stack([x1, x2, x3, x4, x5, x6])
print(" x1 x2 x3 x4 x5 x6")
print(np.round(results, 2))
Mixed ridge+lasso penalties toward two different target vectors — rpb/test_lasso5.py
"""Translated from Examples/rpb/test_lasso5.m -- ridge/lasso/mixed
portfolios with two different ridge/lasso target vectors (equal-weight
and a custom 20/20/30/30 target)."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
a_eq, b_eq = np.ones((1, n)), np.array([1.0])
gamma_x = 0.5
y1 = np.full(n, 1 / n)
y2 = np.array([0.20, 0.20, 0.30, 0.30])
S_ridge = np.diag(np.diag(cov_matrix))
lambda_lasso = 0.005
x1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0).weights
x2 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, ridge_penalty=(S_ridge, y1)
)
x4 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, lasso_penalty=(lambda_lasso, y1)
)
x6 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, y1),
lasso_penalty=(lambda_lasso, y1),
)
# mixed with DIFFERENT targets for ridge (toward y1) vs lasso (toward y2)
x8 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, y1),
lasso_penalty=(lambda_lasso, y2),
)
for name, x in [
("MVO", x1),
("Ridge->y1", x2),
("Lasso->y1", x4),
("Mixed(ridge->y1,lasso->y1)", x6),
("Mixed(ridge->y1,lasso->y2)", x8),
]:
print(f"{name}: {np.round(x, 4)}")
Ridge, lasso, and mixed-norm penalized portfolios — rpb/test_lasso3.py
"""Translated from Examples/rpb/test_lasso3.m (byte-identical to
test_lasso4.m) -- MVO/ridge/lasso/mixed-penalty portfolios via the
consolidated solve_qp (replacing the original's separate quadprog_ridge/
quadprog_lasso/quadprog_mixed calls -- see optim/quadprog.py docstring)."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
x0 = np.full(n, 1 / n)
gamma_x = 0.5
a_eq, b_eq = np.ones((1, n)), np.array([1.0])
# gamma-problem (plain MVO)
r1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0)
x1 = r1.weights
# ridge-problem: penalize toward zero, scaled by each asset's own variance
S_ridge = np.diag(np.diag(cov_matrix))
x2 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, ridge_penalty=(S_ridge, x0)
)
# lasso-problem: L1 penalty toward equal weight
lambda_lasso = 0.005
x3 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, lasso_penalty=(lambda_lasso, x0)
)
# mixed-problem: both ridge and lasso, toward various targets
x4 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, np.zeros(n)),
lasso_penalty=(lambda_lasso, np.zeros(n)),
)
x5 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, x0),
lasso_penalty=(lambda_lasso, np.zeros(n)),
)
x6 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, np.zeros(n)),
lasso_penalty=(lambda_lasso, x0),
)
x7 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, x0),
lasso_penalty=(lambda_lasso, x0),
)
for name, x in [
("MVO", x1),
("Ridge", x2),
("Lasso", x3),
("Mixed(0,0)", x4),
("Mixed(EW,0)", x5),
("Mixed(0,EW)", x6),
("Mixed(EW,EW)", x7),
]:
print(f"{name}: {np.round(x, 4)}")
optim.bisection¶
Python alternatives
Hybrid: scipy.optimize.brentq/bisect converge faster (superlinear) for scalar root-finding and are compiled. Keep our vectorized (array-broadcast, many-roots-at-once) version — scipy's root finders are scalar-only.
quanttoolbox.optim.bisection
¶
Scalar (or elementwise-vectorized) bisection root-finding, and linear constraint explicit<->implicit (null-space) parametrization conversion.
Ported from QuantToolBox/optim/{bisection,bisection2,explicit2implicit, implicit2explicit}.m
Translation notes:
- Both bisection variants are vectorized in the original (operating on
arrays
a/belementwise, not just scalars) -- preserved here via plain NumPy elementwise operations. bisection2carries an auxiliary state variablezthrough the function evaluations (useful whenfhandlealso needs to return some side computation to warm-start the next evaluation); the Python signature keeps the same(y, z) = fhandle(x, z)calling convention.explicit2implicit/implicit2explicitconvert between an explicit linear-constraint representation (C @ x = c) and an implicit null-space parametrization (x = R @ rfor free parameter r) -- MATLAB'snull(...)maps toscipy.linalg.null_space.- MATLAB's
global BISECTION_Tolis replaced byquanttoolbox.config.BisectionConfig.
bisection(fhandle, a, b, config=None)
¶
Find the root of fhandle within bracket [a, b] via bisection (elementwise, if a/b are arrays -- each element is bracketed and solved independently).
Original: optim/bisection.m
Source code in src/quanttoolbox/optim/bisection.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
bisection2(fhandle, a, b, z0, config=None)
¶
Bisection root-finding where fhandle also threads an auxiliary state z through each evaluation: (y, z) = fhandle(x, z).
Original: optim/bisection2.m
Source code in src/quanttoolbox/optim/bisection.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
explicit_to_implicit(cc, c)
¶
Convert an explicit linear-constraint system (CC @ x = c) into an implicit null-space parametrization x = RR @ r + r0, returning (RR, r0).
Original: optim/explicit2implicit.m
Source code in src/quanttoolbox/optim/bisection.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
implicit_to_explicit(rr, r)
¶
Convert an implicit null-space parametrization (x = RR @ r) back into an explicit linear-constraint system (CC @ x = c).
Original: optim/implicit2explicit.m
Source code in src/quanttoolbox/optim/bisection.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Examples¶
Explicit-to-implicit conversion for three simultaneous restrictions — optim/explicit3.py
"""Translated from Examples/optim/explicit3.m -- explicit-to-implicit
conversion for three simultaneous zero-restrictions on an 8-parameter
vector (as would arise e.g. from AR1_12=0, MA1_11=0, MA1_21=0 in a VAR
specification)."""
import numpy as np
from quanttoolbox.optim.bisection import explicit_to_implicit
CC = np.zeros((3, 8))
CC[0, 2] = 1 # beta[2] = 0 (AR1_12)
CC[1, 4] = 1 # beta[4] = 0 (MA1_11)
CC[2, 5] = 1 # beta[5] = 0 (MA1_21)
c = np.zeros(3)
RR, r = explicit_to_implicit(CC, c)
print("R:")
print(RR)
print("\nr:", r)
Explicit/implicit constraint round-trip, plus a design() demo — optim/explicit2.py
"""Translated from Examples/optim/explicit2.m -- explicit<->implicit
constraint conversion for a single equality constraint (x[0] = x[1]), plus
a `design` matrix built from a category-index vector."""
import numpy as np
from quanttoolbox.linalg.special_matrices import design
from quanttoolbox.optim.bisection import explicit_to_implicit, implicit_to_explicit
CC = np.array([[1.0, -1.0, 0, 0, 0, 0, 0, 0]]) # constraint: x[0] - x[1] = 0
c = np.array([0.0])
RR, r = explicit_to_implicit(CC, c)
CC2, c2 = implicit_to_explicit(RR, r)
print("R:")
print(RR)
print("\nr:", r)
print("\nC:")
print(CC2)
print("\nc:", c2)
w = np.concatenate([[1.0, 1.0], np.arange(2, 8, dtype=float)]) # seqa(2,1,6)
RR2 = design(w)
r2 = np.zeros(8)
print("\nR (design(w)):")
print(RR2)
print("\nr:", r2)