skfolio 0.9.0__py3-none-any.whl → 0.10.0__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/distribution/multivariate/_vine_copula.py +35 -34
- skfolio/distribution/univariate/_base.py +20 -15
- skfolio/exceptions.py +5 -0
- skfolio/measures/__init__.py +2 -0
- skfolio/measures/_measures.py +390 -155
- skfolio/optimization/_base.py +21 -4
- skfolio/optimization/cluster/hierarchical/_base.py +16 -13
- skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
- skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
- skfolio/optimization/convex/_base.py +238 -144
- skfolio/optimization/convex/_distributionally_robust.py +32 -20
- skfolio/optimization/convex/_maximum_diversification.py +15 -18
- skfolio/optimization/convex/_mean_risk.py +35 -25
- skfolio/optimization/convex/_risk_budgeting.py +23 -21
- skfolio/optimization/ensemble/__init__.py +2 -4
- skfolio/optimization/ensemble/_stacking.py +1 -1
- skfolio/optimization/naive/_naive.py +2 -2
- skfolio/population/_population.py +30 -9
- skfolio/portfolio/_base.py +68 -26
- skfolio/portfolio/_multi_period_portfolio.py +5 -0
- skfolio/portfolio/_portfolio.py +5 -0
- skfolio/pre_selection/_select_non_expiring.py +1 -1
- skfolio/prior/__init__.py +6 -2
- skfolio/prior/_base.py +7 -3
- skfolio/prior/_black_litterman.py +14 -12
- skfolio/prior/_empirical.py +8 -7
- skfolio/prior/_entropy_pooling.py +1493 -0
- skfolio/prior/_factor_model.py +39 -22
- skfolio/prior/_opinion_pooling.py +475 -0
- skfolio/prior/_synthetic_data.py +10 -8
- skfolio/uncertainty_set/_bootstrap.py +4 -4
- skfolio/uncertainty_set/_empirical.py +6 -6
- skfolio/utils/equations.py +11 -5
- skfolio/utils/figure.py +185 -0
- skfolio/utils/tools.py +4 -2
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/METADATA +94 -5
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/RECORD +41 -39
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/WHEEL +1 -1
- skfolio/synthetic_returns/__init__.py +0 -1
- /skfolio/{optimization/ensemble/_base.py → utils/composition.py} +0 -0
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/top_level.txt +0 -0
@@ -46,7 +46,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
46
46
|
|
47
47
|
prior_estimator : BasePrior, optional
|
48
48
|
:ref:`Prior estimator <prior>`.
|
49
|
-
The prior estimator is used to estimate the :class:`~skfolio.prior.
|
49
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
|
50
50
|
containing the estimation of assets expected returns, covariance matrix,
|
51
51
|
returns and Cholesky decomposition of the covariance.
|
52
52
|
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
@@ -124,23 +124,23 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
124
124
|
Linear constraints.
|
125
125
|
The linear constraints must match any of following patterns:
|
126
126
|
|
127
|
-
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
128
|
-
* "ref1 >= 2.9 * ref2"
|
129
|
-
* "ref1 == ref2"
|
130
|
-
* "ref1 >= ref1"
|
127
|
+
* `"2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"`
|
128
|
+
* `"ref1 >= 2.9 * ref2"`
|
129
|
+
* `"ref1 == ref2"`
|
130
|
+
* `"ref1 >= ref1"`
|
131
131
|
|
132
|
-
With "ref1"
|
132
|
+
With `"ref1"`, `"ref2"` ... the assets names or the groups names provided
|
133
133
|
in the parameter `groups`. Assets names can be referenced without the need of
|
134
134
|
`groups` if the input `X` of the `fit` method is a DataFrame with these
|
135
135
|
assets names in columns.
|
136
136
|
|
137
137
|
For example:
|
138
138
|
|
139
|
-
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
140
|
-
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
141
|
-
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
142
|
-
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
143
|
-
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
139
|
+
* `"SPX >= 0.10"` --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
140
|
+
* `"SX5E + TLT >= 0.2"` --> the sum of SX5E and TLT weights must be greater than 20%
|
141
|
+
* `"US == 0.7"` --> the sum of all US weights must be equal to 70%
|
142
|
+
* `"Equity == 3 * Bond"` --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
143
|
+
* `"2*SPX + 3*Europe <= Bond + 0.05"` --> mixing assets and group constraints
|
144
144
|
|
145
145
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
146
146
|
The assets groups referenced in `linear_constraints`.
|
@@ -150,8 +150,8 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
150
150
|
|
151
151
|
For example:
|
152
152
|
|
153
|
-
* groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}
|
154
|
-
* groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
|
153
|
+
* `groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}`
|
154
|
+
* `groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]`
|
155
155
|
|
156
156
|
left_inequality : array-like of shape (n_constraints, n_assets), optional
|
157
157
|
Left inequality matrix :math:`A` of the linear
|
@@ -343,8 +343,8 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
343
343
|
check_type=BasePrior,
|
344
344
|
)
|
345
345
|
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
346
|
-
|
347
|
-
n_observations, n_assets =
|
346
|
+
return_distribution = self.prior_estimator_.return_distribution_
|
347
|
+
n_observations, n_assets = return_distribution.returns.shape
|
348
348
|
|
349
349
|
# set solvers params
|
350
350
|
if self.solver == "CLARABEL":
|
@@ -378,12 +378,16 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
378
378
|
u * self._scale_constraints >= cp.Constant(0),
|
379
379
|
v * self._scale_constraints >= cp.Constant(0),
|
380
380
|
b1 * tau * self._scale_constraints
|
381
|
-
+ a1 * (
|
382
|
-
+ cp.multiply(u, (1 +
|
381
|
+
+ a1 * (return_distribution.returns @ w) * self._scale_constraints
|
382
|
+
+ cp.multiply(u, (1 + return_distribution.returns))
|
383
|
+
@ ones
|
384
|
+
* self._scale_constraints
|
383
385
|
<= s * self._scale_constraints,
|
384
386
|
b2 * tau * self._scale_constraints
|
385
|
-
+ a2 * (
|
386
|
-
+ cp.multiply(v, (1 +
|
387
|
+
+ a2 * (return_distribution.returns @ w) * self._scale_constraints
|
388
|
+
+ cp.multiply(v, (1 + return_distribution.returns))
|
389
|
+
@ ones
|
390
|
+
* self._scale_constraints
|
387
391
|
<= s * self._scale_constraints,
|
388
392
|
]
|
389
393
|
|
@@ -403,9 +407,17 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
403
407
|
custom_objective = self._get_custom_objective(w=w)
|
404
408
|
constraints += self._get_custom_constraints(w=w)
|
405
409
|
|
410
|
+
if return_distribution.sample_weight is None:
|
411
|
+
risk = cp.sum(s) / n_observations * self._scale_objective
|
412
|
+
else:
|
413
|
+
risk = (
|
414
|
+
cp.sum(cp.multiply(return_distribution.sample_weight, s))
|
415
|
+
* self._scale_objective
|
416
|
+
)
|
417
|
+
|
406
418
|
objective = cp.Minimize(
|
407
419
|
cp.Constant(self.wasserstein_ball_radius) * lb * self._scale_objective
|
408
|
-
+
|
420
|
+
+ risk
|
409
421
|
+ custom_objective * self._scale_objective
|
410
422
|
)
|
411
423
|
|
@@ -29,7 +29,7 @@ class MaximumDiversification(MeanRisk):
|
|
29
29
|
----------
|
30
30
|
prior_estimator : BasePrior, optional
|
31
31
|
:ref:`Prior estimator <prior>`.
|
32
|
-
The prior estimator is used to estimate the :class:`~skfolio.prior.
|
32
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
|
33
33
|
containing the estimation of assets expected returns, covariance matrix,
|
34
34
|
returns and Cholesky decomposition of the covariance.
|
35
35
|
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
@@ -130,7 +130,7 @@ class MaximumDiversification(MeanRisk):
|
|
130
130
|
needs to be homogenous to the periodicity of :math:`\mu`. For example, if
|
131
131
|
the input `X` is composed of **daily** returns, the `transaction_costs` need
|
132
132
|
to be expressed as **daily** costs.
|
133
|
-
(See :ref:`
|
133
|
+
(See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
|
134
134
|
|
135
135
|
management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
|
136
136
|
Management fees of the assets. It is used to add linear management fees to the
|
@@ -200,23 +200,23 @@ class MaximumDiversification(MeanRisk):
|
|
200
200
|
Linear constraints.
|
201
201
|
The linear constraints must match any of following patterns:
|
202
202
|
|
203
|
-
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
204
|
-
* "ref1 >= 2.9 * ref2"
|
205
|
-
* "ref1 == ref2"
|
206
|
-
* "ref1 >= ref1"
|
203
|
+
* `"2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"`
|
204
|
+
* `"ref1 >= 2.9 * ref2"`
|
205
|
+
* `"ref1 == ref2"`
|
206
|
+
* `"ref1 >= ref1"`
|
207
207
|
|
208
|
-
With "ref1"
|
208
|
+
With `"ref1"`, `"ref2"` ... the assets names or the groups names provided
|
209
209
|
in the parameter `groups`. Assets names can be referenced without the need of
|
210
210
|
`groups` if the input `X` of the `fit` method is a DataFrame with these
|
211
211
|
assets names in columns.
|
212
212
|
|
213
213
|
For example:
|
214
214
|
|
215
|
-
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
216
|
-
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
217
|
-
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
218
|
-
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
219
|
-
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
215
|
+
* `"SPX >= 0.10"` --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
216
|
+
* `"SX5E + TLT >= 0.2"` --> the sum of SX5E and TLT weights must be greater than 20%
|
217
|
+
* `"US == 0.7"` --> the sum of all US weights must be equal to 70%
|
218
|
+
* `"Equity == 3 * Bond"` --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
219
|
+
* `"2*SPX + 3*Europe <= Bond + 0.05"` --> mixing assets and group constraints
|
220
220
|
|
221
221
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
222
222
|
The assets groups referenced in `linear_constraints`.
|
@@ -226,8 +226,8 @@ class MaximumDiversification(MeanRisk):
|
|
226
226
|
|
227
227
|
For example:
|
228
228
|
|
229
|
-
* groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}
|
230
|
-
* groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
|
229
|
+
* `groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}`
|
230
|
+
* `groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]`
|
231
231
|
|
232
232
|
left_inequality : array-like of shape (n_constraints, n_assets), optional
|
233
233
|
Left inequality matrix :math:`A` of the linear
|
@@ -256,9 +256,6 @@ class MaximumDiversification(MeanRisk):
|
|
256
256
|
min_return : float | array-like of shape (n_optimization), optional
|
257
257
|
Lower bound constraint on the expected return.
|
258
258
|
|
259
|
-
min_return : float | array-like of shape (n_optimization), optional
|
260
|
-
Lower bound constraint on the expected return.
|
261
|
-
|
262
259
|
add_objective : Callable[[cp.Variable], cp.Expression], optional
|
263
260
|
Add a custom objective to the existing objective expression.
|
264
261
|
It is a function that must take as argument the weights `w` and returns a
|
@@ -429,7 +426,7 @@ class MaximumDiversification(MeanRisk):
|
|
429
426
|
|
430
427
|
def func(w, obj):
|
431
428
|
"""Weighted volatilities."""
|
432
|
-
covariance = obj.prior_estimator_.
|
429
|
+
covariance = obj.prior_estimator_.return_distribution_.covariance
|
433
430
|
return np.sqrt(np.diag(covariance)) @ w
|
434
431
|
|
435
432
|
self.overwrite_expected_return = func
|
@@ -141,7 +141,7 @@ class MeanRisk(ConvexOptimization):
|
|
141
141
|
|
142
142
|
prior_estimator : BasePrior, optional
|
143
143
|
:ref:`Prior estimator <prior>`.
|
144
|
-
The prior estimator is used to estimate the :class:`~skfolio.prior.
|
144
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
|
145
145
|
containing the estimation of assets expected returns, covariance matrix,
|
146
146
|
returns and Cholesky decomposition of the covariance.
|
147
147
|
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
@@ -277,7 +277,7 @@ class MeanRisk(ConvexOptimization):
|
|
277
277
|
needs to be homogenous to the periodicity of :math:`\mu`. For example, if
|
278
278
|
the input `X` is composed of **daily** returns, the `transaction_costs` need
|
279
279
|
to be expressed as **daily** costs.
|
280
|
-
(See :ref:`
|
280
|
+
(See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
|
281
281
|
|
282
282
|
management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
|
283
283
|
Management fees of the assets. It is used to add linear management fees to the
|
@@ -369,23 +369,23 @@ class MeanRisk(ConvexOptimization):
|
|
369
369
|
Linear constraints.
|
370
370
|
The linear constraints must match any of following patterns:
|
371
371
|
|
372
|
-
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
373
|
-
* "ref1 >= 2.9 * ref2"
|
374
|
-
* "ref1 == ref2"
|
375
|
-
* "ref1 >= ref1"
|
372
|
+
* `"2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"`
|
373
|
+
* `"ref1 >= 2.9 * ref2"`
|
374
|
+
* `"ref1 == ref2"`
|
375
|
+
* `"ref1 >= ref1"`
|
376
376
|
|
377
|
-
With "ref1"
|
377
|
+
With `"ref1"`, `"ref2"` ... the assets names or the groups names provided
|
378
378
|
in the parameter `groups`. Assets names can be referenced without the need of
|
379
379
|
`groups` if the input `X` of the `fit` method is a DataFrame with these
|
380
380
|
assets names in columns.
|
381
381
|
|
382
382
|
For example:
|
383
383
|
|
384
|
-
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
385
|
-
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
386
|
-
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
387
|
-
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
388
|
-
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
384
|
+
* `"SPX >= 0.10"` --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
385
|
+
* `"SX5E + TLT >= 0.2"` --> the sum of SX5E and TLT weights must be greater than 20%
|
386
|
+
* `"US == 0.7"` --> the sum of all US weights must be equal to 70%
|
387
|
+
* `"Equity == 3 * Bond"` --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
388
|
+
* `"2*SPX + 3*Europe <= Bond + 0.05"` --> mixing assets and group constraints
|
389
389
|
|
390
390
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
391
391
|
The assets groups referenced in `linear_constraints`.
|
@@ -395,8 +395,8 @@ class MeanRisk(ConvexOptimization):
|
|
395
395
|
|
396
396
|
For example:
|
397
397
|
|
398
|
-
* groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}
|
399
|
-
* groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
|
398
|
+
* `groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}`
|
399
|
+
* `groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]`
|
400
400
|
|
401
401
|
left_inequality : array-like of shape (n_constraints, n_assets), optional
|
402
402
|
Left inequality matrix :math:`A` of the linear
|
@@ -778,8 +778,8 @@ class MeanRisk(ConvexOptimization):
|
|
778
778
|
check_type=BasePrior,
|
779
779
|
)
|
780
780
|
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
781
|
-
|
782
|
-
n_observations, n_assets =
|
781
|
+
return_distribution = self.prior_estimator_.return_distribution_
|
782
|
+
n_observations, n_assets = return_distribution.returns.shape
|
783
783
|
|
784
784
|
# set solvers params
|
785
785
|
match self.solver:
|
@@ -874,9 +874,11 @@ class MeanRisk(ConvexOptimization):
|
|
874
874
|
|
875
875
|
# Expected returns
|
876
876
|
expected_return = (
|
877
|
-
self._cvx_expected_return(
|
878
|
-
- self._cvx_transaction_cost(
|
879
|
-
|
877
|
+
self._cvx_expected_return(return_distribution=return_distribution, w=w)
|
878
|
+
- self._cvx_transaction_cost(
|
879
|
+
return_distribution=return_distribution, w=w, factor=factor
|
880
|
+
)
|
881
|
+
- self._cvx_management_fee(return_distribution=return_distribution, w=w)
|
880
882
|
- mu_uncertainty_set
|
881
883
|
)
|
882
884
|
|
@@ -898,11 +900,11 @@ class MeanRisk(ConvexOptimization):
|
|
898
900
|
y = y[y.columns[0]]
|
899
901
|
_, y = skv.validate_data(self, X, y)
|
900
902
|
tracking_error = self._tracking_error(
|
901
|
-
|
903
|
+
return_distribution=return_distribution, w=w, y=y, factor=factor
|
902
904
|
)
|
903
905
|
constraints += [
|
904
906
|
tracking_error * self._scale_constraints
|
905
|
-
<= self.max_tracking_error * self._scale_constraints
|
907
|
+
<= self.max_tracking_error * factor * self._scale_constraints
|
906
908
|
]
|
907
909
|
|
908
910
|
# Turnover
|
@@ -977,8 +979,8 @@ class MeanRisk(ConvexOptimization):
|
|
977
979
|
|
978
980
|
args = {}
|
979
981
|
for arg_name in args_names(risk_func):
|
980
|
-
if arg_name == "
|
981
|
-
args[arg_name] =
|
982
|
+
if arg_name == "return_distribution":
|
983
|
+
args[arg_name] = return_distribution
|
982
984
|
elif arg_name == "w":
|
983
985
|
args[arg_name] = w
|
984
986
|
elif arg_name == "factor":
|
@@ -1036,8 +1038,17 @@ class MeanRisk(ConvexOptimization):
|
|
1036
1038
|
+ custom_objective * self._scale_objective
|
1037
1039
|
)
|
1038
1040
|
case ObjectiveFunction.MAXIMIZE_RATIO:
|
1041
|
+
# Capture common obvious mistake before solver failure to help user
|
1042
|
+
if np.isscalar(self.min_weights) and self.min_weights >= 0:
|
1043
|
+
if np.max(return_distribution.mu) - self.risk_free_rate <= 0:
|
1044
|
+
raise ValueError(
|
1045
|
+
"Cannot optimize for Maximum Ratio with your current "
|
1046
|
+
"constraints and input. This is because your assets' "
|
1047
|
+
"expected returns are all under-performing your risk-free "
|
1048
|
+
f"rate {self.risk_free_rate:.2%}."
|
1049
|
+
)
|
1039
1050
|
homogenization_factor = _optimal_homogenization_factor(
|
1040
|
-
mu=
|
1051
|
+
mu=return_distribution.mu
|
1041
1052
|
)
|
1042
1053
|
|
1043
1054
|
if expected_return.is_affine():
|
@@ -1060,7 +1071,6 @@ class MeanRisk(ConvexOptimization):
|
|
1060
1071
|
# Fractional Programming Problems".
|
1061
1072
|
# The condition to work is f1 >= 0, so we need to raise an user
|
1062
1073
|
# warning when it's not the case.
|
1063
|
-
# TODO: raise user warning when f1<0
|
1064
1074
|
|
1065
1075
|
constraints += [
|
1066
1076
|
expected_return * self._scale_constraints
|
@@ -96,7 +96,7 @@ class RiskBudgeting(ConvexOptimization):
|
|
96
96
|
|
97
97
|
prior_estimator : BasePrior, optional
|
98
98
|
:ref:`Prior estimator <prior>`.
|
99
|
-
The prior estimator is used to estimate the :class:`~skfolio.prior.
|
99
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
|
100
100
|
containing the estimation of assets expected returns, covariance matrix,
|
101
101
|
returns and Cholesky decomposition of the covariance.
|
102
102
|
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
@@ -166,7 +166,7 @@ class RiskBudgeting(ConvexOptimization):
|
|
166
166
|
needs to be homogenous to the periodicity of :math:`\mu`. For example, if
|
167
167
|
the input `X` is composed of **daily** returns, the `transaction_costs` need
|
168
168
|
to be expressed as **daily** costs.
|
169
|
-
(See :ref:`
|
169
|
+
(See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
|
170
170
|
|
171
171
|
management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
|
172
172
|
Management fees of the assets. It is used to add linear management fees to the
|
@@ -216,23 +216,23 @@ class RiskBudgeting(ConvexOptimization):
|
|
216
216
|
Linear constraints.
|
217
217
|
The linear constraints must match any of following patterns:
|
218
218
|
|
219
|
-
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
220
|
-
* "ref1 >= 2.9 * ref2"
|
221
|
-
* "ref1 == ref2"
|
222
|
-
* "ref1 >= ref1"
|
219
|
+
* `"2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"`
|
220
|
+
* `"ref1 >= 2.9 * ref2"`
|
221
|
+
* `"ref1 == ref2"`
|
222
|
+
* `"ref1 >= ref1"`
|
223
223
|
|
224
|
-
With "ref1"
|
224
|
+
With `"ref1"`, `"ref2"` ... the assets names or the groups names provided
|
225
225
|
in the parameter `groups`. Assets names can be referenced without the need of
|
226
226
|
`groups` if the input `X` of the `fit` method is a DataFrame with these
|
227
227
|
assets names in columns.
|
228
228
|
|
229
229
|
For example:
|
230
230
|
|
231
|
-
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
232
|
-
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
233
|
-
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
234
|
-
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
235
|
-
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
231
|
+
* `"SPX >= 0.10"` --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
232
|
+
* `"SX5E + TLT >= 0.2"` --> the sum of SX5E and TLT weights must be greater than 20%
|
233
|
+
* `"US == 0.7"` --> the sum of all US weights must be equal to 70%
|
234
|
+
* `"Equity == 3 * Bond"` --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
235
|
+
* `"2*SPX + 3*Europe <= Bond + 0.05"` --> mixing assets and group constraints
|
236
236
|
|
237
237
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
238
238
|
The assets groups referenced in `linear_constraints`.
|
@@ -242,8 +242,8 @@ class RiskBudgeting(ConvexOptimization):
|
|
242
242
|
|
243
243
|
For example:
|
244
244
|
|
245
|
-
* groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}
|
246
|
-
* groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]
|
245
|
+
* `groups = {"SX5E": ["Equity", "Europe"], "SPX": ["Equity", "US"], "TLT": ["Bond", "US"]}`
|
246
|
+
* `groups = [["Equity", "Equity", "Bond"], ["Europe", "US", "US"]]`
|
247
247
|
|
248
248
|
left_inequality : array-like of shape (n_constraints, n_assets), optional
|
249
249
|
Left inequality matrix :math:`A` of the linear
|
@@ -467,8 +467,8 @@ class RiskBudgeting(ConvexOptimization):
|
|
467
467
|
check_type=BasePrior,
|
468
468
|
)
|
469
469
|
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
470
|
-
|
471
|
-
n_observations, n_assets =
|
470
|
+
return_distribution = self.prior_estimator_.return_distribution_
|
471
|
+
n_observations, n_assets = return_distribution.returns.shape
|
472
472
|
|
473
473
|
# set solvers params
|
474
474
|
if self.solver == "CLARABEL":
|
@@ -500,9 +500,11 @@ class RiskBudgeting(ConvexOptimization):
|
|
500
500
|
|
501
501
|
# Expected returns
|
502
502
|
expected_return = (
|
503
|
-
self._cvx_expected_return(
|
504
|
-
- self._cvx_transaction_cost(
|
505
|
-
|
503
|
+
self._cvx_expected_return(return_distribution=return_distribution, w=w)
|
504
|
+
- self._cvx_transaction_cost(
|
505
|
+
return_distribution=return_distribution, w=w, factor=factor
|
506
|
+
)
|
507
|
+
- self._cvx_management_fee(return_distribution=return_distribution, w=w)
|
506
508
|
)
|
507
509
|
|
508
510
|
# risk budgeting constraint
|
@@ -531,8 +533,8 @@ class RiskBudgeting(ConvexOptimization):
|
|
531
533
|
risk_func = getattr(self, f"_{self.risk_measure.value}_risk")
|
532
534
|
args = {}
|
533
535
|
for arg_name in args_names(risk_func):
|
534
|
-
if arg_name == "
|
535
|
-
args[arg_name] =
|
536
|
+
if arg_name == "return_distribution":
|
537
|
+
args[arg_name] = return_distribution
|
536
538
|
elif arg_name == "w":
|
537
539
|
args[arg_name] = w
|
538
540
|
elif arg_name == "factor":
|
@@ -1,8 +1,6 @@
|
|
1
1
|
"""Ensemble Optimization module."""
|
2
2
|
|
3
|
-
from skfolio.optimization.ensemble._stacking import
|
4
|
-
|
5
|
-
StackingOptimization,
|
6
|
-
)
|
3
|
+
from skfolio.optimization.ensemble._stacking import StackingOptimization
|
4
|
+
from skfolio.utils.composition import BaseComposition
|
7
5
|
|
8
6
|
__all__ = ["BaseComposition", "StackingOptimization"]
|
@@ -23,7 +23,7 @@ from skfolio.measures import RatioMeasure
|
|
23
23
|
from skfolio.model_selection import BaseCombinatorialCV, cross_val_predict
|
24
24
|
from skfolio.optimization._base import BaseOptimization
|
25
25
|
from skfolio.optimization.convex import MeanRisk
|
26
|
-
from skfolio.
|
26
|
+
from skfolio.utils.composition import BaseComposition
|
27
27
|
from skfolio.utils.tools import check_estimator, fit_single_estimator
|
28
28
|
|
29
29
|
|
@@ -25,7 +25,7 @@ class InverseVolatility(BaseOptimization):
|
|
25
25
|
----------
|
26
26
|
prior_estimator : BasePrior, optional
|
27
27
|
:ref:`Prior estimator <prior>`.
|
28
|
-
The prior estimator is used to estimate the :class:`~skfolio.prior.
|
28
|
+
The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
|
29
29
|
containing the estimation of assets expected returns, covariance matrix,
|
30
30
|
returns and Cholesky decomposition of the covariance.
|
31
31
|
The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
|
@@ -98,7 +98,7 @@ class InverseVolatility(BaseOptimization):
|
|
98
98
|
check_type=BasePrior,
|
99
99
|
)
|
100
100
|
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
101
|
-
covariance = self.prior_estimator_.
|
101
|
+
covariance = self.prior_estimator_.return_distribution_.covariance
|
102
102
|
w = 1 / np.sqrt(np.diag(covariance))
|
103
103
|
self.weights_ = w / sum(w)
|
104
104
|
return self
|
@@ -14,11 +14,11 @@ import pandas as pd
|
|
14
14
|
import plotly.express as px
|
15
15
|
import plotly.graph_objects as go
|
16
16
|
import scipy.interpolate as sci
|
17
|
-
import scipy.stats as st
|
18
17
|
|
19
18
|
import skfolio.typing as skt
|
20
19
|
from skfolio.measures import RatioMeasure
|
21
20
|
from skfolio.portfolio import BasePortfolio, MultiPeriodPortfolio
|
21
|
+
from skfolio.utils.figure import kde_trace
|
22
22
|
from skfolio.utils.sorting import non_denominated_sort
|
23
23
|
from skfolio.utils.tools import deduplicate_names, optimal_rounding_decimals
|
24
24
|
|
@@ -940,23 +940,44 @@ class Population(list):
|
|
940
940
|
)
|
941
941
|
return fig
|
942
942
|
|
943
|
-
def plot_returns_distribution(
|
943
|
+
def plot_returns_distribution(
|
944
|
+
self, percentile_cutoff: float | None = None
|
945
|
+
) -> go.Figure:
|
944
946
|
"""Plot the Portfolios returns distribution using Gaussian KDE.
|
945
947
|
|
948
|
+
Parameters
|
949
|
+
----------
|
950
|
+
percentile_cutoff : float, default=None
|
951
|
+
Percentile cutoff for tail truncation (percentile), in percent.
|
952
|
+
If a float p is provided, the distribution support is truncated at the p-th
|
953
|
+
and (100 - p)-th percentiles.
|
954
|
+
If None, no truncation is applied (uses full min/max of returns).
|
955
|
+
|
946
956
|
Returns
|
947
957
|
-------
|
948
958
|
plot : Figure
|
949
959
|
Returns the plot Figure object
|
950
960
|
"""
|
951
|
-
traces = []
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
961
|
+
traces: list[go.Scatter] = []
|
962
|
+
colors = px.colors.qualitative.Plotly
|
963
|
+
|
964
|
+
for i, ptf in enumerate(self):
|
965
|
+
color = colors[i % len(colors)]
|
966
|
+
returns = ptf.returns
|
957
967
|
traces.append(
|
958
|
-
|
968
|
+
kde_trace(
|
969
|
+
x=returns,
|
970
|
+
sample_weight=ptf.sample_weight,
|
971
|
+
percentile_cutoff=percentile_cutoff,
|
972
|
+
name=ptf.name,
|
973
|
+
line_color=color,
|
974
|
+
fill_opacity=0.3,
|
975
|
+
line_dash="solid",
|
976
|
+
line_width=1,
|
977
|
+
visible=True,
|
978
|
+
)
|
959
979
|
)
|
980
|
+
|
960
981
|
fig = go.Figure(traces)
|
961
982
|
fig.update_layout(
|
962
983
|
title="Returns Distribution",
|