Coverage for cvxrisk/factor/factor.py: 95%
41 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-18 11:11 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-18 11:11 +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"""Factor risk model."""
16from __future__ import annotations
18from dataclasses import dataclass
20import cvxpy as cvx
21import numpy as np
23from ..bounds import Bounds
24from ..linalg import cholesky
25from ..model import Model
28@dataclass
29class FactorModel(Model):
30 """Factor risk model."""
32 assets: int = 0
33 """Maximal number of assets"""
35 k: int = 0
36 """Maximal number of factors"""
38 def __post_init__(self):
39 """Initialize the parameters after the class is instantiated.
41 Creates parameters for factor exposure, idiosyncratic risk, and the Cholesky
42 decomposition of the factor covariance matrix. Also initializes bounds for
43 both assets and factors.
44 """
45 self.parameter["exposure"] = cvx.Parameter(
46 shape=(self.k, self.assets),
47 name="exposure",
48 value=np.zeros((self.k, self.assets)),
49 )
51 self.parameter["idiosyncratic_risk"] = cvx.Parameter(
52 shape=self.assets, name="idiosyncratic risk", value=np.zeros(self.assets)
53 )
55 self.parameter["chol"] = cvx.Parameter(
56 shape=(self.k, self.k),
57 name="cholesky of covariance",
58 value=np.zeros((self.k, self.k)),
59 )
61 self.bounds_assets = Bounds(m=self.assets, name="assets")
62 self.bounds_factors = Bounds(m=self.k, name="factors")
64 def estimate(self, weights: cvx.Variable, **kwargs) -> cvx.Expression:
65 """Compute the total portfolio risk using the factor model.
67 Combines systematic risk (from factor exposures) and idiosyncratic risk
68 to calculate the total portfolio risk.
70 Args:
71 weights: CVXPY variable representing portfolio weights
73 **kwargs: Additional keyword arguments, may include:
75 - y: Factor exposures (if not provided, calculated as exposure @ weights)
77 Returns:
78 CVXPY expression: The total portfolio risk
80 """
81 var_residual = cvx.norm2(cvx.multiply(self.parameter["idiosyncratic_risk"], weights))
83 y = kwargs.get("y", self.parameter["exposure"] @ weights)
85 return cvx.norm2(cvx.vstack([cvx.norm2(self.parameter["chol"] @ y), var_residual]))
87 def update(self, **kwargs) -> None:
88 """Update the factor model parameters.
90 Args:
91 **kwargs: Keyword arguments containing:
93 - exposure: Factor exposure matrix
95 - idiosyncratic_risk: Vector of idiosyncratic risks
97 - cov: Factor covariance matrix
99 - Other parameters passed to bounds_assets.update() and bounds_factors.update()
101 """
102 self.parameter["exposure"].value = np.zeros((self.k, self.assets))
103 self.parameter["chol"].value = np.zeros((self.k, self.k))
104 self.parameter["idiosyncratic_risk"].value = np.zeros(self.assets)
106 # get the exposure
107 exposure = kwargs["exposure"]
109 # extract dimensions
110 k, assets = exposure.shape
111 if k > self.k:
112 raise ValueError("Number of factors exceeds maximal number of factors")
113 if assets > self.assets:
114 raise ValueError("Number of assets exceeds maximal number of assets")
116 self.parameter["exposure"].value[:k, :assets] = kwargs["exposure"]
117 self.parameter["idiosyncratic_risk"].value[:assets] = kwargs["idiosyncratic_risk"]
118 self.parameter["chol"].value[:k, :k] = cholesky(kwargs["cov"])
119 self.bounds_assets.update(**kwargs)
120 self.bounds_factors.update(**kwargs)
122 def constraints(self, weights: cvx.Variable, **kwargs) -> list[cvx.Constraint]:
123 """Return constraints for the factor model.
125 Args:
126 weights: CVXPY variable representing portfolio weights
128 **kwargs: Additional keyword arguments, may include:
130 - y: Factor exposures (if not provided, calculated as exposure @ weights)
132 Returns:
133 List of CVXPY constraints including asset bounds, factor bounds,
134 and the constraint that y equals exposure @ weights
136 """
137 y = kwargs.get("y", self.parameter["exposure"] @ weights)
139 return (
140 self.bounds_assets.constraints(weights)
141 + self.bounds_factors.constraints(y)
142 + [y == self.parameter["exposure"] @ weights]
143 )