Coverage for src/cvxmarkowitz/portfolios/max_sharpe.py: 100%

20 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-27 05:46 +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"""Portfolio builder maximizing expected return subject to risk and basic constraints.""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass 

19 

20import cvxpy as cp 

21 

22from cvxmarkowitz.builder import Builder 

23from cvxmarkowitz.models.expected_returns import ExpectedReturns 

24from cvxmarkowitz.names import ConstraintName as C 

25from cvxmarkowitz.names import ModelName as M 

26from cvxmarkowitz.names import ParameterName as P 

27 

28 

29@dataclass(frozen=True) 

30class MaxSharpe(Builder): 

31 """Maximize expected return under long-only, budget, and risk constraints. 

32 

33 Example: 

34 The volatility cap is a parameter, set on the builder before `build`: 

35 

36 >>> import numpy as np 

37 >>> from cvxmarkowitz.names import DataNames as D 

38 >>> from cvxmarkowitz.names import ParameterName as P 

39 >>> builder = MaxSharpe(assets=4) 

40 >>> builder.parameter[P.SIGMA_MAX].value = 1.0 

41 >>> problem = builder.build() 

42 >>> problem.update( 

43 ... **{ 

44 ... D.CHOLESKY: np.linalg.cholesky(np.array([[1.0, 0.5], [0.5, 2.0]])).T, 

45 ... D.LOWER_BOUND_ASSETS: np.zeros(2), 

46 ... D.UPPER_BOUND_ASSETS: np.ones(2), 

47 ... D.VOLA_UNCERTAINTY: np.zeros(2), 

48 ... D.MU: np.array([0.25, 0.30]), 

49 ... D.MU_UNCERTAINTY: np.zeros(2), 

50 ... } 

51 ... ) 

52 >>> round(problem.solve(), 4) 

53 0.275 

54 >>> np.round(problem.weights, 3).tolist() 

55 [0.5, 0.5, 0.0, 0.0] 

56 """ 

57 

58 @property 

59 def objective(self) -> cp.Maximize: 

60 """Return the CVXPY objective for maximizing expected return.""" 

61 return cp.Maximize(self.model[M.RETURN].estimate(self.variables)) 

62 

63 def __post_init__(self) -> None: 

64 """Initialize models, parameters, and constraints for the builder.""" 

65 super().__post_init__() 

66 

67 self.model[M.RETURN] = ExpectedReturns(assets=self.assets) 

68 

69 self.parameter[P.SIGMA_MAX] = cp.Parameter(nonneg=True, name="maximal volatility") 

70 

71 self.constraints[C.LONG_ONLY] = self.weights >= 0 

72 self.constraints[C.BUDGET] = cp.sum(self.weights) == 1.0 

73 self.constraints[C.RISK] = self.risk.estimate(self.variables) <= self.parameter[P.SIGMA_MAX]