Coverage for src / cvx / risk / cvar / cvar.py: 100%
30 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 03:39 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-09 03:39 +0000
1"""Conditional Value at Risk (CVaR) risk model implementation.
3This module provides the CVar class, which implements the Conditional Value at Risk
4(also known as Expected Shortfall) risk measure for portfolio optimization.
6CVaR measures the expected loss in the tail of the portfolio's return distribution,
7making it a popular choice for risk-averse portfolio optimization.
9Example:
10 Create a CVaR model and compute the tail risk:
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
32"""
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
49from dataclasses import dataclass
50from typing import Any, cast
52import cvxpy as cvx
53import numpy as np
55from cvx.risk.bounds import Bounds
56from cvx.risk.model import Model
59@dataclass
60class CVar(Model):
61 """Conditional Value at Risk (CVaR) risk model.
63 CVaR, also known as Expected Shortfall, measures the expected loss in the
64 worst (1-alpha) fraction of scenarios. For example, with alpha=0.95, CVaR
65 is the average of the worst 5% of returns.
67 This implementation uses historical returns to estimate CVaR, which is
68 computed as the negative average of the k smallest portfolio returns,
69 where k = n * (1 - alpha).
71 Attributes:
72 alpha: Confidence level, typically 0.95 or 0.99. Higher alpha means
73 focusing on more extreme tail events.
74 n: Number of historical return observations (scenarios).
75 m: Maximum number of assets in the portfolio.
77 Example:
78 Basic CVaR model setup and optimization:
80 >>> import cvxpy as cp
81 >>> import numpy as np
82 >>> from cvx.risk.cvar import CVar
83 >>> from cvx.risk.portfolio import minrisk_problem
84 >>> # Create model for 95% CVaR with 50 scenarios and 3 assets
85 >>> model = CVar(alpha=0.95, n=50, m=3)
86 >>> # Number of tail samples: k = 50 * (1 - 0.95) = 2.5 -> 2
87 >>> model.k
88 2
89 >>> # Generate sample returns
90 >>> np.random.seed(42)
91 >>> returns = np.random.randn(50, 3)
92 >>> model.update(
93 ... returns=returns,
94 ... lower_assets=np.zeros(3),
95 ... upper_assets=np.ones(3)
96 ... )
97 >>> # Create and solve optimization
98 >>> weights = cp.Variable(3)
99 >>> problem = minrisk_problem(model, weights)
100 >>> _ = problem.solve(solver="CLARABEL")
102 Mathematical verification of CVaR calculation:
104 >>> model = CVar(alpha=0.95, n=20, m=2)
105 >>> # Simple returns: asset 1 always returns 0.05, asset 2 returns vary
106 >>> returns = np.zeros((20, 2))
107 >>> returns[:, 0] = 0.05 # Asset 1 constant return
108 >>> returns[:, 1] = np.linspace(-0.20, 0.18, 20) # Asset 2 varying
109 >>> model.update(
110 ... returns=returns,
111 ... lower_assets=np.zeros(2),
112 ... upper_assets=np.ones(2)
113 ... )
114 >>> # k = 20 * (1 - 0.95) = 1, so we take the single worst return
115 >>> model.k
116 1
117 >>> # For 100% in asset 2, worst return is -0.20
118 >>> w = np.array([0.0, 1.0])
119 >>> cvar = model.estimate(w).value
120 >>> expected_cvar = 0.20 # negative of worst return
121 >>> bool(np.isclose(cvar, expected_cvar, rtol=1e-6))
122 True
124 Different alpha values affect the tail focus:
126 >>> # Higher alpha = focus on more extreme events
127 >>> model_95 = CVar(alpha=0.95, n=100, m=2)
128 >>> model_95.k # Only 5 worst scenarios
129 5
130 >>> model_75 = CVar(alpha=0.75, n=100, m=2)
131 >>> model_75.k # 25 worst scenarios
132 25
134 """
136 alpha: float = 0.95
137 """Confidence level for CVaR (e.g., 0.95 for 95% CVaR)."""
139 n: int = 0
140 """Number of historical return observations (scenarios)."""
142 m: int = 0
143 """Maximum number of assets in the portfolio."""
145 def __post_init__(self) -> None:
146 """Initialize the parameters after the class is instantiated.
148 Calculates the number of samples in the tail (k) based on alpha,
149 creates the returns parameter matrix, and initializes the bounds.
151 Example:
152 >>> from cvx.risk.cvar import CVar
153 >>> model = CVar(alpha=0.95, n=100, m=5)
154 >>> # k is the number of samples in the tail
155 >>> model.k
156 5
157 >>> # Returns parameter is created
158 >>> model.parameter["R"].shape
159 (100, 5)
161 """
162 self.k = int(self.n * (1 - self.alpha))
163 self.parameter["R"] = cvx.Parameter(shape=(self.n, self.m), name="returns", value=np.zeros((self.n, self.m)))
164 self.bounds = Bounds(m=self.m, name="assets")
166 def estimate(self, weights: cvx.Variable, **kwargs: Any) -> cvx.Expression:
167 """Estimate the Conditional Value at Risk (CVaR) for the given weights.
169 Computes the negative average of the k smallest returns in the portfolio,
170 where k is determined by the alpha parameter. This represents the expected
171 loss in the worst (1-alpha) fraction of scenarios.
173 Args:
174 weights: CVXPY variable representing portfolio weights.
175 **kwargs: Additional keyword arguments (not used).
177 Returns:
178 CVXPY expression representing the CVaR (expected tail loss).
180 Example:
181 >>> import cvxpy as cp
182 >>> import numpy as np
183 >>> from cvx.risk.cvar import CVar
184 >>> model = CVar(alpha=0.95, n=100, m=3)
185 >>> np.random.seed(42)
186 >>> returns = np.random.randn(100, 3)
187 >>> model.update(
188 ... returns=returns,
189 ... lower_assets=np.zeros(3),
190 ... upper_assets=np.ones(3)
191 ... )
192 >>> weights = cp.Variable(3)
193 >>> cvar = model.estimate(weights)
194 >>> isinstance(cvar, cp.Expression)
195 True
197 """
198 # R is a matrix of returns, n is the number of rows in R
199 # k is the number of returns in the left tail
200 # average value of the k elements in the left tail
201 return cast(
202 cvx.Expression,
203 -cvx.sum_smallest(self.parameter["R"] @ weights, k=self.k) / self.k,
204 )
206 def update(self, **kwargs: Any) -> None:
207 """Update the returns data and bounds parameters.
209 Updates the returns matrix and asset bounds. The returns matrix can
210 have fewer columns than m (maximum assets), in which case only the
211 first columns are updated.
213 Args:
214 **kwargs: Keyword arguments containing:
215 - returns: Matrix of returns with shape (n, num_assets).
216 - lower_assets: Array of lower bounds for asset weights.
217 - upper_assets: Array of upper bounds for asset weights.
219 Example:
220 >>> import numpy as np
221 >>> from cvx.risk.cvar import CVar
222 >>> model = CVar(alpha=0.95, n=50, m=5)
223 >>> # Update with 3 assets (less than maximum of 5)
224 >>> np.random.seed(42)
225 >>> returns = np.random.randn(50, 3)
226 >>> model.update(
227 ... returns=returns,
228 ... lower_assets=np.zeros(3),
229 ... upper_assets=np.ones(3)
230 ... )
231 >>> model.parameter["R"].value[:, :3].shape
232 (50, 3)
234 """
235 ret = kwargs["returns"]
236 num_assets = ret.shape[1]
238 returns_arr = np.zeros((self.n, self.m))
239 returns_arr[:, :num_assets] = ret
240 self.parameter["R"].value = returns_arr
241 self.bounds.update(**kwargs)
243 def constraints(self, weights: cvx.Variable, **kwargs: Any) -> list[cvx.Constraint]:
244 """Return constraints for the CVaR model.
246 Returns the asset bounds constraints from the internal bounds object.
248 Args:
249 weights: CVXPY variable representing portfolio weights.
250 **kwargs: Additional keyword arguments passed to bounds.constraints().
252 Returns:
253 List of CVXPY constraints from the bounds object.
255 Example:
256 >>> import cvxpy as cp
257 >>> import numpy as np
258 >>> from cvx.risk.cvar import CVar
259 >>> model = CVar(alpha=0.95, n=50, m=3)
260 >>> np.random.seed(42)
261 >>> returns = np.random.randn(50, 3)
262 >>> model.update(
263 ... returns=returns,
264 ... lower_assets=np.zeros(3),
265 ... upper_assets=np.ones(3)
266 ... )
267 >>> weights = cp.Variable(3)
268 >>> constraints = model.constraints(weights)
269 >>> len(constraints)
270 2
272 """
273 return self.bounds.constraints(weights)