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

23 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"""Risk models based on the sample covariance matrix. 

15 

16This module provides the SampleCovariance class, which implements a risk model 

17based on the Cholesky decomposition of the sample covariance matrix. This is 

18one of the most common approaches to portfolio risk estimation. 

19 

20Example: 

21 Create and use a sample covariance risk model: 

22 

23 >>> import cvxpy as cp 

24 >>> import numpy as np 

25 >>> from cvx.risk.sample import SampleCovariance 

26 >>> # Create risk model for up to 3 assets 

27 >>> model = SampleCovariance(num=3) 

28 >>> # Update with a covariance matrix 

29 >>> cov = np.array([[1.0, 0.5, 0.0], [0.5, 1.0, 0.5], [0.0, 0.5, 1.0]]) 

30 >>> model.update( 

31 ... cov=cov, 

32 ... lower_assets=np.zeros(3), 

33 ... upper_assets=np.ones(3) 

34 ... ) 

35 >>> # Estimate risk for a given portfolio 

36 >>> weights = np.array([0.4, 0.3, 0.3]) 

37 >>> risk = model.estimate(weights).value 

38 >>> isinstance(risk, float) 

39 True 

40 

41""" 

42 

43from __future__ import annotations 

44 

45from dataclasses import dataclass 

46 

47import cvxpy as cvx 

48import numpy as np 

49 

50from cvx.risk.bounds import Bounds 

51from cvx.risk.linalg import cholesky 

52from cvx.risk.model import Model 

53 

54 

55@dataclass 

56class SampleCovariance(Model): 

57 """Risk model based on the Cholesky decomposition of the sample covariance matrix. 

58 

59 This model computes portfolio risk as the L2 norm of the product of the 

60 Cholesky factor and the weights vector. Mathematically, if R is the upper 

61 triangular Cholesky factor of the covariance matrix (R^T @ R = cov), then: 

62 

63 risk = ||R @ w||_2 = sqrt(w^T @ cov @ w) 

64 

65 This represents the portfolio standard deviation (volatility). 

66 

67 Attributes: 

68 num: Maximum number of assets the model can handle. The model can be 

69 updated with fewer assets, but not more. 

70 

71 Example: 

72 Basic usage: 

73 

74 >>> import cvxpy as cp 

75 >>> import numpy as np 

76 >>> from cvx.risk.sample import SampleCovariance 

77 >>> model = SampleCovariance(num=2) 

78 >>> model.update( 

79 ... cov=np.array([[1.0, 0.5], [0.5, 2.0]]), 

80 ... lower_assets=np.zeros(2), 

81 ... upper_assets=np.ones(2) 

82 ... ) 

83 >>> # Equal weight portfolio 

84 >>> weights = np.array([0.5, 0.5]) 

85 >>> risk = model.estimate(weights).value 

86 >>> # Risk should be sqrt(0.5^2 * 1 + 0.5^2 * 2 + 2 * 0.5 * 0.5 * 0.5) 

87 >>> bool(np.isclose(risk, 1.0)) 

88 True 

89 

90 Using in optimization: 

91 

92 >>> from cvx.risk.portfolio import minrisk_problem 

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

94 >>> problem = minrisk_problem(model, weights) 

95 >>> _ = problem.solve(solver="CLARABEL") 

96 >>> # Lower variance asset gets higher weight 

97 >>> bool(weights.value[0] > weights.value[1]) 

98 True 

99 

100 """ 

101 

102 num: int = 0 

103 """Maximum number of assets the model can handle.""" 

104 

105 def __post_init__(self): 

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

107 

108 Creates the Cholesky decomposition parameter and initializes the bounds. 

109 The Cholesky parameter is a square matrix of size (num, num), and bounds 

110 are created for asset weights. 

111 

112 Example: 

113 >>> from cvx.risk.sample import SampleCovariance 

114 >>> model = SampleCovariance(num=5) 

115 >>> # Parameters are automatically created 

