Coverage for src / cvx / risk / portfolio / min_risk.py: 100%

8 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-09 03:39 +0000

1"""Minimum risk portfolio optimization. 

2 

3This module provides functions for creating and solving minimum risk portfolio 

4optimization problems using various risk models. 

5 

6Example: 

7 Create and solve a minimum risk portfolio problem: 

8 

9 >>> import cvxpy as cp 

10 >>> import numpy as np 

11 >>> from cvx.risk.sample import SampleCovariance 

12 >>> from cvx.risk.portfolio import minrisk_problem 

13 >>> # Create risk model 

14 >>> model = SampleCovariance(num=3) 

15 >>> model.update( 

16 ... cov=np.array([[1.0, 0.5, 0.0], [0.5, 1.0, 0.5], [0.0, 0.5, 1.0]]), 

17 ... lower_assets=np.zeros(3), 

18 ... upper_assets=np.ones(3) 

19 ... ) 

20 >>> # Create optimization problem 

21 >>> weights = cp.Variable(3) 

22 >>> problem = minrisk_problem(model, weights) 

23 >>> # Solve the problem 

24 >>> _ = problem.solve(solver="CLARABEL") 

25 >>> # Optimal weights sum to 1 

26 >>> bool(np.isclose(np.sum(weights.value), 1.0)) 

27 True 

28 

29""" 

30 

31# Copyright 2023 Stanford University Convex Optimization Group 

32# 

33# Licensed under the Apache License, Version 2.0 (the "License"); 

34# you may not use this file except in compliance with the License. 

35# You may obtain a copy of the License at 

36# 

37# http://www.apache.org/licenses/LICENSE-2.0 

38# 

39# Unless required by applicable law or agreed to in writing, software 

40# distributed under the License is distributed on an "AS IS" BASIS, 

41# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

42# See the License for the specific language governing permissions and 

43# limitations under the License. 

44from __future__ import annotations 

45 

46from typing import Any 

47 

48import cvxpy as cp 

49 

50from cvx.risk import Model 

51 

52 

53def minrisk_problem( 

54 riskmodel: Model, 

55 weights: cp.Variable, 

56 base: cp.Expression | float = 0.0, 

57 constraints: list[cp.Constraint] | None = None, 

58 **kwargs: Any, 

59) -> cp.Problem: 

