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

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 

43 

44import cvxpy as cp 

45import numpy as np 

46 

47from .model import Model 

48 

49 

50@dataclass 

51class Bounds(Model): 

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

53 

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. 

57 

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

59 which can be updated without reconstructing the optimization problem. 

60 

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"). 

64 

65 Example: 

66 Create and use bounds for portfolio weights: 

67 

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]) 

83 

84 """ 

85 

86 m: int = 0 

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

88 

89 name: str = "" 

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

91 

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

93 """No estimation for bounds. 

94 

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

96 This method raises NotImplementedError. 

97 

98 Args: 

99 weights: CVXPY variable representing portfolio weights. 

100 **kwargs: Additional keyword arguments. 

101 

102 Raises: 

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

104 

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 

115 

116 """ 

117 raise NotImplementedError("No estimation for bounds") 

118 

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

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

121 

122 This internal method creates consistent parameter names by combining 

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

124 

125 Args: 

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

127 

128 Returns: 

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

130 

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' 

138 

139 """ 

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

141 

142 def __post_init__(self): 

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

144 

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. 

148 

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,) 

157 

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 ) 

169 

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

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

172 

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. 

176 

177 Args: 

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

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

180 

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]) 

192 

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 

197 

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 

201 

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

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

204 

205 Creates CVXPY constraints that enforce the lower and upper bounds 

206 on the weights variable. 

207 

208 Args: 

209 weights: CVXPY variable representing portfolio weights. 

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

211 

212 Returns: 

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

214 

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 

228 

229 """ 

230 return [ 

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

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

233 ]