Coverage for src / cvx / risk / portfolio / min_risk.py: 100%
8 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 03:39 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 03:39 +0000
1"""Minimum risk portfolio optimization.
3This module provides functions for creating and solving minimum risk portfolio
4optimization problems using various risk models.
6Example:
7 Create and solve a minimum risk portfolio problem:
9 >>> import cvxpy as cp
10 >>> import numpy as np
11 >>> from cvx.risk.sample import SampleCovariance
12 >>> from cvx.risk.portfolio import minrisk_problem
13 >>> # Create risk model
14 >>> model = SampleCovariance(num=3)
15 >>> model.update(
16 ... cov=np.array([[1.0, 0.5, 0.0], [0.5, 1.0, 0.5], [0.0, 0.5, 1.0]]),
17 ... lower_assets=np.zeros(3),
18 ... upper_assets=np.ones(3)
19 ... )
20 >>> # Create optimization problem
21 >>> weights = cp.Variable(3)
22 >>> problem = minrisk_problem(model, weights)
23 >>> # Solve the problem
24 >>> _ = problem.solve(solver="CLARABEL")
25 >>> # Optimal weights sum to 1
26 >>> bool(np.isclose(np.sum(weights.value), 1.0))
27 True
29"""
31# Copyright 2023 Stanford University Convex Optimization Group
32#
33# Licensed under the Apache License, Version 2.0 (the "License");
34# you may not use this file except in compliance with the License.
35# You may obtain a copy of the License at
36#
37# http://www.apache.org/licenses/LICENSE-2.0
38#
39# Unless required by applicable law or agreed to in writing, software
40# distributed under the License is distributed on an "AS IS" BASIS,
41# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
42# See the License for the specific language governing permissions and
43# limitations under the License.
44from __future__ import annotations
46from typing import Any
48import cvxpy as cp
50from cvx.risk import Model
53def minrisk_problem(
54 riskmodel: Model,
55 weights: cp.Variable,
56 base: cp.Expression | float = 0.0,
57 constraints: list[cp.Constraint] | None = None,
58 **kwargs: Any,
59) -> cp.Problem:
60 """Create a minimum-risk portfolio optimization problem.
62 This function creates a CVXPY optimization problem that minimizes portfolio
63 risk subject to constraints. The problem includes standard constraints
64 (weights sum to 1, weights are non-negative) plus any model-specific and
65 custom constraints.
67 Args:
68 riskmodel: A risk model implementing the `Model` interface, used to
69 compute portfolio risk. Can be SampleCovariance, FactorModel,
70 CVar, etc.
71 weights: CVXPY variable representing the portfolio weights. Should have
72 shape (n,) where n is the number of assets.
73 base: Expression representing the base portfolio (default 0.0). Use this
74 for tracking error minimization where you want to minimize the risk
75 of deviating from a benchmark.
76 constraints: Optional list of additional CVXPY constraints to apply to
77 the optimization problem.
78 **kwargs: Additional keyword arguments passed to the risk model's
79 estimate and constraints methods.
81 Returns:
82 A CVXPY Problem that minimizes portfolio risk subject to constraints.
83 The problem includes:
84 - Objective: minimize risk(weights - base)
85 - Constraint: sum(weights) == 1
86 - Constraint: weights >= 0
87 - Model-specific constraints from riskmodel.constraints()
88 - Any additional constraints passed in the constraints argument
90 Example:
91 Basic minimum risk portfolio:
93 >>> import cvxpy as cp
94 >>> import numpy as np
95 >>> from cvx.risk.sample import SampleCovariance
96 >>> from cvx.risk.portfolio import minrisk_problem
97 >>> model = SampleCovariance(num=2)
98 >>> model.update(
99 ... cov=np.array([[1.0, 0.5], [0.5, 2.0]]),
100 ... lower_assets=np.zeros(2),
101 ... upper_assets=np.ones(2)
102 ... )
103 >>> weights = cp.Variable(2)
104 >>> problem = minrisk_problem(model, weights)
105 >>> _ = problem.solve(solver="CLARABEL")
106 >>> # Lower variance asset gets higher weight
107 >>> bool(weights.value[0] > weights.value[1])
108 True
110 With tracking error (minimize deviation from benchmark):
112 >>> benchmark = np.array([0.5, 0.5])
113 >>> problem = minrisk_problem(model, weights, base=benchmark)
114 >>> _ = problem.solve(solver="CLARABEL")
116 With custom constraints:
118 >>> custom_constraints = [weights[0] >= 0.3] # At least 30% in first asset
119 >>> problem = minrisk_problem(model, weights, constraints=custom_constraints)
120 >>> _ = problem.solve(solver="CLARABEL")
121 >>> bool(weights.value[0] >= 0.3 - 1e-6)
122 True
124 Sector constraints example (limiting sector exposure):
126 >>> model = SampleCovariance(num=4)
127 >>> # Tech: assets 0,1; Finance: assets 2,3
128 >>> model.update(
129 ... cov=np.eye(4) * 0.04, # 20% vol each, uncorrelated
130 ... lower_assets=np.zeros(4),
131 ... upper_assets=np.ones(4)
132 ... )
133 >>> weights = cp.Variable(4)
134 >>> tech_constraint = [weights[0] + weights[1] <= 0.5] # Max 50% tech
135 >>> problem = minrisk_problem(model, weights, constraints=tech_constraint)
136 >>> _ = problem.solve(solver="CLARABEL")
137 >>> tech_weight = weights.value[0] + weights.value[1]
138 >>> bool(tech_weight <= 0.5 + 1e-6)
139 True
141 Long-short portfolio (removing non-negativity by adjusting bounds):
143 >>> from cvx.risk.bounds import Bounds
144 >>> model = SampleCovariance(num=3)
145 >>> cov = np.array([[0.04, 0.01, 0.02],
146 ... [0.01, 0.09, 0.01],
147 ... [0.02, 0.01, 0.04]])
148 >>> model.update(
149 ... cov=cov,
150 ... lower_assets=np.array([-0.5, -0.5, -0.5]), # Allow shorting
151 ... upper_assets=np.array([1.5, 1.5, 1.5])
152 ... )
153 >>> weights = cp.Variable(3)
154 >>> # Override default non-negativity with custom constraints
155 >>> long_short_constraints = [weights >= -0.5, weights <= 1.5]
156 >>> problem = cp.Problem(
157 ... cp.Minimize(model.estimate(weights)),
158 ... [cp.sum(weights) == 1.0] + model.constraints(weights)
159 ... )
160 >>> _ = problem.solve(solver="CLARABEL")
161 >>> bool(np.isclose(np.sum(weights.value), 1.0, atol=1e-4))
162 True
164 Using with FactorModel and explicit factor exposure variable:
166 >>> from cvx.risk.factor import FactorModel
167 >>> factor_model = FactorModel(assets=4, k=2)
168 >>> factor_model.update(
169 ... exposure=np.array([[1.0, 0.8, 0.2, 0.1],
170 ... [0.1, 0.2, 0.9, 1.0]]),
171 ... cov=np.eye(2) * 0.04,
172 ... idiosyncratic_risk=np.array([0.1, 0.1, 0.1, 0.1]),
173 ... lower_assets=np.zeros(4),
174 ... upper_assets=np.ones(4),
175 ... lower_factors=-np.ones(2),
176 ... upper_factors=np.ones(2)
177 ... )
178 >>> weights = cp.Variable(4)
179 >>> y = cp.Variable(2) # Factor exposures
180 >>> problem = minrisk_problem(factor_model, weights, y=y)
181 >>> _ = problem.solve(solver="CLARABEL")
182 >>> bool(np.isclose(np.sum(weights.value), 1.0, atol=1e-4))
183 True
185 """
186 # if no constraints are specified
187 constraints = constraints or []
189 problem = cp.Problem(
190 objective=cp.Minimize(riskmodel.estimate(weights - base, **kwargs)),
191 constraints=[
192 cp.sum(weights) == 1.0,
193 weights >= 0,
194 *riskmodel.constraints(weights, **kwargs),
195 *constraints,
196 ],
197 )
199 return problem