60 """Create a minimum-risk portfolio optimization problem. 

61 

62 This function creates a CVXPY optimization problem that minimizes portfolio 

63 risk subject to constraints. The problem includes standard constraints 

64 (weights sum to 1, weights are non-negative) plus any model-specific and 

65 custom constraints. 

66 

67 Args: 

68 riskmodel: A risk model implementing the `Model` interface, used to 

69 compute portfolio risk. Can be SampleCovariance, FactorModel, 

70 CVar, etc. 

71 weights: CVXPY variable representing the portfolio weights. Should have 

72 shape (n,) where n is the number of assets. 

73 base: Expression representing the base portfolio (default 0.0). Use this 

74 for tracking error minimization where you want to minimize the risk 

75 of deviating from a benchmark. 

76 constraints: Optional list of additional CVXPY constraints to apply to 

77 the optimization problem. 

78 **kwargs: Additional keyword arguments passed to the risk model's 

79 estimate and constraints methods. 

80 

81 Returns: 

82 A CVXPY Problem that minimizes portfolio risk subject to constraints. 

83 The problem includes: 

84 - Objective: minimize risk(weights - base) 

85 - Constraint: sum(weights) == 1 

86 - Constraint: weights >= 0 

87 - Model-specific constraints from riskmodel.constraints() 

88 - Any additional constraints passed in the constraints argument 

89 

90 Example: 

91 Basic minimum risk portfolio: 

92 

93 >>> import cvxpy as cp 

94 >>> import numpy as np 

95 >>> from cvx.risk.sample import SampleCovariance 

96 >>> from cvx.risk.portfolio import minrisk_problem 

97 >>> model = SampleCovariance(num=2) 

98 >>> model.update( 

99 ... cov=np.array([[1.0, 0.5], [0.5, 2.0]]), 

100 ... lower_assets=np.zeros(2), 

101 ... upper_assets=np.ones(2) 

102 ... ) 

103 >>> weights = cp.Variable(2) 

104 >>> problem = minrisk_problem(model, weights) 

105 >>> _ = problem.solve(solver="CLARABEL") 

106 >>> # Lower variance asset gets higher weight 

107 >>> bool(weights.value[0] > weights.value[1]) 

108 True 

109 

110 With tracking error (minimize deviation from benchmark): 

111 

112 >>> benchmark = np.array([0.5, 0.5]) 

113 >>> problem = minrisk_problem(model, weights, base=benchmark) 

114 >>> _ = problem.solve(solver="CLARABEL") 

115 

116 With custom constraints: 

117 

118 >>> custom_constraints = [weights[0] >= 0.3] # At least 30% in first asset 

119 >>> problem = minrisk_problem(model, weights, constraints=custom_constraints) 

120 >>> _ = problem.solve(solver="CLARABEL") 

121 >>> bool(weights.value[0] >= 0.3 - 1e-6) 

122 True 

123 

124 Sector constraints example (limiting sector exposure): 

125 

126 >>> model = SampleCovariance(num=4) 

127 >>> # Tech: assets 0,1; Finance: assets 2,3 

128 >>> model.update( 

129 ... cov=np.eye(4) * 0.04, # 20% vol each, uncorrelated 

130 ... lower_assets=np.zeros(4), 

131 ... upper_assets=np.ones(4) 

132 ... ) 

133 >>> weights = cp.Variable(4) 

134 >>> tech_constraint = [weights[0] + weights[1] <= 0.5] # Max 50% tech 

135 >>> problem = minrisk_problem(model, weights, constraints=tech_constraint) 

136 >>> _ = problem.solve(solver="CLARABEL") 

137 >>> tech_weight = weights.value[0] + weights.value[1] 

138 >>> bool(tech_weight <= 0.5 + 1e-6) 

139 True 

140 

141 Long-short portfolio (removing non-negativity by adjusting bounds): 

142 

143 >>> from cvx.risk.bounds import Bounds 

144 >>> model = SampleCovariance(num=3) 

145 >>> cov = np.array([[0.04, 0.01, 0.02], 

146 ... [0.01, 0.09, 0.01], 

147 ... [0.02, 0.01, 0.04]]) 

148 >>> model.update( 

149 ... cov=cov, 

150 ... lower_assets=np.array([-0.5, -0.5, -0.5]), # Allow shorting 

151 ... upper_assets=np.array([1.5, 1.5, 1.5]) 

152 ... ) 

153 >>> weights = cp.Variable(3) 

154 >>> # Override default non-negativity with custom constraints 

155 >>> long_short_constraints = [weights >= -0.5, weights <= 1.5] 

156 >>> problem = cp.Problem( 

157 ... cp.Minimize(model.estimate(weights)), 

158 ... [cp.sum(weights) == 1.0] + model.constraints(weights) 

159 ... ) 

160 >>> _ = problem.solve(solver="CLARABEL") 

161 >>> bool(np.isclose(np.sum(weights.value), 1.0, atol=1e-4)) 

162 True 

163 

164 Using with FactorModel and explicit factor exposure variable: 

165 

166 >>> from cvx.risk.factor import FactorModel 

167 >>> factor_model = FactorModel(assets=4, k=2) 

168 >>> factor_model.update( 

169 ... exposure=np.array([[1.0, 0.8, 0.2, 0.1], 

170 ... [0.1, 0.2, 0.9, 1.0]]), 

171 ... cov=np.eye(2) * 0.04, 

172 ... idiosyncratic_risk=np.array([0.1, 0.1, 0.1, 0.1]), 

173 ... lower_assets=np.zeros(4), 

174 ... upper_assets=np.ones(4), 

175 ... lower_factors=-np.ones(2), 

176 ... upper_factors=np.ones(2) 

177 ... ) 

178 >>> weights = cp.Variable(4) 

179 >>> y = cp.Variable(2) # Factor exposures 

180 >>> problem = minrisk_problem(factor_model, weights, y=y) 

181 >>> _ = problem.solve(solver="CLARABEL") 

182 >>> bool(np.isclose(np.sum(weights.value), 1.0, atol=1e-4)) 

183 True 

184 

185 """ 

186 # if no constraints are specified 

187 constraints = constraints or [] 

188 

189 problem = cp.Problem( 

190 objective=cp.Minimize(riskmodel.estimate(weights - base, **kwargs)), 

191 constraints=[ 

192 cp.sum(weights) == 1.0, 

193 weights >= 0, 

194 *riskmodel.constraints(weights, **kwargs), 

195 *constraints, 

196 ], 

197 ) 

198 

199 return problem