Coverage for src / cvx / markowitz / models / bounds.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-08 13:49 +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"""Bounds.""" 

15 

16from __future__ import annotations 

17 

18from dataclasses import dataclass 

19 

20import cvxpy as cp 

21import numpy as np 

22 

23from cvx.markowitz.model import Model 

24from cvx.markowitz.types import Expressions, Matrix, Parameter, Variables # noqa: F401 

25from cvx.markowitz.utils.fill import fill_vector 

26 

27 

28@dataclass(frozen=True) 

29class Bounds(Model): 

30 """Lower/upper bound model applied to a variable vector. 

31 

32 Attributes: 

33 name: Suffix used to distinguish multiple bounds (e.g., "assets"). 

34 acting_on: Key in the variables dict this bound constrains (e.g., D.WEIGHTS). 

35 """ 

36 

37 name: str = "" 

38 acting_on: str = "weights" 

39 

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

41 """No estimation for bounds. 

42 

43 Bounds only contribute constraints; they do not produce an objective term. 

44 """ 

45 raise NotImplementedError("No estimation for bounds") 

46 

47 def _f(self, string: str) -> str: 

48 return f"{string}_{self.name}" 

49 

50 def __post_init__(self) -> None: 

51 """Create lower/upper bound parameters with default values. 

52 

53 Initializes two parameters named with the bound type and `name` suffix, 

54 both sized to `assets`. Defaults are zeros for lower and ones for upper. 

55 """ 

56 self.data[self._f("lower")] = cp.Parameter( 

57 shape=self.assets, 

58 name=self._f("lower"), 

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

60 ) 

61 self.data[self._f("upper")] = cp.Parameter( 

62 shape=self.assets, 

63 name=self._f("upper"), 

64 value=np.ones(self.assets), 

65 ) 

66 

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

68 """Assign lower/upper vectors, padding or trimming to asset length.""" 

69 self.data[self._f("lower")].value = fill_vector(num=self.assets, x=kwargs[self._f("lower")]) 

70 self.data[self._f("upper")].value = fill_vector(num=self.assets, x=kwargs[self._f("upper")]) 

71 

72 def constraints(self, variables: Variables) -> Expressions: 

73 """Return lower/upper inequality constraints for `acting_on` variable. 

74 

75 Raises KeyError if `acting_on` is not present in `variables`. 

76 """ 

77 return { 

78 f"lower bound {self.name}": variables[self.acting_on] >= self.data[self._f("lower")], 

79 f"upper bound {self.name}": variables[self.acting_on] <= self.data[self._f("upper")], 

80 }