Coverage for src / cvxmarkowitz / portfolios / soft_risk.py: 0%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-14 15:50 +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 with soft risk penalty allowing target risk relaxation.""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass, field 

19 

20import cvxpy as cp 

21 

22from cvxmarkowitz.builder import Builder 

23from cvxmarkowitz.model import Model # noqa: F401 

24from cvxmarkowitz.models.expected_returns import ExpectedReturns 

25from cvxmarkowitz.names import ConstraintName as C 

26from cvxmarkowitz.names import ModelName as M 

27from cvxmarkowitz.names import ParameterName as P 

28from cvxmarkowitz.types import Parameter, Variables # noqa: F401 

29 

30 

31@dataclass(frozen=True) 

32class SoftRisk(Builder): 

33 """Maximize w^T mu minus a soft penalty on excess risk. 

34 

35 The objective is maximize w^T mu - omega * (sigma - sigma_target)_+ 

36 subject to long-only, budget, and max-risk constraints. 

37 """ 

38 

39 _sigma: cp.Variable = field(default_factory=lambda: cp.Variable(nonneg=True, name="sigma")) 

40 _sigma_target_times_omega: cp.CallbackParam = field( 

41 default_factory=lambda: cp.CallbackParam( 

42 callback=lambda p, q: 0.0, nonneg=True, name="sigma_target_times_omega" 

43 ) 

44 ) 

45 

46 @property 

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

48 """Return the CVXPY objective for soft-risk maximization.""" 

49 expected_return = self.model[M.RETURN].estimate(self.variables) 

50 soft_risk = cp.pos(self.parameter[P.OMEGA] * self._sigma - self._sigma_target_times_omega) 

51 return cp.Maximize(expected_return - soft_risk) 

52 

53 def __post_init__(self) -> None: 

54 """Initialize models, parameters, and constraints for soft-risk portfolio.""" 

55 super().__post_init__() 

56 

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

58 

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

60 

61 self.parameter[P.SIGMA_TARGET] = cp.Parameter(nonneg=True, name="target volatility") 

62 

63 self.parameter[P.OMEGA] = cp.Parameter(nonneg=True, name="risk priority") 

64 self._sigma_target_times_omega._callback = lambda: ( 

65 self.parameter[P.SIGMA_TARGET].value * self.parameter[P.OMEGA].value 

66 ) 

67 

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

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

70 self.constraints[C.RISK] = self.risk.estimate(self.variables) <= self._sigma 

71 self.constraints["max_risk"] = self._sigma <= self.parameter[P.SIGMA_MAX]