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

27 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-15 12:21 +0000

1"""Conditional Value at Risk (CVaR) risk model implementation. 

2 

3This module provides the CVar class, which implements the Conditional Value at Risk 

4(also known as Expected Shortfall) risk measure for portfolio optimization. 

5 

6CVaR measures the expected loss in the tail of the portfolio's return distribution, 

7making it a popular choice for risk-averse portfolio optimization. 

8 

9Example: 

10 Create a CVaR model and compute the tail risk: 

11 

12 >>> import cvxpy as cp 

13 >>> import numpy as np 

14 >>> from cvx.risk.cvar import CVar 

15 >>> # Create CVaR model with 95% confidence level 

16 >>> model = CVar(alpha=0.95, n=100, m=5) 

17 >>> # Generate sample returns 

18 >>> np.random.seed(42) 

19 >>> returns = np.random.randn(100, 5) 

20 >>> # Update model with returns data 

21 >>> model.update( 

22 ... returns=returns, 

23 ... lower_assets=np.zeros(5), 

24 ... upper_assets=np.ones(5) 

25 ... ) 

26 >>> # The model is ready for optimization 

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

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

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

30 True 

31 

32""" 

33 

34# Copyright 2023 Stanford University Convex Optimization Group 

35# 

36# Licensed under the Apache License, Version 2.0 (the "License"); 

37# you may not use this file except in compliance with the License. 

38# You may obtain a copy of the License at 

39# 

40# http://www.apache.org/licenses/LICENSE-2.0 

41# 

42# Unless required by applicable law or agreed to in writing, software 

43# distributed under the License is distributed on an "AS IS" BASIS, 

44# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

45# See the License for the specific language governing permissions and 

46# limitations under the License. 

47from __future__ import annotations 

48 

49from dataclasses import dataclass 

50 

51import cvxpy as cvx 

52import numpy as np 

53 

54from cvx.risk.bounds import Bounds 

55from cvx.risk.model import Model 

56 

57 

58@dataclass 

59class CVar(Model): 

60 """Conditional Value at Risk (CVaR) risk model. 

61 

62 CVaR, also known as Expected Shortfall, measures the expected loss in the 

63 worst (1-alpha) fraction of scenarios. For example, with alpha=0.95, CVaR 

64 is the average of the worst 5% of returns. 

65 

66 This implementation uses historical returns to estimate CVaR, which is 

67 computed as the negative average of the k smallest portfolio returns, 

68 where k = n * (1 - alpha). 

69 

70 Attributes: 

71 alpha: Confidence level, typically 0.95 or 0.99. Higher alpha means 

72 focusing on more extreme tail events. 

73 n: Number of historical return observations (scenarios). 

74 m: Maximum number of assets in the portfolio. 

75 

76 Example: 

77 Basic CVaR model setup and optimization: 

78 

79 >>> import cvxpy as cp 

80 >>> import numpy as np 

81 >>> from cvx.risk.cvar import CVar 

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

83 >>> # Create model for 95% CVaR with 50 scenarios and 3 assets 

84 >>> model = CVar(alpha=0.95, n=50, m=3) 

85 >>> # Number of tail samples: k = 50 * (1 - 0.95) = 2.5 -> 2 

86 >>> model.k 

87 2 

88 >>> # Generate sample returns 

89 >>> np.random.seed(42) 

90 >>> returns = np.random.randn(50, 3) 

91 >>> model.update( 

92 ... returns=returns, 

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

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

95 ... ) 

96 >>> # Create and solve optimization 

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

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

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

100 

101 """ 

102 

103 alpha: float = 0.95 

104 """Confidence level for CVaR (e.g., 0.95 for 95% CVaR).""" 

105 

106 n: int = 0 

107 """Number of historical return observations (scenarios).""" 

108 

109 m: int = 0 

110 """Maximum number of assets in the portfolio.""" 

111 

112 def __post_init__(self): 

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

114 

