Coverage for src / cvx / risk / bounds.py: 100%
27 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 12:21 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-15 12:21 +0000
1# Copyright 2023 Stanford University Convex Optimization Group
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Bounds for portfolio optimization.
16This module provides the Bounds class for defining and enforcing lower and upper
17bounds on portfolio weights or other variables in optimization problems.
19Example:
20 Create bounds for a portfolio and use them as constraints:
22 >>> import cvxpy as cp
23 >>> import numpy as np
24 >>> from cvx.risk.bounds import Bounds
25 >>> # Create bounds for 3 assets
26 >>> bounds = Bounds(m=3, name="assets")
27 >>> # Update bounds with actual values
28 >>> bounds.update(
29 ... lower_assets=np.array([0.0, 0.1, 0.0]),
30 ... upper_assets=np.array([0.5, 0.4, 0.3])
31 ... )
32 >>> # Create constraints
33 >>> weights = cp.Variable(3)
34 >>> constraints = bounds.constraints(weights)
35 >>> len(constraints)
36 2
38"""
40from __future__ import annotations
42from dataclasses import dataclass
44import cvxpy as cp
45import numpy as np
47from .model import Model
50@dataclass
51class Bounds(Model):
52 """Representation of bounds for a model, defining constraints and parameters.
54 This dataclass provides functionality to establish and manage bounds for a model.
55 It includes methods to handle bound parameters, update them dynamically, and
56 generate constraints that can be used in optimization models.
58 The Bounds class creates CVXPY Parameter objects for lower and upper bounds,
59 which can be updated without reconstructing the optimization problem.
61 Attributes:
62 m: Maximum number of bounds (e.g., number of assets or factors).
63 name: Name for the bounds used in parameter naming (e.g., "assets" or "factors").
65 Example:
66 Create and use bounds for portfolio weights:
68 >>> import cvxpy as cp
69 >>> import numpy as np
70 >>> from cvx.risk.bounds import Bounds
71 >>> # Create bounds with capacity for 5 assets
72 >>> bounds = Bounds(m=5, name="assets")
73 >>> # Initialize with actual bounds (can be smaller than m)
74 >>> bounds.update(
75 ... lower_assets=np.array([0.0, 0.0, 0.1]),
76 ... upper_assets=np.array([0.5, 0.5, 0.4])
77 ... )
78 >>> # Check parameter values
79 >>> bounds.parameter["lower_assets"].value[:3]
80 array([0. , 0. , 0.1])
81 >>> bounds.parameter["upper_assets"].value[:3]
82 array([0.5, 0.5, 0.4])
84 """
86 m: int = 0
87 """Maximum number of bounds (e.g., number of assets)."""
89 name: str = ""
90 """Name for the bounds, used in parameter naming (e.g., 'assets' or 'factors')."""
92 def estimate(self, weights: cp.Variable, **kwargs) -> cp.Expression:
93 """No estimation for bounds.
95 Bounds do not provide a risk estimate; they only provide constraints.
96 This method raises NotImplementedError.
98 Args:
99 weights: CVXPY variable representing portfolio weights.
100 **kwargs: Additional keyword arguments.
102 Raises:
103 NotImplementedError: Always raised as bounds do not provide risk estimates.
105 Example:
106 >>> import cvxpy as cp
107 >>> from cvx.risk.bounds import Bounds
108 >>> bounds = Bounds(m=3, name="assets")
109 >>> weights = cp.Variable(3)
110 >>> try:
111 ... bounds.estimate(weights)
112 ... except NotImplementedError:
113 ... print("estimate not implemented for Bounds")
114 estimate not implemented for Bounds
116 """
117 raise NotImplementedError("No estimation for bounds")
119 def _f(self, str_prefix: str) -> str:
120 """Create a parameter name by appending the name attribute.
122 This internal method creates consistent parameter names by combining
123 a prefix with the bounds name (e.g., "lower_assets" or "upper_factors").
125 Args:
126 str_prefix: Base string for the parameter name (e.g., "lower" or "upper").
128 Returns:
129 Combined parameter name in the format "{str_prefix}_{self.name}".
131 Example:
132 >>> from cvx.risk.bounds import Bounds
133 >>> bounds = Bounds(m=3, name="assets")
134 >>> bounds._f("lower")
135 'lower_assets'
136 >>> bounds._f("upper")
137 'upper_assets'
139 """
140 return f"{str_prefix}_{self.name}"
142 def __post_init__(self):
143 """Initialize the parameters after the class is instantiated.
145 Creates lower and upper bound CVXPY Parameter objects with appropriate
146 shapes and default values. Lower bounds default to zeros, upper bounds
147 default to ones.
149 Example:
150 >>> from cvx.risk.bounds import Bounds
151 >>> bounds = Bounds(m=3, name="assets")
152 >>> # Parameters are automatically created
153 >>> bounds.parameter["lower_assets"].shape
154 (3,)
155 >>> bounds.parameter["upper_assets"].shape
156 (3,)
158 """
159 self.parameter[self._f("lower")] = cp.Parameter(
160 shape=self.m,
161 name="lower bound",
162 value=np.zeros(self.m),
163 )
164 self.parameter[self._f("upper")] = cp.Parameter(
165 shape=self.m,
166 name="upper bound",
167 value=np.ones(self.m),
168 )
170 def update(self, **kwargs) -> None:
171 """Update the lower and upper bound parameters.
173 This method updates the bound parameters with new values. The input
174 arrays can be shorter than m, in which case remaining values are set
175 to zero.
177 Args:
178 **kwargs: Keyword arguments containing lower and upper bounds
179 with keys formatted as "{lower/upper}_{self.name}".
181 Example:
182 >>> import numpy as np
183 >>> from cvx.risk.bounds import Bounds
184 >>> bounds = Bounds(m=5, name="assets")
185 >>> # Update with bounds for only 3 assets
186 >>> bounds.update(
187 ... lower_assets=np.array([0.0, 0.1, 0.2]),
188 ... upper_assets=np.array([0.5, 0.4, 0.3])
189 ... )
190 >>> bounds.parameter["lower_assets"].value[:3]
191 array([0. , 0.1, 0.2])
193 """
194 lower = kwargs[self._f("lower")]
195 self.parameter[self._f("lower")].value = np.zeros(self.m)
196 self.parameter[self._f("lower")].value[: len(lower)] = lower
198 upper = kwargs[self._f("upper")]
199 self.parameter[self._f("upper")].value = np.zeros(self.m)
200 self.parameter[self._f("upper")].value[: len(upper)] = upper
202 def constraints(self, weights: cp.Variable, **kwargs) -> list[cp.Constraint]:
203 """Return constraints that enforce the bounds on weights.
205 Creates CVXPY constraints that enforce the lower and upper bounds
206 on the weights variable.
208 Args:
209 weights: CVXPY variable representing portfolio weights.
210 **kwargs: Additional keyword arguments (not used).
212 Returns:
213 List of two CVXPY constraints: lower bound and upper bound.
215 Example:
216 >>> import cvxpy as cp
217 >>> import numpy as np
218 >>> from cvx.risk.bounds import Bounds
219 >>> bounds = Bounds(m=2, name="assets")
220 >>> bounds.update(
221 ... lower_assets=np.array([0.1, 0.2]),
222 ... upper_assets=np.array([0.6, 0.7])
223 ... )
224 >>> weights = cp.Variable(2)
225 >>> constraints = bounds.constraints(weights)
226 >>> len(constraints)
227 2
229 """
230 return [
231 weights >= self.parameter[self._f("lower")],
232 weights <= self.parameter[self._f("upper")],
233 ]