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
« 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."""
16from __future__ import annotations
18from dataclasses import dataclass
20import cvxpy as cp
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
29@dataclass(frozen=True)
30class MaxSharpe(Builder):
31 """Maximize expected return under long-only, budget, and risk constraints.
33 Example:
34 The volatility cap is a parameter, set on the builder before `build`:
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 """
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))
63 def __post_init__(self) -> None:
64 """Initialize models, parameters, and constraints for the builder."""
65 super().__post_init__()
67 self.model[M.RETURN] = ExpectedReturns(assets=self.assets)
69 self.parameter[P.SIGMA_MAX] = cp.Parameter(nonneg=True, name="maximal volatility")
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]