115 Calculates the number of samples in the tail (k) based on alpha, 

116 creates the returns parameter matrix, and initializes the bounds. 

117 

118 Example: 

119 >>> from cvx.risk.cvar import CVar 

120 >>> model = CVar(alpha=0.95, n=100, m=5) 

121 >>> # k is the number of samples in the tail 

122 >>> model.k 

123 5 

124 >>> # Returns parameter is created 

125 >>> model.parameter["R"].shape 

126 (100, 5) 

127 

128 """ 

129 self.k = int(self.n * (1 - self.alpha)) 

130 self.parameter["R"] = cvx.Parameter(shape=(self.n, self.m), name="returns", value=np.zeros((self.n, self.m))) 

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

132 

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

134 """Estimate the Conditional Value at Risk (CVaR) for the given weights. 

135 

136 Computes the negative average of the k smallest returns in the portfolio, 

137 where k is determined by the alpha parameter. This represents the expected 

138 loss in the worst (1-alpha) fraction of scenarios. 

139 

140 Args: 

141 weights: CVXPY variable representing portfolio weights. 

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

143 

144 Returns: 

145 CVXPY expression representing the CVaR (expected tail loss). 

146 

147 Example: 

148 >>> import cvxpy as cp 

149 >>> import numpy as np 

150 >>> from cvx.risk.cvar import CVar 

151 >>> model = CVar(alpha=0.95, n=100, m=3) 

152 >>> np.random.seed(42) 

153 >>> returns = np.random.randn(100, 3) 

154 >>> model.update( 

155 ... returns=returns, 

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

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

158 ... ) 

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

160 >>> cvar = model.estimate(weights) 

161 >>> isinstance(cvar, cp.Expression) 

162 True 

163 

164 """ 

165 # R is a matrix of returns, n is the number of rows in R 

166 # k is the number of returns in the left tail 

167 # average value of the k elements in the left tail 

168 return -cvx.sum_smallest(self.parameter["R"] @ weights, k=self.k) / self.k 

169 

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

171 """Update the returns data and bounds parameters. 

172 

173 Updates the returns matrix and asset bounds. The returns matrix can 

174 have fewer columns than m (maximum assets), in which case only the 

175 first columns are updated. 

176 

177 Args: 

178 **kwargs: Keyword arguments containing: 

179 - returns: Matrix of returns with shape (n, num_assets). 

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

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

182 

183 Example: 

184 >>> import numpy as np 

185 >>> from cvx.risk.cvar import CVar 

186 >>> model = CVar(alpha=0.95, n=50, m=5) 

187 >>> # Update with 3 assets (less than maximum of 5) 

188 >>> np.random.seed(42) 

189 >>> returns = np.random.randn(50, 3) 

190 >>> model.update( 

191 ... returns=returns, 

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

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

194 ... ) 

195 >>> model.parameter["R"].value[:, :3].shape 

196 (50, 3) 

197 

198 """ 

199 ret = kwargs["returns"] 

200 m = ret.shape[1] 

201 

202 self.parameter["R"].value[:, :m] = kwargs["returns"] 

203 self.bounds.update(**kwargs) 

204 

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

206 """Return constraints for the CVaR model. 

207 

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

209 

210 Args: 

211 weights: CVXPY variable representing portfolio weights. 

212 **kwargs: Additional keyword arguments passed to bounds.constraints(). 

213 

214 Returns: 

215 List of CVXPY constraints from the bounds object. 

216 

217 Example: 

218 >>> import cvxpy as cp 

219 >>> import numpy as np 

220 >>> from cvx.risk.cvar import CVar 

221 >>> model = CVar(alpha=0.95, n=50, m=3) 

222 >>> np.random.seed(42) 

223 >>> returns = np.random.randn(50, 3) 

224 >>> model.update( 

225 ... returns=returns, 

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

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

228 ... ) 

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

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

231 >>> len(constraints) 

232 2 

233 

234 """ 

235 return self.bounds.constraints(weights)