Coverage for src/cvxmarkowitz/models/expected_returns.py: 100%

28 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-15 05: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"""Model for expected returns.""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass 

19 

20import cvxpy as cp 

21import numpy as np 

22 

23from cvxmarkowitz.cvxerror import CvxDataError 

24from cvxmarkowitz.model import Model 

25from cvxmarkowitz.names import DataNames as D 

26from cvxmarkowitz.types import Dimensions, Matrix, Variables 

27from cvxmarkowitz.utils.fill import fill_vector 

28 

29 

30@dataclass(frozen=True) 

31class ExpectedReturns(Model): 

32 """Model for expected returns.""" 

33 

34 def __post_init__(self) -> None: 

35 """Initialize expected-return parameters and uncertainty bounds.""" 

36 self.data[D.MU] = cp.Parameter( 

37 shape=self.assets, 

38 name=D.MU, 

39 value=np.zeros(self.assets), 

40 ) 

41 

42 # Robust return estimate 

43 self.parameter[D.MU_UNCERTAINTY] = cp.Parameter( 

44 shape=self.assets, 

45 name=D.MU_UNCERTAINTY, 

46 value=np.zeros(self.assets), 

47 nonneg=True, 

48 ) 

49 

50 @property 

51 def keywords(self) -> tuple[str, ...]: 

52 """Return the keywords `update` consumes, including `mu_uncertainty`. 

53 

54 `mu_uncertainty` is registered in `parameter` rather than `data`, so the 

55 base implementation -- the keys of `data` -- would not list it, and 

56 `Problem.update` would let a caller omit it and then fail with a 

57 `KeyError` from `update` below. 

58 """ 

59 return (*self.data, D.MU_UNCERTAINTY) 

60 

61 def estimate(self, variables: Variables) -> cp.Expression: 

62 """Return robust expected return w^T mu - mu_uncertainty^T |w|. 

63 

64 Args: 

65 variables: Optimization variables containing D.WEIGHTS. 

66 

67 Returns: 

68 A CVXPY expression for the robust expected return. 

69 """ 

70 return self.data[D.MU] @ variables[D.WEIGHTS] - self.parameter[D.MU_UNCERTAINTY] @ cp.abs(variables[D.WEIGHTS]) 

71 

72 def dimensions(self, **kwargs: Matrix) -> Dimensions: 

73 """Return the number of assets `mu` and its uncertainty imply.""" 

74 return ( 

75 (D.WEIGHTS, len(kwargs[D.MU])), 

76 (D.WEIGHTS, len(kwargs[D.MU_UNCERTAINTY])), 

77 ) 

78 

79 def update(self, **kwargs: Matrix) -> None: 

80 """Update expected returns and their uncertainty bounds. 

81 

82 Expected keyword arguments: 

83 mu: Vector of expected returns. 

84 mu_uncertainty: Nonnegative vector with element-wise uncertainty. 

85 """ 

86 exp_returns = kwargs[D.MU] 

87 self.data[D.MU].value = fill_vector(num=self.assets, x=exp_returns) 

88 

89 # Robust return estimate 

90 uncertainty = kwargs[D.MU_UNCERTAINTY] 

91 if not uncertainty.shape[0] == exp_returns.shape[0]: 

92 raise CvxDataError("Mismatch in length for mu and mu_uncertainty") # noqa: TRY003 

93 

94 self.parameter[D.MU_UNCERTAINTY].value = fill_vector(num=self.assets, x=uncertainty)