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
« 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.
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.
20Example:
21 Create and use a sample covariance risk model:
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
41"""
43from __future__ import annotations
45from dataclasses import dataclass
47import cvxpy as cvx
48import numpy as np
50from cvx.risk.bounds import Bounds
51from cvx.risk.linalg import cholesky
52from cvx.risk.model import Model
55@dataclass
56class SampleCovariance(Model):
57 """Risk model based on the Cholesky decomposition of the sample covariance matrix.
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:
63 risk = ||R @ w||_2 = sqrt(w^T @ cov @ w)
65 This represents the portfolio standard deviation (volatility).
67 Attributes:
68 num: Maximum number of assets the model can handle. The model can be
69 updated with fewer assets, but not more.
71 Example:
72 Basic usage:
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
90 Using in optimization:
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
100 """
102 num: int = 0
103 """Maximum number of assets the model can handle."""
105 def __post_init__(self):
106 """Initialize the parameters after the class is instantiated.
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.
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)
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")
127 def estimate(self, weights: cvx.Variable, **kwargs) -> cvx.Expression:
128 """Estimate the portfolio risk using the Cholesky decomposition.
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).
134 Args:
135 weights: CVXPY variable or numpy array representing portfolio weights.
136 **kwargs: Additional keyword arguments (not used).
138 Returns:
139 CVXPY expression representing the portfolio risk (standard deviation).
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
157 """
158 return cvx.norm2(self.parameter["chol"] @ weights)
160 def update(self, **kwargs) -> None:
161 """Update the Cholesky decomposition parameter and bounds.
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.
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.
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)
190 """
191 cov = kwargs["cov"]
192 n = cov.shape[0]
194 self.parameter["chol"].value[:n, :n] = cholesky(cov)
195 self.bounds.update(**kwargs)
197 def constraints(self, weights: cvx.Variable, **kwargs) -> list[cvx.Constraint]:
198 """Return constraints for the sample covariance model.
200 Returns the asset bounds constraints from the internal bounds object.
202 Args:
203 weights: CVXPY variable representing portfolio weights.
204 **kwargs: Additional keyword arguments (not used).
206 Returns:
207 List of CVXPY constraints from the bounds object (lower and upper bounds).
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
224 """
225 return self.bounds.constraints(weights)