116 >>> model.parameter["chol"].shape 

117 (5, 5) 

118 

119 """ 

120 self.parameter["chol"] = cvx.Parameter( 

121 shape=(self.num, self.num), 

122 name="cholesky of covariance", 

123 value=np.zeros((self.num, self.num)), 

124 ) 

125 self.bounds = Bounds(m=self.num, name="assets") 

126 

127 def estimate(self, weights: cvx.Variable, **kwargs) -> cvx.Expression: 

128 """Estimate the portfolio risk using the Cholesky decomposition. 

129 

130 Computes the L2 norm of the product of the Cholesky factor and the 

131 weights vector. This is equivalent to the square root of the portfolio 

132 variance (i.e., portfolio volatility). 

133 

134 Args: 

135 weights: CVXPY variable or numpy array representing portfolio weights. 

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

137 

138 Returns: 

139 CVXPY expression representing the portfolio risk (standard deviation). 

140 

141 Example: 

142 >>> import cvxpy as cp 

143 >>> import numpy as np 

144 >>> from cvx.risk.sample import SampleCovariance 

145 >>> model = SampleCovariance(num=2) 

146 >>> # Identity covariance (uncorrelated assets with unit variance) 

147 >>> model.update( 

148 ... cov=np.eye(2), 

149 ... lower_assets=np.zeros(2), 

150 ... upper_assets=np.ones(2) 

151 ... ) 

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

153 >>> risk = model.estimate(weights) 

154 >>> isinstance(risk, cp.Expression) 

155 True 

156 

157 """ 

158 return cvx.norm2(self.parameter["chol"] @ weights) 

159 

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

161 """Update the Cholesky decomposition parameter and bounds. 

162 

163 Computes the Cholesky decomposition of the provided covariance matrix 

164 and updates the model parameters. The covariance matrix can be smaller 

165 than num x num. 

166 

167 Args: 

168 **kwargs: Keyword arguments containing: 

169 - cov: Covariance matrix (numpy.ndarray). Must be positive definite. 

170 - lower_assets: Array of lower bounds for asset weights. 

171 - upper_assets: Array of upper bounds for asset weights. 

172 

173 Example: 

174 >>> import numpy as np 

175 >>> from cvx.risk.sample import SampleCovariance 

176 >>> model = SampleCovariance(num=5) 

177 >>> # Update with a 3x3 covariance (smaller than max) 

178 >>> cov = np.array([[1.0, 0.3, 0.1], 

179 ... [0.3, 1.0, 0.2], 

180 ... [0.1, 0.2, 1.0]]) 

181 >>> model.update( 

182 ... cov=cov, 

183 ... lower_assets=np.zeros(3), 

184 ... upper_assets=np.ones(3) 

185 ... ) 

186 >>> # Cholesky factor is updated 

187 >>> model.parameter["chol"].value[:3, :3].shape 

188 (3, 3) 

189 

190 """ 

191 cov = kwargs["cov"] 

192 n = cov.shape[0] 

193 

194 self.parameter["chol"].value[:n, :n] = cholesky(cov) 

195 self.bounds.update(**kwargs) 

196 

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

198 """Return constraints for the sample covariance model. 

199 

200 Returns the asset bounds constraints from the internal bounds object. 

201 

202 Args: 

203 weights: CVXPY variable representing portfolio weights. 

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

205 

206 Returns: 

207 List of CVXPY constraints from the bounds object (lower and upper bounds). 

208 

209 Example: 

210 >>> import cvxpy as cp 

211 >>> import numpy as np 

212 >>> from cvx.risk.sample import SampleCovariance 

213 >>> model = SampleCovariance(num=3) 

214 >>> model.update( 

215 ... cov=np.eye(3), 

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

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

218 ... ) 

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

220 >>> constraints = model.constraints(weights) 

221 >>> len(constraints) 

222 2 

223 

224 """ 

225 return self.bounds.constraints(weights)