1
1
import numpy as np
2
- import warnings
3
2
from tqdm import tqdm
4
3
5
4
try :
@@ -102,25 +101,9 @@ def maxiter(self, value):
102
101
103
102
class SuccessiveConvexOptimizer :
104
103
"""
105
- Successive Convex Approximation optimizer tailored for the risk parity problem including the linear constraints:
106
- Cmat @ w = cvec
107
- Dmat @ w <= dvec,
108
- where matrices Cmat and Dmat have n columns (n being the number of assets). Based on the paper:
109
-
110
- Feng, Y., and Palomar, D. P. (2015). SCRIP: Successive convex optimization methods for risk parity portfolios design.
111
- IEEE Trans. Signal Processing, 63(19), 5285–5300.
112
-
113
- By default, the constraints are set to sum(w) = 1 and w >= 0, i.e.,
114
- Cmat = np.ones((1, n))
115
- cvec = np.array([1.0])
116
- Dmat = -np.eye(n)
117
- dvec = np.zeros(n).
118
-
119
- Notes:
120
- 1) If equality constraints are not needed, set Cmat = np.empty((0, n)) and cvec = [].
121
- 2) If the matrices Cmat and Dmat have more than n columns, it is assumed that the additional columns
122
- (same number for both matrices) correspond to dummy variables (which do not appear in the objective function).
104
+ Successive Convex Approximation optimizer tailored for the risk parity problem.
123
105
"""
106
+
124
107
def __init__ (
125
108
self ,
126
109
portfolio ,
@@ -136,27 +119,28 @@ def __init__(
136
119
dvec = None ,
137
120
):
138
121
self .portfolio = portfolio
139
- self .tau = tau or 1e-4 # 0.05 * np.trace(self.portfolio.covariance) / (2 * self.portfolio.number_of_assets)
122
+ self .tau = tau or 0.05 * np .sum (np .diag (self .portfolio .covariance )) / (
123
+ 2 * self .portfolio .number_of_assets
124
+ )
140
125
sca_validator = SuccessiveConvexOptimizerValidator ()
141
126
self .gamma = sca_validator .gamma = gamma
142
127
self .zeta = sca_validator .zeta = zeta
143
128
self .funtol = sca_validator .funtol = funtol
144
129
self .wtol = sca_validator .wtol = wtol
145
130
self .maxiter = sca_validator .maxiter = maxiter
146
131
self .Cmat = Cmat
147
- self .Dmat = Dmat # Dmat @ w <= dvec
132
+ self .Dmat = Dmat
148
133
self .cvec = cvec
149
134
self .dvec = dvec
150
- self .number_of_vars = self .Cmat .shape [1 ]
151
- self .number_of_dummy_vars = self .number_of_vars - self .portfolio .number_of_assets
152
- self .dummy_vars = np .zeros (self .number_of_dummy_vars )
153
- self .CCmat = np .vstack ((self .Cmat , - self .Dmat )).T # CCmat.T @ w >= bvec
154
- self .bvec = np .concatenate ((self .cvec , - self .dvec ))
135
+ self .CCmat = np .vstack ((self .Cmat , self .Dmat )).T
136
+ self .bvec = np .concatenate ((self .cvec , self .dvec ))
155
137
self .meq = self .Cmat .shape [0 ]
156
138
self ._funk = self .get_objective_function_value ()
157
139
self .objective_function = [self ._funk ]
158
140
self ._tauI = self .tau * np .eye (self .portfolio .number_of_assets )
159
- self .Amat = self .portfolio .risk_concentration .jacobian_risk_concentration_vector ()
141
+ self .Amat = (
142
+ self .portfolio .risk_concentration .jacobian_risk_concentration_vector ()
143
+ )
160
144
self .gvec = self .portfolio .risk_concentration .risk_concentration_vector
161
145
162
146
@property
@@ -167,7 +151,7 @@ def Cmat(self):
167
151
def Cmat (self , value ):
168
152
if value is None :
169
153
self ._Cmat = np .atleast_2d (np .ones (self .portfolio .number_of_assets ))
170
- elif np .atleast_2d (value ).shape [1 ] > = self .portfolio .number_of_assets :
154
+ elif np .atleast_2d (value ).shape [1 ] = = self .portfolio .number_of_assets :
171
155
self ._Cmat = np .atleast_2d (value )
172
156
else :
173
157
raise ValueError (
@@ -182,9 +166,9 @@ def Dmat(self):
182
166
@Dmat .setter
183
167
def Dmat (self , value ):
184
168
if value is None :
185
- self ._Dmat = - np .eye (self .portfolio .number_of_assets )
186
- elif np .atleast_2d (value ).shape [1 ] == self .Cmat . shape [ 1 ] :
187
- self ._Dmat = np .atleast_2d (value )
169
+ self ._Dmat = np .eye (self .portfolio .number_of_assets )
170
+ elif np .atleast_2d (value ).shape [1 ] == self .portfolio . number_of_assets :
171
+ self ._Dmat = - np .atleast_2d (value )
188
172
else :
189
173
raise ValueError (
190
174
"Dmat shape {} doesnt agree with the number of"
@@ -216,7 +200,7 @@ def dvec(self, value):
216
200
if value is None :
217
201
self ._dvec = np .zeros (self .portfolio .number_of_assets )
218
202
elif len (value ) == self .Dmat .shape [0 ]:
219
- self ._dvec = np .atleast_1d (value )
203
+ self ._dvec = - np .atleast_1d (value )
220
204
else :
221
205
raise ValueError (
222
206
"dvec shape {} doesnt agree with Dmat shape"
@@ -231,74 +215,42 @@ def get_objective_function_value(self):
231
215
obj += self .portfolio .lmd * self .portfolio .volatility ** 2
232
216
return obj
233
217
234
- def iterate (self , verbose = True ):
218
+ def iterate (self ):
235
219
wk = self .portfolio .weights
236
220
g = self .gvec (wk )
237
221
A = np .ascontiguousarray (self .Amat (wk ))
238
222
At = np .transpose (A )
239
223
Q = 2 * At @ A + self ._tauI
240
- q = 2 * np .matmul (At , g ) - Q @ wk # np.matmul() is necessary here since g is not a numpy array
224
+ q = 2 * np .matmul (At , g ) - np .matmul (Q , wk )
241
225
if self .portfolio .has_variance :
242
226
Q += self .portfolio .lmd * self .portfolio .covariance
243
227
if self .portfolio .has_mean_return :
244
228
q -= self .portfolio .alpha * self .portfolio .mean
245
- if self .number_of_dummy_vars > 0 :
246
- Q = np .vstack ([np .hstack ([Q , np .zeros ((self .portfolio .number_of_assets , self .number_of_dummy_vars ))]),
247
- np .hstack ([np .zeros ((self .number_of_dummy_vars , self .portfolio .number_of_assets )),
248
- self .tau * np .eye (self .portfolio .number_of_assets )])])
249
- q = np .concatenate ([q , - self .tau * self .dummy_vars ])
250
- # Call QP solver (min 0.5*x.T G x + a.T x s.t. C.T x >= b) controlling for ill-conditioning:
251
- try :
252
- w_hat = quadprog .solve_qp (Q , - q , C = self .CCmat , b = self .bvec , meq = self .meq )[0 ]
253
- except ValueError as e :
254
- if str (e ) == "matrix G is not positive definite" :
255
- warnings .warn (
256
- "Matrix Q is not positive definite: adding regularization term and then calling QP solver again." )
257
- # eigvals = np.linalg.eigvals(Q)
258
- # print(" - before regularization: cond. number = {:,.0f}".format(max(eigvals) / min(eigvals)))
259
- # print(" - after regularization: cond. number = {:,.0f}".format(max(eigvals + np.trace(Q)/1e7) / min(eigvals + np.trace(Q)/1e7)))
260
- Q += np .eye (Q .shape [0 ]) * np .trace (Q )/ 1e7
261
- w_hat = quadprog .solve_qp (Q , - q , C = self .CCmat , b = self .bvec , meq = self .meq )[0 ]
262
- else :
263
- # If the error is different, re-raise it
264
- raise
265
- self .portfolio .weights = wk + self .gamma * (w_hat [:self .portfolio .number_of_assets ] - wk )
229
+ w_hat = quadprog .solve_qp (Q , - q , C = self .CCmat , b = self .bvec , meq = self .meq )[0 ]
230
+ self .portfolio .weights = wk + self .gamma * (w_hat - wk )
266
231
fun_next = self .get_objective_function_value ()
267
232
self .objective_function .append (fun_next )
268
233
has_w_converged = (
269
- ( np .abs (self .portfolio .weights - wk ) <= self . wtol * 0.5 * ( np . abs ( self . portfolio . weights ) + np . abs ( wk )) )
270
- | (( np .abs (self .portfolio .weights ) < 1e-6 ) & ( np .abs (wk ) < 1e-6 ))
234
+ np .abs (self .portfolio .weights - wk )
235
+ <= 0.5 * self . wtol * ( np .abs (self .portfolio .weights ) + np .abs (wk ))
271
236
).all ()
272
237
has_fun_converged = (
273
- (np .abs (self ._funk - fun_next ) <= self .funtol * 0.5 * (np .abs (self ._funk ) + np .abs (fun_next )))
274
- | ((np .abs (self ._funk ) <= 1e-10 ) & (np .abs (fun_next ) <= 1e-10 ))
275
- )
276
- if self .number_of_dummy_vars > 0 :
277
- have_dummies_converged = (
278
- (np .abs (w_hat [self .portfolio .number_of_assets :] - self .dummy_vars ) <= self .wtol * 0.5 *
279
- (np .abs (w_hat [self .portfolio .number_of_assets :]) + np .abs (self .dummy_vars )))
280
- | ((np .abs (w_hat [self .portfolio .number_of_assets :]) < 1e-6 ) & (np .abs (self .dummy_vars ) < 1e-6 ))
281
- ).all ()
282
- self .dummy_vars = w_hat [self .portfolio .number_of_assets :]
283
- else :
284
- have_dummies_converged = True
285
- if (has_w_converged and have_dummies_converged ) or has_fun_converged :
286
- # if verbose:
287
- # print(f" Has func. converged: {has_fun_converged}; has w converged: {has_w_converged}")
238
+ np .abs (self ._funk - fun_next )
239
+ <= 0.5 * self .funtol * (np .abs (self ._funk ) + np .abs (fun_next ))
240
+ ).all ()
241
+ if has_w_converged or has_fun_converged :
288
242
return False
289
243
self .gamma = self .gamma * (1 - self .zeta * self .gamma )
290
244
self ._funk = fun_next
291
245
return True
292
246
293
- def solve (self , verbose = True ):
247
+ def solve (self ):
294
248
i = 0
295
- iterator = range (self .maxiter )
296
- if verbose :
297
- iterator = tqdm (iterator )
298
- for _ in iterator :
299
- if not self .iterate (verbose = verbose ):
300
- break
301
- i += 1
249
+ with tqdm (total = self .maxiter ) as pbar :
250
+ while self .iterate () and i < self .maxiter :
251
+ i += 1
252
+ pbar .update ()
253
+
302
254
303
255
def project_line_and_box (weights , lower_bound , upper_bound ):
304
256
def objective_function (variable , weights ):
0 commit comments