Coverage for src / cvx / risk / bounds.py: 100%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-09 03:39 +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. 

15 

16This module provides the Bounds class for defining and enforcing lower and upper 

17bounds on portfolio weights or other variables in optimization problems. 

18 

19Example: 

20 Create bounds for a portfolio and use them as constraints: 

21 

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 

37 

38""" 

39 

40from __future__ import annotations 

41 

42from dataclasses import dataclass 

43from typing import Any 

44 

45import cvxpy as cp 

46import numpy as np 

47 

48from .model import Model 

49 

50 

51@dataclass 

52class Bounds(Model): 

53 """Representation of bounds for a model, defining constraints and parameters. 

54 

55 This dataclass provides functionality to establish and manage bounds for a model. 

56 It includes methods to handle bound parameters, update them dynamically, and 

57 generate constraints that can be used in optimization models. 

58 

59 The Bounds class creates CVXPY Parameter objects for lower and upper bounds, 

60 which can be updated without reconstructing the optimization problem. 

61 

62 Attributes: 

63 m: Maximum number of bounds (e.g., number of assets or factors). 

64 name: Name for the bounds used in parameter naming (e.g., "assets" or "factors"). 

65 

66 Example: 

67 Create and use bounds for portfolio weights: 

68 

69 >>> import cvxpy as cp 

70 >>> import numpy as np 

71 >>> from cvx.risk.bounds import Bounds 

72 >>> # Create bounds with capacity for 5 assets 

73 >>> bounds = Bounds(m=5, name="assets") 

74 >>> # Initialize with actual bounds (can be smaller than m) 

75 >>> bounds.update( 

76 ... lower_assets=np.array([0.0, 0.0, 0.1]), 

77 ... upper_assets=np.array([0.5, 0.5, 0.4]) 

78 ... ) 

79 >>> # Check parameter values 

80 >>> bounds.parameter["lower_assets"].value[:3] 

81 array([0. , 0. , 0.1]) 

82 >>> bounds.parameter["upper_assets"].value[:3] 

83 array([0.5, 0.5, 0.4]) 

84 

85 Bounds can be used with different variable types (factors, sectors, etc.): 

86 

87 >>> factor_bounds = Bounds(m=3, name="factors") 

88 >>> factor_bounds.update( 

89 ... lower_factors=np.array([-0.1, -0.2, -0.15]), 

90 ... upper_factors=np.array([0.1, 0.2, 0.15]) 

91 ... ) 

92 >>> # Factor exposure variable 

93 >>> y = cp.Variable(3) 

94 >>> factor_constraints = factor_bounds.constraints(y) 

95 >>> len(factor_constraints) 

96 2 

97 

98 Verify bounds are enforced correctly in optimization: 

99 

100 >>> weights = cp.Variable(5) 

101 >>> bounds.update( 

102 ... lower_assets=np.array([0.3, 0.0, 0.0, 0.0, 0.0]), 

103 ... upper_assets=np.array([0.5, 0.2, 0.2, 0.2, 0.2]) 

104 ... ) 

105 >>> prob = cp.Problem( 

106 ... cp.Minimize(weights[0]), # Minimize first weight 

107 ... bounds.constraints(weights) + [cp.sum(weights) == 1.0] 

108 ... ) 

109 >>> _ = prob.solve(solver="CLARABEL") 

110 >>> # First weight should be at lower bound (0.3) 

111 >>> bool(np.isclose(weights.value[0], 0.3, atol=1e-4)) 

112 True 

113 

114 """ 

115 

116 m: int = 0 

117 """Maximum number of bounds (e.g., number of assets).""" 

118 

119 name: str = "" 

120 """Name for the bounds, used in parameter naming (e.g., 'assets' or 'factors').""" 

121 

122 def estimate(self, weights: cp.Variable, **kwargs: Any) -> cp.Expression: 

123 """No estimation for bounds. 

124 

125 Bounds do not provide a risk estimate; they only provide constraints. 

126 This method raises NotImplementedError. 

127 

128 Args: 

129 weights: CVXPY variable representing portfolio weights. 

130 **kwargs: Additional keyword arguments. 

131 

132 Raises: 

133 NotImplementedError: Always raised as bounds do not provide risk estimates. 

134 

135 Example: 

136 >>> import cvxpy as cp 

137 >>> from cvx.risk.bounds import Bounds 

138 >>> bounds = Bounds(m=3, name="assets") 

139 >>> weights = cp.Variable(3) 

140 >>> try: 

141 ... bounds.estimate(weights) 

142 ... except NotImplementedError: 

143 ... print("estimate not implemented for Bounds") 

