skfolio 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skfolio/__init__.py +29 -0
- skfolio/cluster/__init__.py +8 -0
- skfolio/cluster/_hierarchical.py +387 -0
- skfolio/datasets/__init__.py +20 -0
- skfolio/datasets/_base.py +389 -0
- skfolio/datasets/data/__init__.py +0 -0
- skfolio/datasets/data/factors_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_index.csv.gz +0 -0
- skfolio/distance/__init__.py +26 -0
- skfolio/distance/_base.py +55 -0
- skfolio/distance/_distance.py +574 -0
- skfolio/exceptions.py +30 -0
- skfolio/measures/__init__.py +76 -0
- skfolio/measures/_enums.py +355 -0
- skfolio/measures/_measures.py +607 -0
- skfolio/metrics/__init__.py +3 -0
- skfolio/metrics/_scorer.py +121 -0
- skfolio/model_selection/__init__.py +18 -0
- skfolio/model_selection/_combinatorial.py +407 -0
- skfolio/model_selection/_validation.py +194 -0
- skfolio/model_selection/_walk_forward.py +221 -0
- skfolio/moments/__init__.py +41 -0
- skfolio/moments/covariance/__init__.py +29 -0
- skfolio/moments/covariance/_base.py +101 -0
- skfolio/moments/covariance/_covariance.py +1108 -0
- skfolio/moments/expected_returns/__init__.py +21 -0
- skfolio/moments/expected_returns/_base.py +31 -0
- skfolio/moments/expected_returns/_expected_returns.py +415 -0
- skfolio/optimization/__init__.py +36 -0
- skfolio/optimization/_base.py +147 -0
- skfolio/optimization/cluster/__init__.py +13 -0
- skfolio/optimization/cluster/_nco.py +348 -0
- skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
- skfolio/optimization/cluster/hierarchical/_base.py +440 -0
- skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
- skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
- skfolio/optimization/convex/__init__.py +16 -0
- skfolio/optimization/convex/_base.py +1944 -0
- skfolio/optimization/convex/_distributionally_robust.py +392 -0
- skfolio/optimization/convex/_maximum_diversification.py +417 -0
- skfolio/optimization/convex/_mean_risk.py +974 -0
- skfolio/optimization/convex/_risk_budgeting.py +560 -0
- skfolio/optimization/ensemble/__init__.py +6 -0
- skfolio/optimization/ensemble/_base.py +87 -0
- skfolio/optimization/ensemble/_stacking.py +326 -0
- skfolio/optimization/naive/__init__.py +3 -0
- skfolio/optimization/naive/_naive.py +173 -0
- skfolio/population/__init__.py +3 -0
- skfolio/population/_population.py +883 -0
- skfolio/portfolio/__init__.py +13 -0
- skfolio/portfolio/_base.py +1096 -0
- skfolio/portfolio/_multi_period_portfolio.py +610 -0
- skfolio/portfolio/_portfolio.py +842 -0
- skfolio/pre_selection/__init__.py +7 -0
- skfolio/pre_selection/_pre_selection.py +342 -0
- skfolio/preprocessing/__init__.py +3 -0
- skfolio/preprocessing/_returns.py +114 -0
- skfolio/prior/__init__.py +18 -0
- skfolio/prior/_base.py +63 -0
- skfolio/prior/_black_litterman.py +238 -0
- skfolio/prior/_empirical.py +163 -0
- skfolio/prior/_factor_model.py +268 -0
- skfolio/typing.py +50 -0
- skfolio/uncertainty_set/__init__.py +23 -0
- skfolio/uncertainty_set/_base.py +108 -0
- skfolio/uncertainty_set/_bootstrap.py +281 -0
- skfolio/uncertainty_set/_empirical.py +237 -0
- skfolio/utils/__init__.py +0 -0
- skfolio/utils/bootstrap.py +115 -0
- skfolio/utils/equations.py +350 -0
- skfolio/utils/sorting.py +117 -0
- skfolio/utils/stats.py +466 -0
- skfolio/utils/tools.py +567 -0
- skfolio-0.0.1.dist-info/LICENSE +29 -0
- skfolio-0.0.1.dist-info/METADATA +568 -0
- skfolio-0.0.1.dist-info/RECORD +79 -0
- skfolio-0.0.1.dist-info/WHEEL +5 -0
- skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1944 @@
|
|
1
|
+
"""Base Convex Optimization estimator."""
|
2
|
+
|
3
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
4
|
+
# License: BSD 3 clause
|
5
|
+
|
6
|
+
import warnings
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from enum import auto
|
9
|
+
|
10
|
+
import cvxpy as cp
|
11
|
+
import cvxpy.constraints.constraint as cpc
|
12
|
+
import numpy as np
|
13
|
+
import numpy.typing as npt
|
14
|
+
import scipy as sc
|
15
|
+
import scipy.sparse.linalg as scl
|
16
|
+
|
17
|
+
import skfolio.typing as skt
|
18
|
+
from skfolio.measures import RiskMeasure, owa_gmd_weights
|
19
|
+
from skfolio.optimization._base import BaseOptimization
|
20
|
+
from skfolio.prior import BasePrior, PriorModel
|
21
|
+
from skfolio.uncertainty_set import (
|
22
|
+
BaseCovarianceUncertaintySet,
|
23
|
+
BaseMuUncertaintySet,
|
24
|
+
UncertaintySet,
|
25
|
+
)
|
26
|
+
from skfolio.utils.equations import equations_to_matrix
|
27
|
+
from skfolio.utils.tools import AutoEnum, cache_method, input_to_array
|
28
|
+
|
29
|
+
INSTALLED_SOLVERS = cp.installed_solvers()
|
30
|
+
|
31
|
+
|
32
|
+
class ObjectiveFunction(AutoEnum):
|
33
|
+
r"""Enumeration of objective functions.
|
34
|
+
|
35
|
+
Attributes
|
36
|
+
----------
|
37
|
+
MINIMIZE_RISK : str
|
38
|
+
Minimize the risk measure.
|
39
|
+
|
40
|
+
MAXIMIZE_RETURN : str
|
41
|
+
Maximize the expected return.
|
42
|
+
|
43
|
+
MAXIMIZE_UTILITY : str
|
44
|
+
Maximize the utility :math:`w^T\mu - \lambda \times risk(w)`.
|
45
|
+
|
46
|
+
MAXIMIZE_UTILITY : str
|
47
|
+
Maximize the ratio :math:`\frac{w^T\mu - R_{f}}{risk(w)}`.
|
48
|
+
"""
|
49
|
+
MINIMIZE_RISK = auto()
|
50
|
+
MAXIMIZE_RETURN = auto()
|
51
|
+
MAXIMIZE_UTILITY = auto()
|
52
|
+
MAXIMIZE_RATIO = auto()
|
53
|
+
|
54
|
+
|
55
|
+
class ConvexOptimization(BaseOptimization, ABC):
|
56
|
+
r"""Base class for all convex optimization estimators in skfolio.
|
57
|
+
|
58
|
+
All risk measures that have a convex formulation are defined in class methods with
|
59
|
+
naming convention: `_{risk_measure}_risk`. That naming convention is used for
|
60
|
+
dynamic lookup.
|
61
|
+
|
62
|
+
CVX expressions that are shared among multiple risk measures are cached in a
|
63
|
+
dictionary named `_cvx_cache`.
|
64
|
+
This is to avoid cvx expression duplication and improve performance and convergence.
|
65
|
+
|
66
|
+
Parameters
|
67
|
+
----------
|
68
|
+
risk_measure : RiskMeasure, default=RiskMeasure.VARIANCE
|
69
|
+
:class:`~skfolio.meta.RiskMeasure` of the optimization.
|
70
|
+
Can be any of:
|
71
|
+
|
72
|
+
* VARIANCE
|
73
|
+
* SEMI_VARIANCE
|
74
|
+
* STANDARD_DEVIATION
|
75
|
+
* SEMI_DEVIATION
|
76
|
+
* MEAN_ABSOLUTE_DEVIATION
|
77
|
+
* FIRST_LOWER_PARTIAL_MOMENT
|
78
|
+
* CVAR
|
79
|
+
* EVAR
|
80
|
+
* WORST_REALIZATION
|
81
|
+
* CDAR
|
82
|
+
* MAX_DRAWDOWN
|
83
|
+
* AVERAGE_DRAWDOWN
|
84
|
+
* EDAR
|
85
|
+
* ULCER_INDEX
|
86
|
+
* GINI_MEAN_DIFFERENCE_RATIO
|
87
|
+
|
88
|
+
The default is `RiskMeasure.VARIANCE`.
|
89
|
+
|
90
|
+
prior_estimator : BasePrior, optional
|
91
|
+
:ref:`Prior estimator <prior>`.
|
92
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.PriorModel`
|
93
|
+
containing the estimation of assets expected returns, covariance matrix,
|
94
|
+
returns and Cholesky decomposition of the covariance.
|
95
|
+
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
96
|
+
|
97
|
+
min_weights : float | dict[str, float] | array-like of shape (n_assets, ) | None, default=0.0
|
98
|
+
Minimum assets weights (weights lower bounds).
|
99
|
+
If a float is provided, it is applied to each asset.
|
100
|
+
`None` is equivalent to `-np.Inf` (no lower bound).
|
101
|
+
If a dictionary is provided, its (key/value) pair must be the
|
102
|
+
(asset name/asset minium weight) and the input `X` of the `fit` methods must
|
103
|
+
be a DataFrame with the assets names in columns.
|
104
|
+
When using a dictionary, assets values that are not provided are assigned
|
105
|
+
a minimum weight of `0.0`.
|
106
|
+
The default value is `0.0` (no short selling).
|
107
|
+
|
108
|
+
Example:
|
109
|
+
|
110
|
+
* `min_weights = 0` --> long only portfolio (no short selling).
|
111
|
+
* `min_weights = None` --> no lower bound (same as `-np.Inf`).
|
112
|
+
* `min_weights = -2` --> each weight must be above -200%.
|
113
|
+
* `min_weights = {"SX5E": 0, "SPX": -2}`
|
114
|
+
* `min_weights = [0, -2]`
|
115
|
+
|
116
|
+
max_weights : float | dict[str, float] | array-like of shape (n_assets, ) | None, default=1.0
|
117
|
+
Maximum assets weights (weights upper bounds).
|
118
|
+
If a float is provided, it is applied to each asset.
|
119
|
+
`None` is equivalent to `+np.Inf` (no upper bound).
|
120
|
+
If a dictionary is provided, its (key/value) pair must be the
|
121
|
+
(asset name/asset maximum weight) and the input `X` of the `fit` methods must
|
122
|
+
be a DataFrame with the assets names in columns.
|
123
|
+
When using a dictionary, assets values that are not provided are assigned
|
124
|
+
a minimum weight of `1.0`.
|
125
|
+
The default value is `1.0` (each asset is below 100%).
|
126
|
+
|
127
|
+
Example:
|
128
|
+
|
129
|
+
* `max_weights = 0` --> no long position (short only portfolio).
|
130
|
+
* `max_weights = None` --> no upper bound.
|
131
|
+
* `max_weights = 2` --> each weight must be below 200%.
|
132
|
+
* `max_weights = {"SX5E": 1, "SPX": 2}`
|
133
|
+
* `max_weights = [1, 2]`
|
134
|
+
|
135
|
+
budget : float | None, default=1.0
|
136
|
+
Investment budget. It is the sum of long positions and short positions (sum of
|
137
|
+
all weights). `None` means no budget constraints.
|
138
|
+
The default value is `1.0` (fully invested portfolio).
|
139
|
+
|
140
|
+
Examples:
|
141
|
+
|
142
|
+
* `budget = 1` --> fully invested portfolio.
|
143
|
+
* `budget = 0` --> market neutral portfolio.
|
144
|
+
* `budget = None` --> no constraints on the sum of weights.
|
145
|
+
|
146
|
+
min_budget : float, optional
|
147
|
+
Minimum budget. It is the lower bound of the sum of long and short positions
|
148
|
+
(sum of all weights). If provided, you must set `budget=None`.
|
149
|
+
The default (`None`) means no minimum budget constraint.
|
150
|
+
|
151
|
+
max_budget : float, optional
|
152
|
+
Maximum budget. It is the upper bound of the sum of long and short positions
|
153
|
+
(sum of all weights). If provided, you must set `budget=None`.
|
154
|
+
The default (`None`) means no maximum budget constraint.
|
155
|
+
|
156
|
+
max_short : float, optional
|
157
|
+
Maximum short position. The short position is defined as the sum of negative
|
158
|
+
weights (in absolute term).
|
159
|
+
The default (`None`) means no maximum short position.
|
160
|
+
|
161
|
+
max_long : float, optional
|
162
|
+
Maximum long position. The long position is defined as the sum of positive
|
163
|
+
weights.
|
164
|
+
The default (`None`) means no maximum long position.
|
165
|
+
|
166
|
+
transaction_costs : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
|
167
|
+
Transaction costs of the assets. It is used to add linear transaction costs to
|
168
|
+
the optimization problem:
|
169
|
+
|
170
|
+
.. math:: total\_cost = \sum_{i=1}^{N} c_{i} \times |w_{i} - w\_prev_{i}|
|
171
|
+
|
172
|
+
with :math:`c_{i}` the transaction cost of asset i, :math:`w_{i}` its weight
|
173
|
+
and :math:`w\_prev_{i}` its previous weight (defined in `previous_weights`).
|
174
|
+
The float :math:`total\_cost` is used in the portfolio expected return:
|
175
|
+
|
176
|
+
.. math:: expected\_return = \mu^{T} \cdot w - total\_cost
|
177
|
+
|
178
|
+
with :math:`\mu` the vector af assets' expected returns and :math:`w` the
|
179
|
+
vector of assets weights.
|
180
|
+
|
181
|
+
If a float is provided, it is applied to each asset.
|
182
|
+
If a dictionary is provided, its (key/value) pair must be the
|
183
|
+
(asset name/asset cost) and the input `X` of the `fit` methods must be a
|
184
|
+
DataFrame with the assets names in columns.
|
185
|
+
The default value is `0.0`.
|
186
|
+
|
187
|
+
.. warning::
|
188
|
+
|
189
|
+
Based on the above formula, the periodicity of the transaction costs
|
190
|
+
needs to be homogenous to the periodicity of :math:`\mu`. For example, if
|
191
|
+
the input `X` is composed of **daily** returns, the `transaction_costs` need
|
192
|
+
to be expressed in **daily** costs.
|
193
|
+
(See :ref:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
|
194
|
+
|
195
|
+
management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
|
196
|
+
Management fees of the assets. It is used to add linear management fees to the
|
197
|
+
optimization problem:
|
198
|
+
|
199
|
+
.. math:: total\_fee = \sum_{i=1}^{N} f_{i} \times w_{i}
|
200
|
+
|
201
|
+
with :math:`f_{i}` the management fee of asset i and :math:`w_{i}` its weight.
|
202
|
+
The float :math:`total\_fee` is used in the portfolio expected return:
|
203
|
+
|
204
|
+
.. math:: expected\_return = \mu^{T} \cdot w - total\_fee
|
205
|
+
|
206
|
+
with :math:`\mu` the vector af assets expected returns and :math:`w` the vector
|
207
|
+
of assets weights.
|
208
|
+
|
209
|
+
If a float is provided, it is applied to each asset.
|
210
|
+
If a dictionary is provided, its (key/value) pair must be the
|
211
|
+
(asset name/asset fee) and the input `X` of the `fit` methods must be a
|
212
|
+
DataFrame with the assets names in columns.
|
213
|
+
The default value is `0.0`.
|
214
|
+
|
215
|
+
.. warning::
|
216
|
+
|
217
|
+
Based on the above formula, the periodicity of the management fees needs to
|
218
|
+
be homogenous to the periodicity of :math:`\mu`. For example, if the input
|
219
|
+
`X` is composed of **daily** returns, the `management_fees` need to be
|
220
|
+
expressed in **daily** fees.
|
221
|
+
|
222
|
+
.. note::
|
223
|
+
|
224
|
+
Another approach is to directly impact the management fees to the input `X`
|
225
|
+
in order to express the returns net of fees. However, when estimating the
|
226
|
+
:math:`\mu` parameter using for example Shrinkage estimators, this approach
|
227
|
+
would mix a deterministic value with an uncertain one leading to unwanted
|
228
|
+
bias in the management fees.
|
229
|
+
|
230
|
+
previous_weights : float | dict[str, float] | array-like of shape (n_assets, ), optional
|
231
|
+
Previous weights of the assets. Previous weights are used to compute the
|
232
|
+
portfolio cost and the portfolio turnover.
|
233
|
+
If a float is provided, it is applied to each asset.
|
234
|
+
If a dictionary is provided, its (key/value) pair must be the
|
235
|
+
(asset name/asset previous weight) and the input `X` of the `fit` methods must
|
236
|
+
be a DataFrame with the assets names in columns.
|
237
|
+
The default (`None`) means no previous weights.
|
238
|
+
|
239
|
+
l1_coef : float, default=0.0
|
240
|
+
L1 regularization coefficient.
|
241
|
+
It is used to penalize the objective function by the L1 norm:
|
242
|
+
|
243
|
+
.. math:: l1\_coef \times \Vert w \Vert_{1} = l1\_coef \times \sum_{i=1}^{N} |w_{i}|
|
244
|
+
|
245
|
+
Increasing this coefficient will reduce the number of non-zero weights
|
246
|
+
(cardinality). It tends to increase robustness (out-of-sample stability) but
|
247
|
+
reduces diversification.
|
248
|
+
The default value is `0.0`.
|
249
|
+
|
250
|
+
l2_coef : float, default=0.0
|
251
|
+
L2 regularization coefficient.
|
252
|
+
It is used to penalize the objective function by the L2 norm:
|
253
|
+
|
254
|
+
.. math:: l2\_coef \times \Vert w \Vert_{2}^{2} = l2\_coef \times \sum_{i=1}^{N} w_{i}^2
|
255
|
+
|
256
|
+
It tends to increase robustness (out-of-sample stability).
|
257
|
+
The default value is `0.0`.
|
258
|
+
|
259
|
+
mu_uncertainty_set_estimator : BaseMuUncertaintySet, optional
|
260
|
+
:ref:`Mu Uncertainty set estimator <uncertainty_set_estimator>`.
|
261
|
+
If provided, the assets expected returns are modelled with an ellipsoidal
|
262
|
+
uncertainty set. It is called worst-case optimization and is a class of robust
|
263
|
+
optimization. It reduces the instability that arises from the estimation errors
|
264
|
+
of the expected returns.
|
265
|
+
The worst case portfolio expect return is:
|
266
|
+
|
267
|
+
.. math:: w^T\hat{\mu} - \kappa_{\mu}\lVert S_{\mu}^\frac{1}{2}w\rVert_{2}
|
268
|
+
|
269
|
+
with :math:`\kappa` the size of the ellipsoid (confidence region) and
|
270
|
+
:math:`S` its shape.
|
271
|
+
The default (`None`) means that no uncertainty set is used.
|
272
|
+
|
273
|
+
covariance_uncertainty_set_estimator : BaseCovarianceUncertaintySet, optional
|
274
|
+
:ref:`Covariance Uncertainty set estimator <uncertainty_set_estimator>`.
|
275
|
+
If provided, the assets covariance matrix is modelled with an ellipsoidal
|
276
|
+
uncertainty set. It is called worst-case optimization and is a class of robust
|
277
|
+
optimization. It reduces the instability that arises from the estimation errors
|
278
|
+
of the covariance matrix.
|
279
|
+
The default (`None`) means that no uncertainty set is used.
|
280
|
+
|
281
|
+
linear_constraints : array-like of shape (n_constraints,), optional
|
282
|
+
Linear constraints.
|
283
|
+
The linear constraints must match any of following patterns:
|
284
|
+
|
285
|
+
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
286
|
+
* "ref1 >= 2.9 * ref2"
|
287
|
+
* "ref1 <= ref2"
|
288
|
+
* "ref1 >= ref1"
|
289
|
+
|
290
|
+
With "ref1", "ref2" ... the assets names or the groups names provided
|
291
|
+
in the parameter `groups`. Assets names can be referenced without the need of
|
292
|
+
`groups` if the input `X` of the `fit` methods is a DataFrame with these
|
293
|
+
assets names in columns.
|
294
|
+
|
295
|
+
Examples:
|
296
|
+
|
297
|
+
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
298
|
+
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
299
|
+
* "US >= 0.7" --> the sum of all US weights must be greater than 70%
|
300
|
+
* "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
|
301
|
+
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
302
|
+
|
303
|
+
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
304
|
+
The assets groups referenced in `linear_constraints`.
|
305
|
+
If a dictionary is provided, its (key/value) pair must be the
|
306
|
+
(asset name/asset groups) and the input `X` of the `fit` methods must be a
|
307
|
+
DataFrame with the assets names in columns.
|
308
|
+
|
309
|
+
Examples:
|
310
|
+
|
311
|
+
* groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}
|
312
|
+
* groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
|
313
|
+
|
314
|
+
left_inequality : array-like of shape (n_constraints, n_assets), optional
|
315
|
+
Left inequality matrix :math:`A` of the linear
|
316
|
+
constraint :math:`A \cdot w \leq b`.
|
317
|
+
|
318
|
+
right_inequality : array-like of shape (n_constraints, ), optional
|
319
|
+
Right inequality vector :math:`b` of the linear
|
320
|
+
constraint :math:`A \cdot w \leq b`.
|
321
|
+
|
322
|
+
risk_free_rate : float, default=0.0
|
323
|
+
Risk-free interest rate.
|
324
|
+
The default value is `0.0`.
|
325
|
+
|
326
|
+
min_acceptable_return : float, optional
|
327
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
328
|
+
returns for the computation of lower partial moments:
|
329
|
+
|
330
|
+
* First Lower Partial Moment
|
331
|
+
* Semi-Variance
|
332
|
+
* Semi-Deviation
|
333
|
+
|
334
|
+
The default (`None`) is to use the mean.
|
335
|
+
|
336
|
+
cvar_beta : float, default=0.95
|
337
|
+
CVaR (Conditional Value at Risk) confidence level.
|
338
|
+
The default value is `0.95`.
|
339
|
+
|
340
|
+
evar_beta : float, default=0
|
341
|
+
EVaR (Entropic Value at Risk) confidence level.
|
342
|
+
The default value is `0.95`.
|
343
|
+
|
344
|
+
cdar_beta : float, default=0.95
|
345
|
+
CDaR (Conditional Drawdown at Risk) confidence level.
|
346
|
+
The default value is `0.95`.
|
347
|
+
|
348
|
+
edar_beta : float, default=0.95
|
349
|
+
EDaR (Entropic Drawdown at Risk) confidence level.
|
350
|
+
The default value is `0.95`.
|
351
|
+
|
352
|
+
add_objective : Callable[[cp.Variable], cp.Expression], optional
|
353
|
+
Add a custom objective to the existing objective expression.
|
354
|
+
It is a function that must take as argument the weights `w` and returns a
|
355
|
+
CVXPY expression.
|
356
|
+
|
357
|
+
add_constraints : Callable[[cp.Variable], cp.Expression|list[cp.Expression]], optional
|
358
|
+
Add a custom constraint or a list of constraints to the existing constraints.
|
359
|
+
It is a function that must take as argument the weights `w` and returns a
|
360
|
+
CVPXY expression or a list of CVPXY expressions.
|
361
|
+
|
362
|
+
overwrite_expected_return : Callable[[cp.Variable], cp.Expression], optional
|
363
|
+
Overwrite the expected return :math:`\mu \cdot w` with a custom expression.
|
364
|
+
It is a function that must take as argument the weights `w` and returns a
|
365
|
+
CVPXY expression.
|
366
|
+
|
367
|
+
solver : str, optional
|
368
|
+
The solver to use. For example, "ECOS", "SCS", or "OSQP".
|
369
|
+
The default (`None`) is set depending on the problem.
|
370
|
+
For more details about available solvers, check the CVXPY documentation:
|
371
|
+
https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver
|
372
|
+
|
373
|
+
solver_params : dict, optional
|
374
|
+
Solver parameters. For example, `solver_params=dict(verbose=True)`.
|
375
|
+
For more details about solver arguments, check the CVXPY documentation:
|
376
|
+
https://www.cvxpy.org/tutorial/advanced/index.html#setting-solver-options
|
377
|
+
|
378
|
+
scale_objective : float, optional
|
379
|
+
Scale each objective element by this value.
|
380
|
+
It can be used to increase the optimization accuracies in specific cases.
|
381
|
+
The default (`None`) is set depending on the problem.
|
382
|
+
|
383
|
+
scale_constraints : float, optional
|
384
|
+
Scale each constraint element by this value.
|
385
|
+
It can be used to increase the optimization accuracies in specific cases.
|
386
|
+
The default (`None`) is set depending on the problem.
|
387
|
+
|
388
|
+
raise_on_failure : bool, default=True
|
389
|
+
If this is set to True, an error is raised when the optimization fail otherwise
|
390
|
+
it passes with a warning.
|
391
|
+
|
392
|
+
portfolio_params : dict, optional
|
393
|
+
Portfolio parameters passed to the portfolio evaluated by the `predict` and
|
394
|
+
`score` methods. If not provided, the `name`, `transaction_costs`,
|
395
|
+
`management_fees` and `previous_weights` are copied from the optimization
|
396
|
+
model and systematically passed to the portfolio.
|
397
|
+
|
398
|
+
Attributes
|
399
|
+
----------
|
400
|
+
weights_ : ndarray of shape (n_assets,) or (n_optimizations, n_assets)
|
401
|
+
Weights of the assets.
|
402
|
+
|
403
|
+
problem_: cvxpy.Problem
|
404
|
+
CVXPY problem used for the optimization.
|
405
|
+
|
406
|
+
problem_values_ : dict[str, float] | list[dict[str, float]] of size n_optimizations
|
407
|
+
Expression values retrieved from the CVXPY problem.
|
408
|
+
|
409
|
+
prior_estimator_ : BasePrior
|
410
|
+
Fitted `prior_estimator`.
|
411
|
+
|
412
|
+
mu_uncertainty_set_estimator_ : BaseMuUncertaintySet
|
413
|
+
Fitted `mu_uncertainty_set_estimator` if provided.
|
414
|
+
|
415
|
+
covariance_uncertainty_set_estimator_ : BaseCovarianceUncertaintySet
|
416
|
+
Fitted `covariance_uncertainty_set_estimator` if provided.
|
417
|
+
"""
|
418
|
+
_solver: str
|
419
|
+
_scale_objective: cp.Constant
|
420
|
+
_scale_constraints: cp.Constant
|
421
|
+
|
422
|
+
_cvx_cache = dict
|
423
|
+
problem_: cp.Problem
|
424
|
+
problem_values_: dict[str, float] | list[dict[str, float]]
|
425
|
+
prior_estimator_: BasePrior
|
426
|
+
mu_uncertainty_set_estimator_: BaseMuUncertaintySet
|
427
|
+
covariance_uncertainty_set_estimator_: BaseCovarianceUncertaintySet
|
428
|
+
|
429
|
+
@abstractmethod
|
430
|
+
def __init__(
|
431
|
+
self,
|
432
|
+
risk_measure: RiskMeasure = RiskMeasure.VARIANCE,
|
433
|
+
prior_estimator: BasePrior | None = None,
|
434
|
+
min_weights: skt.MultiInput | None = 0.0,
|
435
|
+
max_weights: skt.MultiInput | None = 1.0,
|
436
|
+
budget: float | None = 1.0,
|
437
|
+
min_budget: float | None = None,
|
438
|
+
max_budget: float | None = None,
|
439
|
+
max_short: float | None = None,
|
440
|
+
max_long: float | None = None,
|
441
|
+
transaction_costs: skt.MultiInput = 0.0,
|
442
|
+
management_fees: skt.MultiInput = 0.0,
|
443
|
+
previous_weights: skt.MultiInput | None = None,
|
444
|
+
groups: skt.Groups | None = None,
|
445
|
+
linear_constraints: skt.LinearConstraints | None = None,
|
446
|
+
left_inequality: skt.Inequality | None = None,
|
447
|
+
right_inequality: skt.Inequality | None = None,
|
448
|
+
l1_coef: float = 0.0,
|
449
|
+
l2_coef: float = 0.0,
|
450
|
+
mu_uncertainty_set_estimator: BaseMuUncertaintySet | None = None,
|
451
|
+
covariance_uncertainty_set_estimator: BaseCovarianceUncertaintySet
|
452
|
+
| None = None,
|
453
|
+
risk_free_rate: float = 0.0,
|
454
|
+
min_acceptable_return: skt.Target | None = None,
|
455
|
+
cvar_beta: float = 0.95,
|
456
|
+
evar_beta: float = 0.95,
|
457
|
+
cdar_beta: float = 0.95,
|
458
|
+
edar_beta: float = 0.95,
|
459
|
+
solver: str | None = None,
|
460
|
+
solver_params: dict | None = None,
|
461
|
+
scale_objective: float | None = None,
|
462
|
+
scale_constraints: float | None = None,
|
463
|
+
raise_on_failure: bool = True,
|
464
|
+
add_objective: skt.ExpressionFunction | None = None,
|
465
|
+
add_constraints: skt.ExpressionFunction | None = None,
|
466
|
+
overwrite_expected_return: skt.ExpressionFunction | None = None,
|
467
|
+
portfolio_params: dict | None = None,
|
468
|
+
):
|
469
|
+
super().__init__(portfolio_params=portfolio_params)
|
470
|
+
if risk_measure.is_annualized:
|
471
|
+
warnings.warn(
|
472
|
+
f"The annualized risk measure {risk_measure} will be converted"
|
473
|
+
f"to its non-annualized version {risk_measure.non_annualized_measure}",
|
474
|
+
stacklevel=2,
|
475
|
+
)
|
476
|
+
risk_measure = risk_measure.non_annualized_measure
|
477
|
+
self.risk_measure = risk_measure
|
478
|
+
self.prior_estimator = prior_estimator
|
479
|
+
self.mu_uncertainty_set_estimator = mu_uncertainty_set_estimator
|
480
|
+
self.covariance_uncertainty_set_estimator = covariance_uncertainty_set_estimator
|
481
|
+
self.min_weights = min_weights
|
482
|
+
self.max_weights = max_weights
|
483
|
+
self.budget = budget
|
484
|
+
self.min_budget = min_budget
|
485
|
+
self.max_budget = max_budget
|
486
|
+
self.max_short = max_short
|
487
|
+
self.max_long = max_long
|
488
|
+
self.min_acceptable_return = min_acceptable_return
|
489
|
+
self.transaction_costs = transaction_costs
|
490
|
+
self.management_fees = management_fees
|
491
|
+
self.previous_weights = previous_weights
|
492
|
+
self.groups = groups
|
493
|
+
self.linear_constraints = linear_constraints
|
494
|
+
self.left_inequality = left_inequality
|
495
|
+
self.right_inequality = right_inequality
|
496
|
+
self.l1_coef = l1_coef
|
497
|
+
self.l2_coef = l2_coef
|
498
|
+
self.risk_free_rate = risk_free_rate
|
499
|
+
self.add_objective = add_objective
|
500
|
+
self.add_constraints = add_constraints
|
501
|
+
self.overwrite_expected_return = overwrite_expected_return
|
502
|
+
self.solver = solver
|
503
|
+
self.solver_params = solver_params
|
504
|
+
self.raise_on_failure = raise_on_failure
|
505
|
+
self.scale_objective = scale_objective
|
506
|
+
self.scale_constraints = scale_constraints
|
507
|
+
self.cvar_beta = cvar_beta
|
508
|
+
self.evar_beta = evar_beta
|
509
|
+
self.cdar_beta = cdar_beta
|
510
|
+
self.edar_beta = edar_beta
|
511
|
+
|
512
|
+
def _call_custom_func(
|
513
|
+
self, func: skt.ExpressionFunction, w: cp.Variable, name: str = "custom_func"
|
514
|
+
) -> cp.Expression | list[cp.Expression]:
|
515
|
+
"""Call a user specific function, infer arguments and perform validation.
|
516
|
+
|
517
|
+
Parameters
|
518
|
+
----------
|
519
|
+
func : Callable[[cvxpy Variable, any], cvxpy Expression]
|
520
|
+
The custom function. Must have one or two positional arguments.
|
521
|
+
The first argument is the CVXPY weight variable `w` and the second is
|
522
|
+
the reference to the class itself.
|
523
|
+
|
524
|
+
w : cvxpy Variable
|
525
|
+
The CVXPY Variable representing assets weights.
|
526
|
+
|
527
|
+
Returns
|
528
|
+
-------
|
529
|
+
result : cvxpy Expression | list[cvxpy Expression]
|
530
|
+
Result of calling the custom function.
|
531
|
+
"""
|
532
|
+
try:
|
533
|
+
# noinspection PyUnresolvedReferences
|
534
|
+
func_code = func.__code__
|
535
|
+
except AttributeError as err:
|
536
|
+
raise ValueError("Custom functions is invalid") from err
|
537
|
+
|
538
|
+
if func_code.co_argcount == 1:
|
539
|
+
args = (w,)
|
540
|
+
elif func_code.co_argcount == 2:
|
541
|
+
args = (w, self)
|
542
|
+
else:
|
543
|
+
raise ValueError(
|
544
|
+
"Custom functions must have 1 or 2 positional arguments, got"
|
545
|
+
f" {func_code.co_argcount}"
|
546
|
+
)
|
547
|
+
try:
|
548
|
+
return func(*args)
|
549
|
+
except Exception as err:
|
550
|
+
raise TypeError(
|
551
|
+
f"Error while calling {name}. "
|
552
|
+
f"{name} must be a function taking as argument "
|
553
|
+
"the weight variable OR the weight variable and the estimator object."
|
554
|
+
) from err
|
555
|
+
|
556
|
+
def _clean_input(
|
557
|
+
self,
|
558
|
+
value: float | dict | npt.ArrayLike | None,
|
559
|
+
n_assets: int,
|
560
|
+
fill_value: any,
|
561
|
+
name: str,
|
562
|
+
) -> float | np.ndarray:
|
563
|
+
"""Convert input to cleaned float or ndarray.
|
564
|
+
|
565
|
+
Parameters
|
566
|
+
----------
|
567
|
+
value : float, dict, array-like or None.
|
568
|
+
Input value to clean.
|
569
|
+
|
570
|
+
n_assets : int
|
571
|
+
Number of assets. Used to verify the shape of the converted array.
|
572
|
+
|
573
|
+
fill_value : any
|
574
|
+
When `items` is a dictionary, elements that are not in `asset_names` are
|
575
|
+
filled with `fill_value` in the converted array.
|
576
|
+
|
577
|
+
name : str
|
578
|
+
Name used for error messages.
|
579
|
+
|
580
|
+
Returns
|
581
|
+
-------
|
582
|
+
value : float or ndarray of shape (n_assets,)
|
583
|
+
The cleaned float or 1D array.
|
584
|
+
"""
|
585
|
+
if value is None:
|
586
|
+
return fill_value
|
587
|
+
if np.isscalar(value):
|
588
|
+
return float(value)
|
589
|
+
return input_to_array(
|
590
|
+
items=value,
|
591
|
+
n_assets=n_assets,
|
592
|
+
fill_value=fill_value,
|
593
|
+
dim=1,
|
594
|
+
assets_names=(
|
595
|
+
self.feature_names_in_ if hasattr(self, "feature_names_in_") else None
|
596
|
+
),
|
597
|
+
name=name,
|
598
|
+
)
|
599
|
+
|
600
|
+
def _clear_models_cache(self):
|
601
|
+
"""CLear the cache of CVX models"""
|
602
|
+
self._cvx_cache = {}
|
603
|
+
|
604
|
+
def _get_weight_constraints(
|
605
|
+
self, n_assets: int, w: cp.Variable, factor: skt.Factor
|
606
|
+
) -> list[cpc.Constraint]:
|
607
|
+
"""Compute weight constraints from input parameters.
|
608
|
+
|
609
|
+
Parameters
|
610
|
+
----------
|
611
|
+
n_assets : int
|
612
|
+
Number of assets.
|
613
|
+
|
614
|
+
w : cvxpy Variable
|
615
|
+
The CVXPY Variable representing assets weights.
|
616
|
+
|
617
|
+
factor : cvxpy Variable | cvxpy Constant
|
618
|
+
Cvxpy variable or constant.
|
619
|
+
|
620
|
+
Returns
|
621
|
+
-------
|
622
|
+
constrains : list[cvxpy Constrains]
|
623
|
+
The list of weights constraints.
|
624
|
+
"""
|
625
|
+
constraints = []
|
626
|
+
|
627
|
+
if self.min_weights is not None:
|
628
|
+
min_weights = self._clean_input(
|
629
|
+
self.min_weights,
|
630
|
+
n_assets=n_assets,
|
631
|
+
fill_value=0,
|
632
|
+
name="min_weights",
|
633
|
+
)
|
634
|
+
constraints.append(
|
635
|
+
w * self._scale_constraints
|
636
|
+
>= min_weights * factor * self._scale_constraints
|
637
|
+
)
|
638
|
+
|
639
|
+
if self.max_weights is not None:
|
640
|
+
max_weights = self._clean_input(
|
641
|
+
self.max_weights,
|
642
|
+
n_assets=n_assets,
|
643
|
+
fill_value=1,
|
644
|
+
name="max_weights",
|
645
|
+
)
|
646
|
+
constraints.append(
|
647
|
+
w * self._scale_constraints
|
648
|
+
<= max_weights * factor * self._scale_constraints
|
649
|
+
)
|
650
|
+
|
651
|
+
if self.max_long is not None:
|
652
|
+
max_long = float(self.max_long)
|
653
|
+
if max_long <= 0:
|
654
|
+
raise ValueError("`max_long` must be strictly positif")
|
655
|
+
constraints.append(
|
656
|
+
cp.sum(cp.pos(w)) * self._scale_constraints
|
657
|
+
<= max_long * factor * self._scale_constraints
|
658
|
+
)
|
659
|
+
|
660
|
+
if self.max_short is not None:
|
661
|
+
max_short = float(self.max_short)
|
662
|
+
if max_short <= 0:
|
663
|
+
raise ValueError("`max_short` must be strictly positif")
|
664
|
+
constraints.append(
|
665
|
+
cp.sum(cp.neg(w)) * self._scale_constraints
|
666
|
+
<= max_short * factor * self._scale_constraints
|
667
|
+
)
|
668
|
+
|
669
|
+
if self.min_budget is not None:
|
670
|
+
constraints.append(
|
671
|
+
cp.sum(w) * self._scale_constraints
|
672
|
+
>= float(self.min_budget) * factor * self._scale_constraints
|
673
|
+
)
|
674
|
+
|
675
|
+
if self.max_budget is not None:
|
676
|
+
constraints.append(
|
677
|
+
cp.sum(w) * self._scale_constraints
|
678
|
+
<= float(self.max_budget) * factor * self._scale_constraints
|
679
|
+
)
|
680
|
+
|
681
|
+
if self.budget is not None:
|
682
|
+
if self.max_budget is not None:
|
683
|
+
raise ValueError(
|
684
|
+
"`max_budget`and `budget` cannot be provided at the same time"
|
685
|
+
)
|
686
|
+
if self.min_budget is not None:
|
687
|
+
raise ValueError(
|
688
|
+
"`min_budget`and `budget` cannot be provided at the same time"
|
689
|
+
)
|
690
|
+
constraints.append(
|
691
|
+
cp.sum(w) * self._scale_constraints
|
692
|
+
== float(self.budget) * factor * self._scale_constraints
|
693
|
+
)
|
694
|
+
|
695
|
+
if self.linear_constraints is not None:
|
696
|
+
if self.groups is None:
|
697
|
+
if not hasattr(self, "feature_names_in_"):
|
698
|
+
raise ValueError(
|
699
|
+
"If `linear_constraints` is provided you must provide either"
|
700
|
+
" `groups` or `X` as a DataFrame with asset names in columns"
|
701
|
+
)
|
702
|
+
groups = np.asarray([self.feature_names_in_])
|
703
|
+
else:
|
704
|
+
groups = input_to_array(
|
705
|
+
items=self.groups,
|
706
|
+
n_assets=n_assets,
|
707
|
+
fill_value="",
|
708
|
+
dim=2,
|
709
|
+
assets_names=(
|
710
|
+
self.feature_names_in_
|
711
|
+
if hasattr(self, "feature_names_in_")
|
712
|
+
else None
|
713
|
+
),
|
714
|
+
name="groups",
|
715
|
+
)
|
716
|
+
a, b = equations_to_matrix(
|
717
|
+
groups=groups,
|
718
|
+
equations=self.linear_constraints,
|
719
|
+
raise_if_group_missing=False,
|
720
|
+
)
|
721
|
+
if np.any(a != 0):
|
722
|
+
constraints.append(
|
723
|
+
a @ w * self._scale_constraints
|
724
|
+
- b * factor * self._scale_constraints
|
725
|
+
<= 0
|
726
|
+
)
|
727
|
+
|
728
|
+
if self.left_inequality is not None and self.right_inequality is not None:
|
729
|
+
left_inequality = np.asarray(self.left_inequality)
|
730
|
+
right_inequality = np.asarray(self.right_inequality)
|
731
|
+
if left_inequality.ndim != 2:
|
732
|
+
raise ValueError(
|
733
|
+
f"`left_inequality` must be a 2D array, got {left_inequality.ndim}D"
|
734
|
+
" array"
|
735
|
+
)
|
736
|
+
if right_inequality.ndim != 1:
|
737
|
+
raise ValueError(
|
738
|
+
"`right_inequality` must be a 1D array, got"
|
739
|
+
f" {right_inequality.ndim}D array"
|
740
|
+
)
|
741
|
+
if left_inequality.shape[1] != n_assets:
|
742
|
+
raise ValueError(
|
743
|
+
"`left_inequality` must be of shape (n_inequalities, n_assets) "
|
744
|
+
f"with n_assets={n_assets}, got {left_inequality.shape[1]}"
|
745
|
+
)
|
746
|
+
if left_inequality.shape[0] != right_inequality.shape[0]:
|
747
|
+
raise ValueError(
|
748
|
+
"`left_inequality` and `right_inequality` must have same number of"
|
749
|
+
f" rows (i.e. n_inequalities) , got {left_inequality.shape[0]} and"
|
750
|
+
f" {right_inequality.shape[0]}"
|
751
|
+
)
|
752
|
+
constraints.append(
|
753
|
+
self.left_inequality @ w * self._scale_constraints
|
754
|
+
- self.right_inequality * factor * self._scale_constraints
|
755
|
+
<= 0
|
756
|
+
)
|
757
|
+
|
758
|
+
return constraints
|
759
|
+
|
760
|
+
def _set_solver(self, default: str) -> None:
|
761
|
+
"""Set solver by saving its value in `_solver`.
|
762
|
+
It uses `solver` if provided otherwise it uses the `default` solver.
|
763
|
+
|
764
|
+
Parameters
|
765
|
+
----------
|
766
|
+
default : str
|
767
|
+
The default solver to use when `solver` is `None`.
|
768
|
+
"""
|
769
|
+
if self.solver is None:
|
770
|
+
self._solver = default
|
771
|
+
else:
|
772
|
+
self._solver = self.solver
|
773
|
+
if self._solver not in INSTALLED_SOLVERS:
|
774
|
+
raise ValueError(f"The solver {self._solver} is not installed.")
|
775
|
+
|
776
|
+
def _set_scale_objective(self, default: float) -> None:
|
777
|
+
"""Set the objective scale by saving its value in `_scale_objective`.
|
778
|
+
It uses `scale_objective` if provided otherwise it uses the `default` scale.
|
779
|
+
|
780
|
+
Parameters
|
781
|
+
----------
|
782
|
+
default : float
|
783
|
+
The default objective scale to use when `scale_objective` is `None`.
|
784
|
+
"""
|
785
|
+
if self.scale_objective is None:
|
786
|
+
self._scale_objective = cp.Constant(default)
|
787
|
+
else:
|
788
|
+
self._scale_objective = cp.Constant(self.scale_objective)
|
789
|
+
|
790
|
+
def _set_scale_constraints(self, default: float) -> None:
|
791
|
+
"""Set the constraints scale by saving its value in `_scale_constraints`.
|
792
|
+
It uses `scale_constraints` if provided otherwise it uses the `default` scale.
|
793
|
+
|
794
|
+
Parameters
|
795
|
+
----------
|
796
|
+
default : float
|
797
|
+
The default constraints scale to use when `scale_constraints` is `None`.
|
798
|
+
"""
|
799
|
+
if self.scale_constraints is None:
|
800
|
+
self._scale_constraints = cp.Constant(default)
|
801
|
+
else:
|
802
|
+
self._scale_constraints = cp.Constant(self.scale_constraints)
|
803
|
+
|
804
|
+
def _get_custom_objective(self, w: cp.Variable) -> cp.Expression:
|
805
|
+
"""Returns the CVXPY expression evaluated by calling the `add_objective`
|
806
|
+
function if provided, otherwise returns the CVXPY constant `0`.
|
807
|
+
|
808
|
+
Parameters
|
809
|
+
----------
|
810
|
+
w : cvxpy Variable
|
811
|
+
The CVXPY Variable representing assets weights.
|
812
|
+
|
813
|
+
Returns
|
814
|
+
-------
|
815
|
+
expression : cvxpy Expression
|
816
|
+
The CVXPY expression evaluated by calling the `add_objective`
|
817
|
+
function if provided, otherwise returns the CVXPY constant `0`.
|
818
|
+
"""
|
819
|
+
if self.add_objective is None:
|
820
|
+
return cp.Constant(0)
|
821
|
+
return self._call_custom_func(
|
822
|
+
func=self.add_objective, w=w, name="add_objective"
|
823
|
+
)
|
824
|
+
|
825
|
+
def _get_custom_constraints(self, w: cp.Variable) -> list[cp.Expression]:
|
826
|
+
"""Returns the list of CVXPY expressions evaluated by calling the
|
827
|
+
`add_constraint`s function if provided, otherwise returns an empty list.
|
828
|
+
|
829
|
+
Parameters
|
830
|
+
----------
|
831
|
+
w : cvxpy Variable
|
832
|
+
The CVXPY Variable representing assets weights.
|
833
|
+
|
834
|
+
Returns
|
835
|
+
-------
|
836
|
+
expressions : list of cvxpy Expression
|
837
|
+
The list of CVXPY expressions evaluated by calling the
|
838
|
+
`add_constraints` function if provided, otherwise returns an empty list.
|
839
|
+
"""
|
840
|
+
if self.add_constraints is None:
|
841
|
+
return []
|
842
|
+
constraints = self._call_custom_func(
|
843
|
+
func=self.add_constraints, w=w, name="add_constraint"
|
844
|
+
)
|
845
|
+
if isinstance(constraints, list):
|
846
|
+
return constraints
|
847
|
+
return [constraints]
|
848
|
+
|
849
|
+
@cache_method("_cvx_cache")
|
850
|
+
def _cvx_expected_return(
|
851
|
+
self, prior_model: PriorModel, w: cp.Variable
|
852
|
+
) -> cp.Expression:
|
853
|
+
"""Expected Return expression"""
|
854
|
+
if self.overwrite_expected_return is None:
|
855
|
+
expected_return = prior_model.mu @ w
|
856
|
+
else:
|
857
|
+
expected_return = self._call_custom_func(
|
858
|
+
func=self.overwrite_expected_return,
|
859
|
+
w=w,
|
860
|
+
name="overwrite_expected_return",
|
861
|
+
)
|
862
|
+
return expected_return
|
863
|
+
|
864
|
+
# Model reused among multiple risk measure
|
865
|
+
def _solve_problem(
|
866
|
+
self,
|
867
|
+
problem: cp.Problem,
|
868
|
+
w: cp.Variable,
|
869
|
+
factor: skt.Factor,
|
870
|
+
parameters_values: skt.ParametersValues = None,
|
871
|
+
expressions: dict[str, cp.Expression] | None = None,
|
872
|
+
) -> None:
|
873
|
+
"""Solve the CVXPY Problem and save the results in `weights_`, `problem_values_`
|
874
|
+
and `problem_`.
|
875
|
+
|
876
|
+
Parameters
|
877
|
+
----------
|
878
|
+
problem : cvxpy Problem
|
879
|
+
The CVXPY Problem.
|
880
|
+
|
881
|
+
w : cvxpy Variable
|
882
|
+
The CVXPY Variable representing assets weights.
|
883
|
+
|
884
|
+
expressions : dict[str, cvxpy Expression] | None, optional
|
885
|
+
Dictionary of CVXPY Expressions from which values are retrieved and saved
|
886
|
+
in `expression_values_`. It is used to save additional information about
|
887
|
+
the problem.
|
888
|
+
|
889
|
+
parameters_values: list[tuple[cvxpy Parameter, float | ndarray]], optional
|
890
|
+
A list of tuple of CVXPY Parameter and their values.
|
891
|
+
If The values are ndarray instead of float, the optimization is solved for
|
892
|
+
each element in the array.
|
893
|
+
|
894
|
+
factor: cvxpy Variable | cvxpy Constant
|
895
|
+
CVXPY Variable or Constant used for RatioMeasure optimization problems.
|
896
|
+
"""
|
897
|
+
if parameters_values is None:
|
898
|
+
parameters_values = []
|
899
|
+
|
900
|
+
if expressions is None:
|
901
|
+
expressions = {}
|
902
|
+
|
903
|
+
n_optimizations = 1
|
904
|
+
if len(parameters_values) != 0:
|
905
|
+
# If the parameter value is a list, each element is the parameter value of
|
906
|
+
# a distinct optimization. Therefore, each list must have same length.
|
907
|
+
sizes = [len(v) for p, v in parameters_values if not np.isscalar(v)]
|
908
|
+
if not np.all(sizes):
|
909
|
+
raise ValueError(
|
910
|
+
"All list elements from `parameters_values` should have same length"
|
911
|
+
)
|
912
|
+
if len(sizes) != 0:
|
913
|
+
n_optimizations = sizes[0]
|
914
|
+
# Scalar parameter values will be used in each optimization, therefore we
|
915
|
+
# transform them to a list.
|
916
|
+
parameters_values = [
|
917
|
+
(p, [v] * n_optimizations) if np.isscalar(v) else (p, v)
|
918
|
+
for p, v in parameters_values
|
919
|
+
]
|
920
|
+
|
921
|
+
solver_params = self.solver_params
|
922
|
+
if solver_params is None:
|
923
|
+
solver_params = {}
|
924
|
+
all_weights = []
|
925
|
+
all_problem_values = []
|
926
|
+
optimal = True
|
927
|
+
for i in range(n_optimizations):
|
928
|
+
for parameter, values in parameters_values:
|
929
|
+
parameter.value = values[i]
|
930
|
+
|
931
|
+
try:
|
932
|
+
# We suppress cvxpy warning as it is redundant with our warning
|
933
|
+
with warnings.catch_warnings():
|
934
|
+
warnings.simplefilter("ignore")
|
935
|
+
problem.solve(solver=self._solver, **solver_params)
|
936
|
+
|
937
|
+
if w.value is None:
|
938
|
+
raise cp.SolverError("No solution found")
|
939
|
+
|
940
|
+
weights = w.value / factor.value
|
941
|
+
problem_values = {
|
942
|
+
name: expression.value / factor.value
|
943
|
+
for name, expression in expressions.items()
|
944
|
+
}
|
945
|
+
problem_values["objective"] = (
|
946
|
+
problem.value / self._scale_objective.value
|
947
|
+
)
|
948
|
+
|
949
|
+
if (
|
950
|
+
self.risk_measure
|
951
|
+
in [RiskMeasure.VARIANCE, RiskMeasure.SEMI_VARIANCE]
|
952
|
+
and "risk" in problem_values
|
953
|
+
):
|
954
|
+
problem_values["risk"] /= factor.value
|
955
|
+
|
956
|
+
all_problem_values.append(problem_values)
|
957
|
+
all_weights.append(np.array(weights, dtype=float))
|
958
|
+
|
959
|
+
if problem.status != cp.OPTIMAL:
|
960
|
+
optimal = False
|
961
|
+
except (cp.SolverError, scl.ArpackNoConvergence):
|
962
|
+
params_string = " ".join(
|
963
|
+
[f"{p.value:0g}" for p in problem.parameters()]
|
964
|
+
)
|
965
|
+
if len(params_string) != 0:
|
966
|
+
params_string = f" with parameters {params_string}"
|
967
|
+
msg = (
|
968
|
+
f"Solver '{self._solver}' failed{params_string}. Try another"
|
969
|
+
" solver, or solve with solver_params=dict(verbose=True) for more"
|
970
|
+
" information"
|
971
|
+
)
|
972
|
+
if self.raise_on_failure:
|
973
|
+
raise cp.SolverError(msg) from None
|
974
|
+
else:
|
975
|
+
warnings.warn(msg, stacklevel=2)
|
976
|
+
|
977
|
+
if not optimal:
|
978
|
+
warnings.warn(
|
979
|
+
"Solution may be inaccurate. Try changing the solver params or the"
|
980
|
+
" scale. For more details, set `solver_params=dict(verbose=True)`",
|
981
|
+
stacklevel=2,
|
982
|
+
)
|
983
|
+
|
984
|
+
if n_optimizations == 1:
|
985
|
+
self.weights_ = all_weights[0]
|
986
|
+
self.problem_values_ = all_problem_values[0]
|
987
|
+
else:
|
988
|
+
self.weights_ = np.array(all_weights, dtype=float)
|
989
|
+
self.problem_values_ = all_problem_values
|
990
|
+
|
991
|
+
self.problem_ = problem
|
992
|
+
|
993
|
+
@cache_method("_cvx_cache")
|
994
|
+
def _cvx_mu_uncertainty_set(
|
995
|
+
self, mu_uncertainty_set: UncertaintySet, w: cp.Variable
|
996
|
+
) -> cp.Expression:
|
997
|
+
"""Uncertainty Set expression of expected returns.
|
998
|
+
|
999
|
+
Parameters
|
1000
|
+
----------
|
1001
|
+
mu_uncertainty_set : UncertaintySet
|
1002
|
+
The uncertainty set model of expected returns.
|
1003
|
+
|
1004
|
+
w : cvxpy Variable
|
1005
|
+
The CVXPY Variable representing assets weights.
|
1006
|
+
|
1007
|
+
Returns
|
1008
|
+
-------
|
1009
|
+
expression : cvxpy Expression
|
1010
|
+
The CVXPY Expression of the uncertainty set of expected returns.
|
1011
|
+
"""
|
1012
|
+
return mu_uncertainty_set.k * cp.pnorm(
|
1013
|
+
sc.linalg.sqrtm(mu_uncertainty_set.sigma) @ w, 2
|
1014
|
+
)
|
1015
|
+
|
1016
|
+
@cache_method("_cvx_cache")
|
1017
|
+
def _cvx_regularization(self, w: cp.Variable) -> cp.Expression:
|
1018
|
+
"""L1 and L2 regularization expression.
|
1019
|
+
|
1020
|
+
Parameters
|
1021
|
+
----------
|
1022
|
+
w : cvxpy Variable
|
1023
|
+
The CVXPY Variable representing assets weights.
|
1024
|
+
|
1025
|
+
Returns
|
1026
|
+
-------
|
1027
|
+
expression : cvxpy Expression
|
1028
|
+
The CVXPY Expression of L1 and L2 regularization.
|
1029
|
+
"""
|
1030
|
+
# Norm L1
|
1031
|
+
if self.l1_coef is None or self.l1_coef == 0:
|
1032
|
+
l1_reg = cp.Constant(0)
|
1033
|
+
else:
|
1034
|
+
l1_reg = cp.Constant(self.l1_coef) * cp.norm(w, 1)
|
1035
|
+
|
1036
|
+
# Norm L2
|
1037
|
+
if self.l2_coef is None or self.l2_coef == 0:
|
1038
|
+
l2_reg = cp.Constant(0)
|
1039
|
+
else:
|
1040
|
+
l2_reg = self.l2_coef * cp.sum_squares(w)
|
1041
|
+
regularization = l1_reg + l2_reg
|
1042
|
+
return regularization
|
1043
|
+
|
1044
|
+
@cache_method("_cvx_cache")
|
1045
|
+
def _cvx_transaction_cost(
|
1046
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1047
|
+
) -> cp.Expression:
|
1048
|
+
"""Transaction cost expression.
|
1049
|
+
|
1050
|
+
Parameters
|
1051
|
+
----------
|
1052
|
+
prior_model : PriorModel
|
1053
|
+
The prior model of the assets distributions.
|
1054
|
+
|
1055
|
+
w : cvxpy Variable
|
1056
|
+
The CVXPY Variable representing assets weights.
|
1057
|
+
|
1058
|
+
factor : cvxpy Variable | cvxpy Constant
|
1059
|
+
Additional variable used for the optimization of some objective function
|
1060
|
+
like the ratio maximization.
|
1061
|
+
|
1062
|
+
Returns
|
1063
|
+
-------
|
1064
|
+
expression : cvxpy Expression
|
1065
|
+
The CVXPY Expression of transaction cost.
|
1066
|
+
"""
|
1067
|
+
n_assets = prior_model.returns.shape[1]
|
1068
|
+
|
1069
|
+
transaction_costs = self._clean_input(
|
1070
|
+
self.transaction_costs,
|
1071
|
+
n_assets=n_assets,
|
1072
|
+
fill_value=0,
|
1073
|
+
name="transaction_costs",
|
1074
|
+
)
|
1075
|
+
if np.all(transaction_costs == 0):
|
1076
|
+
return cp.Constant(0)
|
1077
|
+
|
1078
|
+
previous_weights = self._clean_input(
|
1079
|
+
self.previous_weights,
|
1080
|
+
n_assets=n_assets,
|
1081
|
+
fill_value=0,
|
1082
|
+
name="previous_weights",
|
1083
|
+
)
|
1084
|
+
if np.isscalar(previous_weights):
|
1085
|
+
previous_weights *= np.ones(n_assets)
|
1086
|
+
|
1087
|
+
if np.isscalar(transaction_costs):
|
1088
|
+
return transaction_costs * cp.norm(previous_weights * factor - w, 1)
|
1089
|
+
return cp.norm(
|
1090
|
+
cp.multiply(transaction_costs, (previous_weights * factor - w)),
|
1091
|
+
1,
|
1092
|
+
)
|
1093
|
+
|
1094
|
+
@cache_method("_cvx_cache")
|
1095
|
+
def _cvx_management_fee(
|
1096
|
+
self, prior_model: PriorModel, w: cp.Variable
|
1097
|
+
) -> cp.Expression:
|
1098
|
+
"""Management fee expression.
|
1099
|
+
|
1100
|
+
Parameters
|
1101
|
+
----------
|
1102
|
+
prior_model : PriorModel
|
1103
|
+
The prior model of the assets distributions.
|
1104
|
+
|
1105
|
+
w : cvxpy Variable
|
1106
|
+
The CVXPY Variable representing assets weights.
|
1107
|
+
|
1108
|
+
Returns
|
1109
|
+
-------
|
1110
|
+
expression : cvxpy Expression
|
1111
|
+
The CVXPY Expression of management fee .
|
1112
|
+
"""
|
1113
|
+
n_assets = prior_model.returns.shape[1]
|
1114
|
+
|
1115
|
+
management_fees = self._clean_input(
|
1116
|
+
self.management_fees,
|
1117
|
+
n_assets=n_assets,
|
1118
|
+
fill_value=0,
|
1119
|
+
name="management_fees",
|
1120
|
+
)
|
1121
|
+
if np.all(management_fees == 0):
|
1122
|
+
return cp.Constant(0)
|
1123
|
+
|
1124
|
+
if np.isscalar(management_fees):
|
1125
|
+
management_fees *= np.ones(n_assets)
|
1126
|
+
return management_fees @ w
|
1127
|
+
|
1128
|
+
@cache_method("_cvx_cache")
|
1129
|
+
def _cvx_returns(self, prior_model: PriorModel, w: cp.Variable) -> cp.Expression:
|
1130
|
+
"""Expression of the portfolio returns series.
|
1131
|
+
|
1132
|
+
Parameters
|
1133
|
+
----------
|
1134
|
+
prior_model : PriorModel
|
1135
|
+
The prior model of the assets distributions.
|
1136
|
+
|
1137
|
+
w : cvxpy Variable
|
1138
|
+
The CVXPY Variable representing assets weights.
|
1139
|
+
|
1140
|
+
Returns
|
1141
|
+
-------
|
1142
|
+
expression : cvxpy Expression
|
1143
|
+
The CVXPY Expression the portfolio returns series.
|
1144
|
+
"""
|
1145
|
+
returns = prior_model.returns @ w
|
1146
|
+
return returns
|
1147
|
+
|
1148
|
+
@cache_method("_cvx_cache")
|
1149
|
+
def _turnover(
|
1150
|
+
self, n_assets: int, w: cp.Variable, factor: skt.Factor
|
1151
|
+
) -> cp.Expression:
|
1152
|
+
"""Expression of the portfolio turnover.
|
1153
|
+
|
1154
|
+
Parameters
|
1155
|
+
----------
|
1156
|
+
n_assets : int
|
1157
|
+
The number of assets.
|
1158
|
+
|
1159
|
+
w : cvxpy Variable
|
1160
|
+
The CVXPY Variable representing assets weights.
|
1161
|
+
|
1162
|
+
factor : cvxpy Variable | cvxpy Constant
|
1163
|
+
Additional variable used for the optimization of some objective function
|
1164
|
+
like the ratio maximization.
|
1165
|
+
|
1166
|
+
Returns
|
1167
|
+
-------
|
1168
|
+
expression : cvxpy Expression
|
1169
|
+
The CVXPY Expression the portfolio turnover.
|
1170
|
+
"""
|
1171
|
+
if self.previous_weights is None:
|
1172
|
+
raise ValueError(
|
1173
|
+
"If you provide `max_turnover`, you must also provide "
|
1174
|
+
" `previous_weights`"
|
1175
|
+
)
|
1176
|
+
previous_weights = self._clean_input(
|
1177
|
+
self.previous_weights,
|
1178
|
+
n_assets=n_assets,
|
1179
|
+
fill_value=0,
|
1180
|
+
name="previous_weights",
|
1181
|
+
)
|
1182
|
+
if np.isscalar(previous_weights):
|
1183
|
+
previous_weights *= np.ones(n_assets)
|
1184
|
+
turnover = cp.abs(w - previous_weights * factor)
|
1185
|
+
return turnover
|
1186
|
+
|
1187
|
+
@cache_method("_cvx_cache")
|
1188
|
+
def _cvx_min_acceptable_return(
|
1189
|
+
self,
|
1190
|
+
prior_model: PriorModel,
|
1191
|
+
w: cp.Variable,
|
1192
|
+
min_acceptable_return: skt.Target = None,
|
1193
|
+
) -> cp.Expression:
|
1194
|
+
"""Expression of the portfolio Minimum Acceptable Returns.
|
1195
|
+
|
1196
|
+
Parameters
|
1197
|
+
----------
|
1198
|
+
prior_model : PriorModel
|
1199
|
+
The prior model of the assets distributions..
|
1200
|
+
|
1201
|
+
w : cvxpy Variable
|
1202
|
+
The CVXPY Variable representing assets weights.
|
1203
|
+
|
1204
|
+
min_acceptable_return : float | ndarray of shape (n_assets,)
|
1205
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
1206
|
+
returns for the computation of lower partial moments.
|
1207
|
+
|
1208
|
+
Returns
|
1209
|
+
-------
|
1210
|
+
expression : cvxpy Expression
|
1211
|
+
The CVXPY Expression the portfolio Minimum Acceptable Returns.
|
1212
|
+
"""
|
1213
|
+
if min_acceptable_return is None:
|
1214
|
+
min_acceptable_return = prior_model.mu
|
1215
|
+
if not np.isscalar(min_acceptable_return) and min_acceptable_return.shape != (
|
1216
|
+
len(min_acceptable_return),
|
1217
|
+
1,
|
1218
|
+
):
|
1219
|
+
min_acceptable_return = min_acceptable_return[np.newaxis, :]
|
1220
|
+
mar = (prior_model.returns - min_acceptable_return) @ w
|
1221
|
+
return mar
|
1222
|
+
|
1223
|
+
@cache_method("_cvx_cache")
|
1224
|
+
def __cvx_drawdown(
|
1225
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1226
|
+
) -> tuple[cp.Variable, list[cp.Expression]]:
|
1227
|
+
"""Expression of the portfolio drawdown.
|
1228
|
+
|
1229
|
+
Parameters
|
1230
|
+
----------
|
1231
|
+
prior_model : PriorModel
|
1232
|
+
The prior model of the assets distributions.
|
1233
|
+
|
1234
|
+
w : cvxpy Variable
|
1235
|
+
The CVXPY Variable representing assets weights.
|
1236
|
+
|
1237
|
+
factor : cvxpy Variable | cvxpy Constant
|
1238
|
+
Additional variable used for the optimization of some objective function
|
1239
|
+
like the ratio maximization.
|
1240
|
+
|
1241
|
+
Returns
|
1242
|
+
-------
|
1243
|
+
expression : cvxpy Expression
|
1244
|
+
The CVXPY Expression the portfolio drawdown.
|
1245
|
+
"""
|
1246
|
+
n_observations = prior_model.returns.shape[0]
|
1247
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1248
|
+
ptf_transaction_cost = self._cvx_transaction_cost(
|
1249
|
+
prior_model=prior_model, w=w, factor=factor
|
1250
|
+
)
|
1251
|
+
ptf_management_fee = self._cvx_management_fee(prior_model=prior_model, w=w)
|
1252
|
+
v = cp.Variable(n_observations + 1)
|
1253
|
+
constraints = [
|
1254
|
+
v[1:] * self._scale_constraints
|
1255
|
+
>= v[:-1] * self._scale_constraints
|
1256
|
+
- ptf_returns * self._scale_constraints
|
1257
|
+
+ ptf_transaction_cost * self._scale_constraints
|
1258
|
+
+ ptf_management_fee * self._scale_constraints,
|
1259
|
+
v[1:] * self._scale_constraints >= 0,
|
1260
|
+
v[0] * self._scale_constraints == 0,
|
1261
|
+
]
|
1262
|
+
return v, constraints
|
1263
|
+
|
1264
|
+
def _cvx_drawdown(
|
1265
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1266
|
+
) -> tuple[cp.Variable, list[cp.Expression]]:
|
1267
|
+
"""Expression of the portfolio drawdown.
|
1268
|
+
Wrapper around __cvx_drawdown to avoid re-adding the constraints when they
|
1269
|
+
have already been included in the problem.
|
1270
|
+
|
1271
|
+
Parameters
|
1272
|
+
----------
|
1273
|
+
prior_model : PriorModel
|
1274
|
+
The prior model of the assets distributions.
|
1275
|
+
|
1276
|
+
w : cvxpy Variable
|
1277
|
+
The CVXPY Variable representing assets weights.
|
1278
|
+
|
1279
|
+
factor : cvxpy Variable | cvxpy Constant
|
1280
|
+
Additional variable used for the optimization of some objective function
|
1281
|
+
like the ratio maximization.
|
1282
|
+
|
1283
|
+
Returns
|
1284
|
+
-------
|
1285
|
+
expression : cvxpy Expression
|
1286
|
+
The CVXPY Expression the portfolio drawdown.
|
1287
|
+
"""
|
1288
|
+
if "__cvx_drawdown" in self._cvx_cache:
|
1289
|
+
v, _ = self.__cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1290
|
+
return v, []
|
1291
|
+
return self.__cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1292
|
+
|
1293
|
+
def _tracking_error(
|
1294
|
+
self, prior_model: PriorModel, w: cp.Variable, y: np.ndarray, factor: skt.Factor
|
1295
|
+
) -> cp.Expression:
|
1296
|
+
"""Expression of the portfolio tracking error.
|
1297
|
+
|
1298
|
+
Parameters
|
1299
|
+
----------
|
1300
|
+
prior_model : PriorModel
|
1301
|
+
The prior model of the assets distributions.
|
1302
|
+
|
1303
|
+
w : cvxpy Variable
|
1304
|
+
The CVXPY Variable representing assets weights.
|
1305
|
+
|
1306
|
+
y : ndarray of shape (n_observations,)
|
1307
|
+
Benchmark for the tracking error computation.
|
1308
|
+
|
1309
|
+
factor : cvxpy Variable | cvxpy Constant
|
1310
|
+
Additional variable used for the optimization of some objective function
|
1311
|
+
like the ratio maximization.
|
1312
|
+
|
1313
|
+
Returns
|
1314
|
+
-------
|
1315
|
+
expression : cvxpy Expression
|
1316
|
+
The CVXPY Expression the portfolio tracking error.
|
1317
|
+
"""
|
1318
|
+
n_observations = prior_model.returns.shape[0]
|
1319
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1320
|
+
tracking_error = cp.norm(ptf_returns - y * factor, "fro") / cp.sqrt(
|
1321
|
+
n_observations - 1
|
1322
|
+
)
|
1323
|
+
return tracking_error
|
1324
|
+
|
1325
|
+
# Risk Measures risk models
|
1326
|
+
# They need to be named f'_{risk_measure}_risk' as they are loaded dynamically in
|
1327
|
+
# mean_risk_optimization()
|
1328
|
+
def _mean_absolute_deviation_risk(
|
1329
|
+
self, prior_model: PriorModel, w: cp.Variable, min_acceptable_return: skt.Target
|
1330
|
+
) -> skt.RiskResult:
|
1331
|
+
"""Expression and Constraints of the Mean Absolute Deviation risk measure.
|
1332
|
+
|
1333
|
+
Parameters
|
1334
|
+
----------
|
1335
|
+
prior_model : PriorModel
|
1336
|
+
The prior model of the assets distributions.
|
1337
|
+
|
1338
|
+
w : cvxpy Variable
|
1339
|
+
The CVXPY Variable representing assets weights.
|
1340
|
+
|
1341
|
+
min_acceptable_return : float | ndarray of shape (n_assets,)
|
1342
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
1343
|
+
returns for the computation of lower partial moments.
|
1344
|
+
|
1345
|
+
Returns
|
1346
|
+
-------
|
1347
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1348
|
+
CVXPY Expression and Constraints of the Mean Absolute Deviation risk
|
1349
|
+
measure.
|
1350
|
+
"""
|
1351
|
+
n_observations = prior_model.returns.shape[0]
|
1352
|
+
ptf_min_acceptable_return = self._cvx_min_acceptable_return(
|
1353
|
+
prior_model=prior_model, w=w, min_acceptable_return=min_acceptable_return
|
1354
|
+
)
|
1355
|
+
v = cp.Variable(n_observations, nonneg=True)
|
1356
|
+
risk = 2 * cp.sum(v) / n_observations
|
1357
|
+
constraints = [
|
1358
|
+
ptf_min_acceptable_return * self._scale_constraints
|
1359
|
+
>= -v * self._scale_constraints
|
1360
|
+
]
|
1361
|
+
return risk, constraints
|
1362
|
+
|
1363
|
+
def _first_lower_partial_moment_risk(
|
1364
|
+
self,
|
1365
|
+
prior_model: PriorModel,
|
1366
|
+
w: cp.Variable,
|
1367
|
+
min_acceptable_return: skt.Target,
|
1368
|
+
factor: skt.Factor,
|
1369
|
+
) -> skt.RiskResult:
|
1370
|
+
"""Expression and Constraints of the First Lower Partial Moment risk measure.
|
1371
|
+
|
1372
|
+
Parameters
|
1373
|
+
----------
|
1374
|
+
prior_model : PriorModel
|
1375
|
+
The prior model of the assets distributions.
|
1376
|
+
|
1377
|
+
w : cvxpy Variable
|
1378
|
+
The CVXPY Variable representing assets weights.
|
1379
|
+
|
1380
|
+
min_acceptable_return : float | ndarray of shape (n_assets,)
|
1381
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
1382
|
+
returns for the computation of lower partial moments.
|
1383
|
+
|
1384
|
+
factor : cvxpy Variable | cvxpy Constant
|
1385
|
+
Additional variable used for the optimization of some objective function
|
1386
|
+
like the ratio maximization.
|
1387
|
+
|
1388
|
+
Returns
|
1389
|
+
-------
|
1390
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1391
|
+
CVXPY Expression and Constraints of the First Lower Partial Moment risk
|
1392
|
+
measure.
|
1393
|
+
"""
|
1394
|
+
n_observations = prior_model.returns.shape[0]
|
1395
|
+
ptf_min_acceptable_return = self._cvx_min_acceptable_return(
|
1396
|
+
prior_model=prior_model, w=w, min_acceptable_return=min_acceptable_return
|
1397
|
+
)
|
1398
|
+
v = cp.Variable(n_observations, nonneg=True)
|
1399
|
+
risk = cp.sum(v) / n_observations
|
1400
|
+
constraints = [
|
1401
|
+
self.risk_free_rate * factor * self._scale_constraints
|
1402
|
+
- ptf_min_acceptable_return * self._scale_constraints
|
1403
|
+
<= v * self._scale_constraints
|
1404
|
+
]
|
1405
|
+
return risk, constraints
|
1406
|
+
|
1407
|
+
def _standard_deviation_risk(
|
1408
|
+
self, prior_model: PriorModel, w: cp.Variable
|
1409
|
+
) -> skt.RiskResult:
|
1410
|
+
"""Expression and Constraints of the Standard Deviation risk measure.
|
1411
|
+
|
1412
|
+
Parameters
|
1413
|
+
----------
|
1414
|
+
prior_model : PriorModel
|
1415
|
+
The prior model of the assets distributions.
|
1416
|
+
|
1417
|
+
w : cvxpy Variable
|
1418
|
+
The CVXPY Variable representing assets weights.
|
1419
|
+
|
1420
|
+
Returns
|
1421
|
+
-------
|
1422
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1423
|
+
CVXPY Expression and Constraints of the Standard Deviation risk measure.
|
1424
|
+
"""
|
1425
|
+
v = cp.Variable(
|
1426
|
+
nonneg=True
|
1427
|
+
) # nonneg=True instead of constraint v>=0 is preferred for better DCP analysis
|
1428
|
+
if prior_model.cholesky is not None:
|
1429
|
+
z = prior_model.cholesky
|
1430
|
+
else:
|
1431
|
+
z = np.linalg.cholesky(prior_model.covariance)
|
1432
|
+
risk = v
|
1433
|
+
constraints = [
|
1434
|
+
cp.SOC(v * self._scale_constraints, z.T @ w * self._scale_constraints)
|
1435
|
+
]
|
1436
|
+
return risk, constraints
|
1437
|
+
|
1438
|
+
def _variance_risk(self, prior_model: PriorModel, w: cp.Variable) -> skt.RiskResult:
|
1439
|
+
"""Expression and Constraints of the Variance risk measure.
|
1440
|
+
|
1441
|
+
Parameters
|
1442
|
+
----------
|
1443
|
+
prior_model : PriorModel
|
1444
|
+
The prior model of the assets distributions.
|
1445
|
+
|
1446
|
+
w : cvxpy Variable
|
1447
|
+
The CVXPY Variable representing assets weights.
|
1448
|
+
|
1449
|
+
Returns
|
1450
|
+
-------
|
1451
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1452
|
+
CVXPY Expression and Constraints the Variance risk measure.
|
1453
|
+
"""
|
1454
|
+
risk, constraints = self._standard_deviation_risk(prior_model=prior_model, w=w)
|
1455
|
+
risk = cp.square(risk)
|
1456
|
+
return risk, constraints
|
1457
|
+
|
1458
|
+
def _worst_case_variance_risk(
|
1459
|
+
self,
|
1460
|
+
prior_model: PriorModel,
|
1461
|
+
covariance_uncertainty_set: UncertaintySet,
|
1462
|
+
w: cp.Variable,
|
1463
|
+
factor: skt.Factor,
|
1464
|
+
) -> skt.RiskResult:
|
1465
|
+
"""Expression and Constraints of the Worst Case Variance.
|
1466
|
+
|
1467
|
+
Parameters
|
1468
|
+
----------
|
1469
|
+
prior_model : PriorModel
|
1470
|
+
The prior model of the assets distributions.
|
1471
|
+
|
1472
|
+
covariance_uncertainty_set : UncertaintySet
|
1473
|
+
:ref:`Covariance Uncertainty set estimator <uncertainty_set_estimator>`.
|
1474
|
+
|
1475
|
+
w : cvxpy Variable
|
1476
|
+
The CVXPY Variable representing assets weights.
|
1477
|
+
|
1478
|
+
factor : cvxpy Variable | cvxpy Constant
|
1479
|
+
Additional variable used for the optimization of some objective function
|
1480
|
+
like the ratio maximization.
|
1481
|
+
|
1482
|
+
Returns
|
1483
|
+
-------
|
1484
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1485
|
+
CVXPY Expression and Constraints the Worst Case Variance.
|
1486
|
+
"""
|
1487
|
+
n_assets = prior_model.returns.shape[1]
|
1488
|
+
x = cp.Variable((n_assets, n_assets), symmetric=True)
|
1489
|
+
y = cp.Variable((n_assets, n_assets), symmetric=True)
|
1490
|
+
w_reshaped = cp.reshape(w, (n_assets, 1))
|
1491
|
+
factor_reshaped = cp.reshape(factor, (1, 1))
|
1492
|
+
z1 = cp.vstack([x, w_reshaped.T])
|
1493
|
+
z2 = cp.vstack([w_reshaped, factor_reshaped])
|
1494
|
+
|
1495
|
+
risk = covariance_uncertainty_set.k * cp.pnorm(
|
1496
|
+
sc.linalg.sqrtm(covariance_uncertainty_set.sigma) @ (cp.vec(x) + cp.vec(y)),
|
1497
|
+
2,
|
1498
|
+
) + cp.trace(prior_model.covariance @ (x + y))
|
1499
|
+
# semi-definite positive constraints
|
1500
|
+
# noinspection PyTypeChecker
|
1501
|
+
constraints = [
|
1502
|
+
cp.hstack([z1, z2]) * self._scale_constraints >> 0,
|
1503
|
+
y * self._scale_constraints >> 0,
|
1504
|
+
]
|
1505
|
+
return risk, constraints
|
1506
|
+
|
1507
|
+
def _semi_variance_risk(
|
1508
|
+
self,
|
1509
|
+
prior_model: PriorModel,
|
1510
|
+
w: cp.Variable,
|
1511
|
+
min_acceptable_return: skt.Target = None,
|
1512
|
+
) -> skt.RiskResult:
|
1513
|
+
"""Expression and Constraints of the Semi Variance risk measure.
|
1514
|
+
|
1515
|
+
Parameters
|
1516
|
+
----------
|
1517
|
+
prior_model : PriorModel
|
1518
|
+
The prior model of the assets distributions.
|
1519
|
+
|
1520
|
+
w : cvxpy Variable
|
1521
|
+
The CVXPY Variable representing assets weights.
|
1522
|
+
|
1523
|
+
min_acceptable_return : float | ndarray of shape (n_assets,)
|
1524
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
1525
|
+
returns for the computation of lower partial moments.
|
1526
|
+
|
1527
|
+
Returns
|
1528
|
+
-------
|
1529
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1530
|
+
CVXPY Expression and Constraints the Semi Variance risk measure.
|
1531
|
+
"""
|
1532
|
+
n_observations = prior_model.returns.shape[0]
|
1533
|
+
ptf_min_acceptable_return = self._cvx_min_acceptable_return(
|
1534
|
+
prior_model=prior_model, w=w, min_acceptable_return=min_acceptable_return
|
1535
|
+
)
|
1536
|
+
v = cp.Variable(n_observations, nonneg=True)
|
1537
|
+
risk = cp.sum_squares(v) / (n_observations - 1)
|
1538
|
+
constraints = [
|
1539
|
+
ptf_min_acceptable_return * self._scale_constraints
|
1540
|
+
>= -v * self._scale_constraints
|
1541
|
+
]
|
1542
|
+
return risk, constraints
|
1543
|
+
|
1544
|
+
def _semi_deviation_risk(
|
1545
|
+
self,
|
1546
|
+
prior_model: PriorModel,
|
1547
|
+
w: cp.Variable,
|
1548
|
+
min_acceptable_return: skt.Target = None,
|
1549
|
+
) -> skt.RiskResult:
|
1550
|
+
"""Expression and Constraints of the Semi Standard Deviation risk measure.
|
1551
|
+
|
1552
|
+
Parameters
|
1553
|
+
----------
|
1554
|
+
prior_model : PriorModel
|
1555
|
+
The prior model of the assets distributions.
|
1556
|
+
|
1557
|
+
w : cvxpy Variable
|
1558
|
+
The CVXPY Variable representing assets weights.
|
1559
|
+
|
1560
|
+
min_acceptable_return : float | ndarray of shape (n_assets,)
|
1561
|
+
The minimum acceptable return used to distinguish "downside" and "upside"
|
1562
|
+
returns for the computation of lower partial moments.
|
1563
|
+
|
1564
|
+
Returns
|
1565
|
+
-------
|
1566
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1567
|
+
CVXPY Expression and Constraints the Semi Standard Deviation risk measure.
|
1568
|
+
"""
|
1569
|
+
n_observations = prior_model.returns.shape[0]
|
1570
|
+
ptf_min_acceptable_return = self._cvx_min_acceptable_return(
|
1571
|
+
prior_model=prior_model, w=w, min_acceptable_return=min_acceptable_return
|
1572
|
+
)
|
1573
|
+
v = cp.Variable(n_observations, nonneg=True)
|
1574
|
+
risk = cp.norm(v, 2) / np.sqrt(n_observations - 1)
|
1575
|
+
constraints = [
|
1576
|
+
ptf_min_acceptable_return * self._scale_constraints
|
1577
|
+
>= -v * self._scale_constraints
|
1578
|
+
]
|
1579
|
+
return risk, constraints
|
1580
|
+
|
1581
|
+
def _fourth_central_moment_risk(self, w: cp.Variable, factor: skt.Factor):
|
1582
|
+
raise NotImplementedError
|
1583
|
+
|
1584
|
+
def _fourth_lower_partial_moment_risk(self, w: cp.Variable, factor: skt.Factor):
|
1585
|
+
raise NotImplementedError
|
1586
|
+
|
1587
|
+
def _worst_realization_risk(
|
1588
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1589
|
+
) -> skt.RiskResult:
|
1590
|
+
"""Expression and Constraints of the Worst Realization risk measure.
|
1591
|
+
|
1592
|
+
Parameters
|
1593
|
+
----------
|
1594
|
+
prior_model : PriorModel
|
1595
|
+
The prior model of the assets distributions.
|
1596
|
+
|
1597
|
+
w : cvxpy Variable
|
1598
|
+
The CVXPY Variable representing assets weights.
|
1599
|
+
|
1600
|
+
factor : cvxpy Variable | cvxpy Constant
|
1601
|
+
Additional variable used for the optimization of some objective function
|
1602
|
+
like the ratio maximization.
|
1603
|
+
|
1604
|
+
Returns
|
1605
|
+
-------
|
1606
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1607
|
+
CVXPY Expression and Constraints the Worst Realization risk measure.
|
1608
|
+
"""
|
1609
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1610
|
+
ptf_transaction_cost = self._cvx_transaction_cost(
|
1611
|
+
prior_model=prior_model, w=w, factor=factor
|
1612
|
+
)
|
1613
|
+
ptf_management_fee = self._cvx_management_fee(prior_model=prior_model, w=w)
|
1614
|
+
v = cp.Variable()
|
1615
|
+
risk = v
|
1616
|
+
constraints = [
|
1617
|
+
-ptf_returns * self._scale_constraints
|
1618
|
+
+ ptf_transaction_cost * self._scale_constraints
|
1619
|
+
+ ptf_management_fee * self._scale_constraints
|
1620
|
+
<= v * self._scale_constraints
|
1621
|
+
]
|
1622
|
+
return risk, constraints
|
1623
|
+
|
1624
|
+
def _cvar_risk(
|
1625
|
+
self,
|
1626
|
+
prior_model: PriorModel,
|
1627
|
+
w: cp.Variable,
|
1628
|
+
factor: skt.Factor,
|
1629
|
+
) -> skt.RiskResult:
|
1630
|
+
"""Expression and Constraints of the CVaR risk measure.
|
1631
|
+
|
1632
|
+
Parameters
|
1633
|
+
----------
|
1634
|
+
prior_model : PriorModel
|
1635
|
+
The prior model of the assets distributions.
|
1636
|
+
|
1637
|
+
w : cvxpy Variable
|
1638
|
+
The CVXPY Variable representing assets weights.
|
1639
|
+
|
1640
|
+
factor : cvxpy Variable | cvxpy Constant
|
1641
|
+
Additional variable used for the optimization of some objective function
|
1642
|
+
like the ratio maximization.
|
1643
|
+
|
1644
|
+
Returns
|
1645
|
+
-------
|
1646
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1647
|
+
CVXPY Expression and Constraints the CVaR risk measure.
|
1648
|
+
"""
|
1649
|
+
n_observations = prior_model.returns.shape[0]
|
1650
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1651
|
+
ptf_transaction_cost = self._cvx_transaction_cost(
|
1652
|
+
prior_model=prior_model, w=w, factor=factor
|
1653
|
+
)
|
1654
|
+
ptf_management_fee = self._cvx_management_fee(prior_model=prior_model, w=w)
|
1655
|
+
alpha = cp.Variable()
|
1656
|
+
v = cp.Variable(n_observations, nonneg=True)
|
1657
|
+
risk = alpha + 1.0 / (n_observations * (1 - self.cvar_beta)) * cp.sum(v)
|
1658
|
+
# noinspection PyTypeChecker
|
1659
|
+
constraints = [
|
1660
|
+
ptf_returns * self._scale_constraints
|
1661
|
+
- ptf_transaction_cost * self._scale_constraints
|
1662
|
+
- ptf_management_fee * self._scale_constraints
|
1663
|
+
+ alpha * self._scale_constraints
|
1664
|
+
+ v * self._scale_constraints
|
1665
|
+
>= 0
|
1666
|
+
]
|
1667
|
+
return risk, constraints
|
1668
|
+
|
1669
|
+
def _evar_risk(
|
1670
|
+
self,
|
1671
|
+
prior_model: PriorModel,
|
1672
|
+
w: cp.Variable,
|
1673
|
+
factor: skt.Factor,
|
1674
|
+
) -> skt.RiskResult:
|
1675
|
+
"""Expression and Constraints of the EVaR risk measure.
|
1676
|
+
|
1677
|
+
Parameters
|
1678
|
+
----------
|
1679
|
+
prior_model : PriorModel
|
1680
|
+
The prior model of the assets distributions.
|
1681
|
+
|
1682
|
+
w : cvxpy Variable
|
1683
|
+
The CVXPY Variable representing assets weights.
|
1684
|
+
|
1685
|
+
factor : cvxpy Variable | cvxpy Constant
|
1686
|
+
Additional variable used for the optimization of some objective function
|
1687
|
+
like the ratio maximization.
|
1688
|
+
|
1689
|
+
Returns
|
1690
|
+
-------
|
1691
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1692
|
+
CVXPY Expression and Constraints the EVaR risk measure.
|
1693
|
+
"""
|
1694
|
+
n_observations = prior_model.returns.shape[0]
|
1695
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1696
|
+
ptf_transaction_cost = self._cvx_transaction_cost(
|
1697
|
+
prior_model=prior_model, w=w, factor=factor
|
1698
|
+
)
|
1699
|
+
ptf_management_fee = self._cvx_management_fee(prior_model=prior_model, w=w)
|
1700
|
+
# We don't include the transaction_cost in the constraint otherwise the problem
|
1701
|
+
# is not DCP
|
1702
|
+
if not isinstance(ptf_transaction_cost, cp.Constant):
|
1703
|
+
warnings.warn(
|
1704
|
+
"The EVaR problem will be relaxed by removing the transaction costs"
|
1705
|
+
" from the Cone constraint to keep the problem DCP. The solution may"
|
1706
|
+
" not be accurate.",
|
1707
|
+
stacklevel=2,
|
1708
|
+
)
|
1709
|
+
|
1710
|
+
x = cp.Variable()
|
1711
|
+
y = cp.Variable(nonneg=True)
|
1712
|
+
z = cp.Variable(n_observations)
|
1713
|
+
risk = x + y * np.log(1 / (n_observations * (1 - self.evar_beta)))
|
1714
|
+
constraints = [
|
1715
|
+
cp.sum(z) * self._scale_constraints <= y * self._scale_constraints,
|
1716
|
+
cp.constraints.ExpCone(
|
1717
|
+
-ptf_returns * self._scale_constraints
|
1718
|
+
+ ptf_management_fee * self._scale_constraints
|
1719
|
+
- x * self._scale_constraints,
|
1720
|
+
np.ones(n_observations) * y * self._scale_constraints,
|
1721
|
+
z * self._scale_constraints,
|
1722
|
+
),
|
1723
|
+
]
|
1724
|
+
return risk, constraints
|
1725
|
+
|
1726
|
+
def _max_drawdown_risk(
|
1727
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1728
|
+
) -> skt.RiskResult:
|
1729
|
+
"""Expression and Constraints of the EVaR risk measure.
|
1730
|
+
|
1731
|
+
Parameters
|
1732
|
+
----------
|
1733
|
+
prior_model : PriorModel
|
1734
|
+
The prior model of the assets distributions.
|
1735
|
+
|
1736
|
+
w : cvxpy Variable
|
1737
|
+
The CVXPY Variable representing assets weights.
|
1738
|
+
|
1739
|
+
factor : cvxpy Variable | cvxpy Constant
|
1740
|
+
Additional variable used for the optimization of some objective function
|
1741
|
+
like the ratio maximization.
|
1742
|
+
|
1743
|
+
Returns
|
1744
|
+
-------
|
1745
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1746
|
+
CVXPY Expression and Constraints the EVaR risk measure.
|
1747
|
+
"""
|
1748
|
+
v, constraints = self._cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1749
|
+
u = cp.Variable()
|
1750
|
+
risk = u
|
1751
|
+
constraints += [u * self._scale_constraints >= v[1:] * self._scale_constraints]
|
1752
|
+
return risk, constraints
|
1753
|
+
|
1754
|
+
def _average_drawdown_risk(
|
1755
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1756
|
+
) -> skt.RiskResult:
|
1757
|
+
"""Expression and Constraints of the Average Drawdown risk measure.
|
1758
|
+
|
1759
|
+
Parameters
|
1760
|
+
----------
|
1761
|
+
prior_model : PriorModel
|
1762
|
+
The prior model of the assets distributions.
|
1763
|
+
|
1764
|
+
w : cvxpy Variable
|
1765
|
+
The CVXPY Variable representing assets weights.
|
1766
|
+
|
1767
|
+
factor : cvxpy Variable | cvxpy Constant
|
1768
|
+
Additional variable used for the optimization of some objective function
|
1769
|
+
like the ratio maximization.
|
1770
|
+
|
1771
|
+
Returns
|
1772
|
+
-------
|
1773
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1774
|
+
CVXPY Expression and Constraints the Average Drawdown risk measure.
|
1775
|
+
"""
|
1776
|
+
n_observations = prior_model.returns.shape[0]
|
1777
|
+
v, constraints = self._cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1778
|
+
risk = cp.sum(v[1:]) / n_observations
|
1779
|
+
return risk, constraints
|
1780
|
+
|
1781
|
+
def _cdar_risk(
|
1782
|
+
self,
|
1783
|
+
prior_model: PriorModel,
|
1784
|
+
w: cp.Variable,
|
1785
|
+
factor: skt.Factor,
|
1786
|
+
) -> skt.RiskResult:
|
1787
|
+
"""Expression and Constraints of the CDaR risk measure.
|
1788
|
+
|
1789
|
+
Parameters
|
1790
|
+
----------
|
1791
|
+
prior_model : PriorModel
|
1792
|
+
The prior model of the assets distributions.
|
1793
|
+
|
1794
|
+
w : cvxpy Variable
|
1795
|
+
The CVXPY Variable representing assets weights.
|
1796
|
+
|
1797
|
+
factor : cvxpy Variable | cvxpy Constant
|
1798
|
+
Additional variable used for the optimization of some objective function
|
1799
|
+
like the ratio maximization.
|
1800
|
+
|
1801
|
+
Returns
|
1802
|
+
-------
|
1803
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1804
|
+
CVXPY Expression and Constraints the CDaR risk measure.
|
1805
|
+
"""
|
1806
|
+
n_observations = prior_model.returns.shape[0]
|
1807
|
+
v, constraints = self._cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1808
|
+
alpha = cp.Variable()
|
1809
|
+
z = cp.Variable(n_observations, nonneg=True)
|
1810
|
+
risk = alpha + 1.0 / (n_observations * (1 - self.cdar_beta)) * cp.sum(z)
|
1811
|
+
constraints += [
|
1812
|
+
z * self._scale_constraints
|
1813
|
+
>= v[1:] * self._scale_constraints - alpha * self._scale_constraints
|
1814
|
+
]
|
1815
|
+
return risk, constraints
|
1816
|
+
|
1817
|
+
def _edar_risk(
|
1818
|
+
self,
|
1819
|
+
prior_model: PriorModel,
|
1820
|
+
w: cp.Variable,
|
1821
|
+
factor: skt.Factor,
|
1822
|
+
) -> skt.RiskResult:
|
1823
|
+
"""Expression and Constraints of the EDaR risk measure.
|
1824
|
+
|
1825
|
+
Parameters
|
1826
|
+
----------
|
1827
|
+
prior_model : PriorModel
|
1828
|
+
The prior model of the assets distributions.
|
1829
|
+
|
1830
|
+
w : cvxpy Variable
|
1831
|
+
The CVXPY Variable representing assets weights.
|
1832
|
+
|
1833
|
+
factor : cvxpy Variable | cvxpy Constant
|
1834
|
+
Additional variable used for the optimization of some objective function
|
1835
|
+
like the ratio maximization.
|
1836
|
+
|
1837
|
+
Returns
|
1838
|
+
-------
|
1839
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1840
|
+
CVXPY Expression and Constraints the EDaR risk measure.
|
1841
|
+
"""
|
1842
|
+
n_observations = prior_model.returns.shape[0]
|
1843
|
+
v, constraints = self._cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1844
|
+
x = cp.Variable()
|
1845
|
+
y = cp.Variable(nonneg=True)
|
1846
|
+
z = cp.Variable(n_observations)
|
1847
|
+
risk = x + y * np.log(1 / (n_observations * (1 - self.edar_beta)))
|
1848
|
+
constraints += [
|
1849
|
+
cp.sum(z) * self._scale_constraints <= y * self._scale_constraints,
|
1850
|
+
cp.constraints.ExpCone(
|
1851
|
+
v[1:] * self._scale_constraints - x * self._scale_constraints,
|
1852
|
+
np.ones(n_observations) * y * self._scale_constraints,
|
1853
|
+
z * self._scale_constraints,
|
1854
|
+
),
|
1855
|
+
]
|
1856
|
+
return risk, constraints
|
1857
|
+
|
1858
|
+
def _ulcer_index_risk(
|
1859
|
+
self,
|
1860
|
+
prior_model: PriorModel,
|
1861
|
+
w: cp.Variable,
|
1862
|
+
factor: skt.Factor,
|
1863
|
+
) -> skt.RiskResult:
|
1864
|
+
"""Expression and Constraints of the Ulcer Index risk measure.
|
1865
|
+
|
1866
|
+
Parameters
|
1867
|
+
----------
|
1868
|
+
prior_model : PriorModel
|
1869
|
+
The prior model of the assets distributions.
|
1870
|
+
|
1871
|
+
w : cvxpy Variable
|
1872
|
+
The CVXPY Variable representing assets weights.
|
1873
|
+
|
1874
|
+
factor : cvxpy Variable | cvxpy Constant
|
1875
|
+
Additional variable used for the optimization of some objective function
|
1876
|
+
like the ratio maximization.
|
1877
|
+
|
1878
|
+
Returns
|
1879
|
+
-------
|
1880
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1881
|
+
CVXPY Expression and Constraints the Ulcer Index risk measure.
|
1882
|
+
"""
|
1883
|
+
v, constraints = self._cvx_drawdown(prior_model=prior_model, w=w, factor=factor)
|
1884
|
+
n_observations = prior_model.returns.shape[0]
|
1885
|
+
risk = cp.norm(v[1:], 2) / (np.sqrt(n_observations))
|
1886
|
+
return risk, constraints
|
1887
|
+
|
1888
|
+
def _gini_mean_difference_risk(
|
1889
|
+
self, prior_model: PriorModel, w: cp.Variable, factor: skt.Factor
|
1890
|
+
) -> skt.RiskResult:
|
1891
|
+
"""Expression and Constraints of the Gini Mean Difference risk measure.
|
1892
|
+
|
1893
|
+
The Gini mean difference (GMD) is a measure of dispersion introduced in the
|
1894
|
+
context of portfolio optimization by Yitzhaki (1982).
|
1895
|
+
The initial formulation was not used by practitioners due to the high number of
|
1896
|
+
variables that increases proportional to T(T−1)/2 ,
|
1897
|
+
|
1898
|
+
Cajas (2021) proposed an alternative reformulation based on the ordered weighted
|
1899
|
+
averaging (OWA) operator for monotonic weights proposed by Chassein and
|
1900
|
+
Goerigk (2015). We implement this formulation which is more efficient for large
|
1901
|
+
scale problems.
|
1902
|
+
|
1903
|
+
Parameters
|
1904
|
+
----------
|
1905
|
+
prior_model : PriorModel
|
1906
|
+
The prior model of the assets distributions.
|
1907
|
+
|
1908
|
+
w : cvxpy Variable
|
1909
|
+
The CVXPY Variable representing assets weights.
|
1910
|
+
|
1911
|
+
factor : cvxpy Variable | cvxpy Constant
|
1912
|
+
Additional variable used for the optimization of some objective function
|
1913
|
+
like the ratio maximization.
|
1914
|
+
|
1915
|
+
Returns
|
1916
|
+
-------
|
1917
|
+
expression : tuple[cvxpy Expression , list[cvxpy Expression]]
|
1918
|
+
CVXPY Expression and Constraints the Ulcer Index risk measure.
|
1919
|
+
"""
|
1920
|
+
ptf_returns = self._cvx_returns(prior_model=prior_model, w=w)
|
1921
|
+
ptf_transaction_cost = self._cvx_transaction_cost(
|
1922
|
+
prior_model=prior_model, w=w, factor=factor
|
1923
|
+
)
|
1924
|
+
ptf_management_fee = self._cvx_management_fee(prior_model=prior_model, w=w)
|
1925
|
+
observation_nb = prior_model.returns.shape[0]
|
1926
|
+
x = cp.Variable((observation_nb, 1))
|
1927
|
+
y = cp.Variable((observation_nb, 1))
|
1928
|
+
z = cp.Variable((observation_nb, 1))
|
1929
|
+
ones = np.ones((observation_nb, 1))
|
1930
|
+
risk = 2 * cp.sum(x + y)
|
1931
|
+
gmd_w = np.array(owa_gmd_weights(observation_nb) / 2).reshape(-1, 1)
|
1932
|
+
# noinspection PyTypeChecker
|
1933
|
+
constraints = [
|
1934
|
+
ptf_returns * self._scale_constraints
|
1935
|
+
- ptf_transaction_cost * self._scale_constraints
|
1936
|
+
- ptf_management_fee * self._scale_constraints
|
1937
|
+
== cp.reshape(z, (observation_nb,)) * self._scale_constraints,
|
1938
|
+
z @ gmd_w.T <= ones @ x.T + y @ ones.T,
|
1939
|
+
]
|
1940
|
+
return risk, constraints
|
1941
|
+
|
1942
|
+
@abstractmethod
|
1943
|
+
def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
|
1944
|
+
pass
|