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

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""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass 

19 

20import cvxpy as cvx 

21import numpy as np 

22 

23from ..bounds import Bounds 

24from ..linalg import cholesky 

25from ..model import Model 

26 

27 

28@dataclass 

29class FactorModel(Model): 

30 """Factor risk model""" 

31 

32 assets: int = 0 

33 """Maximal number of assets""" 

34 

35 k: int = 0 

36 """Maximal number of factors""" 

37 

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 ) 

44 

45 self.parameter["idiosyncratic_risk"] = cvx.Parameter( 

46 shape=self.assets, name="idiosyncratic risk", value=np.zeros(self.assets) 

47 ) 

48 

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 ) 

54 

55 self.bounds_assets = Bounds(m=self.assets, name="assets") 

56 self.bounds_factors = Bounds(m=self.k, name="factors") 

57 

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)) 

63 

64 y = kwargs.get("y", self.parameter["exposure"] @ weights) 

65 

66 return cvx.norm2(cvx.vstack([cvx.norm2(self.parameter["chol"] @ y), var_residual])) 

67 

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) 

72 

73 # get the exposure 

74 exposure = kwargs["exposure"] 

75 

76 # extract dimensions 

77 k, assets = exposure.shape 

78 assert k <= self.k 

79 assert assets <= self.assets 

80 

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) 

86 

87 def constraints(self, weights, **kwargs): 

88 y = kwargs.get("y", self.parameter["exposure"] @ weights) 

89 

90 return ( 

91 self.bounds_assets.constraints(weights) 

92 + self.bounds_factors.constraints(y) 

93 + [y == self.parameter["exposure"] @ weights] 

94 )