144 estimate not implemented for Bounds 

145 

146 """ 

147 raise NotImplementedError("No estimation for bounds") 

148 

149 def _f(self, str_prefix: str) -> str: 

150 """Create a parameter name by appending the name attribute. 

151 

152 This internal method creates consistent parameter names by combining 

153 a prefix with the bounds name (e.g., "lower_assets" or "upper_factors"). 

154 

155 Args: 

156 str_prefix: Base string for the parameter name (e.g., "lower" or "upper"). 

157 

158 Returns: 

159 Combined parameter name in the format "{str_prefix}_{self.name}". 

160 

161 Example: 

162 >>> from cvx.risk.bounds import Bounds 

163 >>> bounds = Bounds(m=3, name="assets") 

164 >>> bounds._f("lower") 

165 'lower_assets' 

166 >>> bounds._f("upper") 

167 'upper_assets' 

168 

169 """ 

170 return f"{str_prefix}_{self.name}" 

171 

172 def __post_init__(self) -> None: 

173 """Initialize the parameters after the class is instantiated. 

174 

175 Creates lower and upper bound CVXPY Parameter objects with appropriate 

176 shapes and default values. Lower bounds default to zeros, upper bounds 

177 default to ones. 

178 

179 Example: 

180 >>> from cvx.risk.bounds import Bounds 

181 >>> bounds = Bounds(m=3, name="assets") 

182 >>> # Parameters are automatically created 

183 >>> bounds.parameter["lower_assets"].shape 

184 (3,) 

185 >>> bounds.parameter["upper_assets"].shape 

186 (3,) 

187 

188 """ 

189 self.parameter[self._f("lower")] = cp.Parameter( 

190 shape=self.m, 

191 name="lower bound", 

192 value=np.zeros(self.m), 

193 ) 

194 self.parameter[self._f("upper")] = cp.Parameter( 

195 shape=self.m, 

196 name="upper bound", 

197 value=np.ones(self.m), 

198 ) 

199 

200 def update(self, **kwargs: Any) -> None: 

201 """Update the lower and upper bound parameters. 

202 

203 This method updates the bound parameters with new values. The input 

204 arrays can be shorter than m, in which case remaining values are set 

205 to zero. 

206 

207 Args: 

208 **kwargs: Keyword arguments containing lower and upper bounds 

209 with keys formatted as "{lower/upper}_{self.name}". 

210 

211 Example: 

212 >>> import numpy as np 

213 >>> from cvx.risk.bounds import Bounds 

214 >>> bounds = Bounds(m=5, name="assets") 

215 >>> # Update with bounds for only 3 assets 

216 >>> bounds.update( 

217 ... lower_assets=np.array([0.0, 0.1, 0.2]), 

218 ... upper_assets=np.array([0.5, 0.4, 0.3]) 

219 ... ) 

220 >>> bounds.parameter["lower_assets"].value[:3] 

221 array([0. , 0.1, 0.2]) 

222 

223 """ 

224 lower = kwargs[self._f("lower")] 

225 lower_arr = np.zeros(self.m) 

226 lower_arr[: len(lower)] = lower 

227 self.parameter[self._f("lower")].value = lower_arr 

228 

229 upper = kwargs[self._f("upper")] 

230 upper_arr = np.zeros(self.m) 

231 upper_arr[: len(upper)] = upper 

232 self.parameter[self._f("upper")].value = upper_arr 

233 

234 def constraints(self, weights: cp.Variable, **kwargs: Any) -> list[cp.Constraint]: 

235 """Return constraints that enforce the bounds on weights. 

236 

237 Creates CVXPY constraints that enforce the lower and upper bounds 

238 on the weights variable. 

239 

240 Args: 

241 weights: CVXPY variable representing portfolio weights. 

242 **kwargs: Additional keyword arguments (not used). 

243 

244 Returns: 

245 List of two CVXPY constraints: lower bound and upper bound. 

246 

247 Example: 

248 >>> import cvxpy as cp 

249 >>> import numpy as np 

250 >>> from cvx.risk.bounds import Bounds 

251 >>> bounds = Bounds(m=2, name="assets") 

252 >>> bounds.update( 

253 ... lower_assets=np.array([0.1, 0.2]), 

254 ... upper_assets=np.array([0.6, 0.7]) 

255 ... ) 

256 >>> weights = cp.Variable(2) 

257 >>> constraints = bounds.constraints(weights) 

258 >>> len(constraints) 

259 2 

260 

261 """ 

262 return [ 

263 weights >= self.parameter[self._f("lower")], 

264 weights <= self.parameter[self._f("upper")], 

265 ]