Skip to content

Commit f5fc854

Browse files
authored
Revert "Changes: Finer control of the iterative algorithm and dummy variables allowed in the constraints."
1 parent 17e5eda commit f5fc854

File tree

6 files changed

+47
-274
lines changed

6 files changed

+47
-274
lines changed

.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ build/
55
riskparityportfolio.egg-info/
66
var/
77
.DS_Store
8-
.idea/
9-
.coverage*
8+
.coverage
109
docs/source/tutorials/.ipynb_checkpoints/*
1110
.eggs/
1211
.ipynb_checkpoints/*

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import setuptools
55
import os
66

7-
__version__ = "0.6.0"
7+
__version__ = "0.5.1"
88

99
# Prepare and send a new release to PyPI
1010
if "release" in sys.argv[-1]:

src/riskparityportfolio/rpp.py

+7-31
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,14 @@ def __init__(self):
1818

1919

2020
class RiskParityPortfolio:
21-
"""Designs risk parity portfolios by solving the following optimization problem:
21+
"""Designs risk parity portfolios by solving the following optimization problem
2222
23-
minimize R(w) - alpha * mu.T w + lambda * w.T Sigma w
24-
subject to Cw = c, Dw <= d
23+
minimize R(w) - alpha * mu.T * w + lambda * w.T Sigma w
24+
subject to Cw = c, Dw <= d
2525
26-
where R(w) is a risk concentration function, and alpha and lambda are trade-off
26+
where R is a risk concentration function, and alpha and lambda are trade-off
2727
parameters for the expected return and the variance, respectively.
2828
29-
The risk concentration R(w) is computed as the squared l2-norm of the risk concentration vector,
30-
which by default is obtained from RiskContribOverVarianceMinusBudget():
31-
32-
R(w) = sum_i (MR_i/sum(MR_i) - budget_i)^2
33-
34-
where MR_i = w_i * (Sigma @ w)_i are the marginal risks (sum(MR_i) = Var(w)), and
35-
budget_i are the risk budgets (by default 1/n).
36-
3729
Parameters
3830
----------
3931
covariance : array, shape=(n, n)
@@ -44,20 +36,6 @@ class RiskParityPortfolio:
4436
weights of the portfolio
4537
risk_concentration : class
4638
any valid child class of RiskConcentrationFunction
47-
48-
Examples:
49-
# Set up:
50-
>>> import numpy as np
51-
>>> import riskparityportfolio as rpp
52-
>>> n = 10
53-
>>> U = np.random.multivariate_normal(mean=np.zeros(n), cov=0.1 * np.eye(n), size=round(.7 * n)).T
54-
>>> Sigma = U @ U.T + np.eye(n)
55-
# Basic usage with default constraints sum(w) = 1 and w >= 0:
56-
>>> my_portfolio = rpp.RiskParityPortfolio(Sigma)
57-
>>> my_portfolio.design()
58-
>>> my_portfolio.weights
59-
# Basic usage with equality and inequality constraints:
60-
>>> my_portfolio.design(Cmat=Cmat, cvec=cvec, Dmat=Dmat, dvec=dvec)
6139
"""
6240

6341
def __init__(
@@ -194,15 +172,13 @@ def add_variance(self, lmd):
194172
self.lmd = lmd
195173
self.has_variance = True
196174

197-
def design(self, verbose=True, **kwargs):
175+
def design(self, **kwargs):
198176
"""Optimize the portfolio.
199177
200178
Parameters
201179
----------
202-
verbose : boolean
203-
Whether to print the optimization process.
204180
kwargs : dict
205-
Dictionary of parameters to be passed to SuccessiveConvexOptimizer().
181+
Dictionary of parameters to be passed to SuccessiveConvexOptimizer.
206182
"""
207183
self.sca = SuccessiveConvexOptimizer(self, **kwargs)
208-
self.sca.solve(verbose)
184+
self.sca.solve()

src/riskparityportfolio/sca.py

+32-80
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numpy as np
2-
import warnings
32
from tqdm import tqdm
43

54
try:
@@ -102,25 +101,9 @@ def maxiter(self, value):
102101

103102
class SuccessiveConvexOptimizer:
104103
"""
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.
123105
"""
106+
124107
def __init__(
125108
self,
126109
portfolio,
@@ -136,27 +119,28 @@ def __init__(
136119
dvec=None,
137120
):
138121
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+
)
140125
sca_validator = SuccessiveConvexOptimizerValidator()
141126
self.gamma = sca_validator.gamma = gamma
142127
self.zeta = sca_validator.zeta = zeta
143128
self.funtol = sca_validator.funtol = funtol
144129
self.wtol = sca_validator.wtol = wtol
145130
self.maxiter = sca_validator.maxiter = maxiter
146131
self.Cmat = Cmat
147-
self.Dmat = Dmat # Dmat @ w <= dvec
132+
self.Dmat = Dmat
148133
self.cvec = cvec
149134
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))
155137
self.meq = self.Cmat.shape[0]
156138
self._funk = self.get_objective_function_value()
157139
self.objective_function = [self._funk]
158140
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+
)
160144
self.gvec = self.portfolio.risk_concentration.risk_concentration_vector
161145

162146
@property
@@ -167,7 +151,7 @@ def Cmat(self):
167151
def Cmat(self, value):
168152
if value is None:
169153
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:
171155
self._Cmat = np.atleast_2d(value)
172156
else:
173157
raise ValueError(
@@ -182,9 +166,9 @@ def Dmat(self):
182166
@Dmat.setter
183167
def Dmat(self, value):
184168
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)
188172
else:
189173
raise ValueError(
190174
"Dmat shape {} doesnt agree with the number of"
@@ -216,7 +200,7 @@ def dvec(self, value):
216200
if value is None:
217201
self._dvec = np.zeros(self.portfolio.number_of_assets)
218202
elif len(value) == self.Dmat.shape[0]:
219-
self._dvec = np.atleast_1d(value)
203+
self._dvec = -np.atleast_1d(value)
220204
else:
221205
raise ValueError(
222206
"dvec shape {} doesnt agree with Dmat shape"
@@ -231,74 +215,42 @@ def get_objective_function_value(self):
231215
obj += self.portfolio.lmd * self.portfolio.volatility ** 2
232216
return obj
233217

234-
def iterate(self, verbose=True):
218+
def iterate(self):
235219
wk = self.portfolio.weights
236220
g = self.gvec(wk)
237221
A = np.ascontiguousarray(self.Amat(wk))
238222
At = np.transpose(A)
239223
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)
241225
if self.portfolio.has_variance:
242226
Q += self.portfolio.lmd * self.portfolio.covariance
243227
if self.portfolio.has_mean_return:
244228
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)
266231
fun_next = self.get_objective_function_value()
267232
self.objective_function.append(fun_next)
268233
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))
271236
).all()
272237
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:
288242
return False
289243
self.gamma = self.gamma * (1 - self.zeta * self.gamma)
290244
self._funk = fun_next
291245
return True
292246

293-
def solve(self, verbose=True):
247+
def solve(self):
294248
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+
302254

303255
def project_line_and_box(weights, lower_bound, upper_bound):
304256
def objective_function(variable, weights):

0 commit comments

Comments
 (0)