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
« 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.
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
51import cvxpy as cvx
52import numpy as np
54from cvx.risk.bounds import Bounds
55from cvx.risk.model import Model
58@dataclass
59class CVar(Model):
60 """Conditional Value at Risk (CVaR) risk model.
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.
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).
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.
76 Example:
77 Basic CVaR model setup and optimization:
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")
101 """
103 alpha: float = 0.95
104 """Confidence level for CVaR (e.g., 0.95 for 95% CVaR)."""
106 n: int = 0
107 """Number of historical return observations (scenarios)."""
109 m: int = 0
110 """Maximum number of assets in the portfolio."""
112 def __post_init__(self):
113 """Initialize the parameters after the class is instantiated.
115 Calculates the number of samples in the tail (k) based on alpha,
116 creates the returns parameter matrix, and initializes the bounds.
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)
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")
133 def estimate(self, weights: cvx.Variable, **kwargs) -> cvx.Expression:
134 """Estimate the Conditional Value at Risk (CVaR) for the given weights.
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.
140 Args:
141 weights: CVXPY variable representing portfolio weights.
142 **kwargs: Additional keyword arguments (not used).
144 Returns:
145 CVXPY expression representing the CVaR (expected tail loss).
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
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
170 def update(self, **kwargs) -> None:
171 """Update the returns data and bounds parameters.
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.
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.
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)
198 """
199 ret = kwargs["returns"]
200 m = ret.shape[1]
202 self.parameter["R"].value[:, :m] = kwargs["returns"]
203 self.bounds.update(**kwargs)
205 def constraints(self, weights: cvx.Variable, **kwargs) -> list[cvx.Constraint]:
206 """Return constraints for the CVaR model.
208 Returns the asset bounds constraints from the internal bounds object.
210 Args:
211 weights: CVXPY variable representing portfolio weights.
212 **kwargs: Additional keyword arguments passed to bounds.constraints().
214 Returns:
215 List of CVXPY constraints from the bounds object.
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
234 """
235 return self.bounds.constraints(weights)