Coverage for cvx/risk/factor/factor.py: 100%
39 statements
« prev ^ index » next coverage.py v7.6.8, created at 2025-01-09 10:59 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2025-01-09 10:59 +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 self.parameter["exposure"] = cvx.Parameter(
40 shape=(self.k, self.assets),
41 name="exposure",
42 value=np.zeros((self.k, self.assets)),
43 )
45 self.parameter["idiosyncratic_risk"] = cvx.Parameter(
46 shape=self.assets, name="idiosyncratic risk", value=np.zeros(self.assets)
47 )
49 self.parameter["chol"] = cvx.Parameter(
50 shape=(self.k, self.k),
51 name="cholesky of covariance",
52 value=np.zeros((self.k, self.k)),
53 )
55 self.bounds_assets = Bounds(m=self.assets, name="assets")
56 self.bounds_factors = Bounds(m=self.k, name="factors")
58 def estimate(self, weights, **kwargs):
59 """
60 Compute the total variance
61 """
62 var_residual = cvx.norm2(cvx.multiply(self.parameter["idiosyncratic_risk"], weights))
64 y = kwargs.get("y", self.parameter["exposure"] @ weights)
66 return cvx.norm2(cvx.vstack([cvx.norm2(self.parameter["chol"] @ y), var_residual]))
68 def update(self, **kwargs):
69 self.parameter["exposure"].value = np.zeros((self.k, self.assets))
70 self.parameter["chol"].value = np.zeros((self.k, self.k))
71 self.parameter["idiosyncratic_risk"].value = np.zeros(self.assets)
73 # get the exposure
74 exposure = kwargs["exposure"]
76 # extract dimensions
77 k, assets = exposure.shape
78 assert k <= self.k
79 assert assets <= self.assets
81 self.parameter["exposure"].value[:k, :assets] = kwargs["exposure"]
82 self.parameter["idiosyncratic_risk"].value[:assets] = kwargs["idiosyncratic_risk"]
83 self.parameter["chol"].value[:k, :k] = cholesky(kwargs["cov"])
84 self.bounds_assets.update(**kwargs)
85 self.bounds_factors.update(**kwargs)
87 def constraints(self, weights, **kwargs):
88 y = kwargs.get("y", self.parameter["exposure"] @ weights)
90 return (
91 self.bounds_assets.constraints(weights)
92 + self.bounds_factors.constraints(y)
93 + [y == self.parameter["exposure"] @ weights]
94 )