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.
Files changed (42) hide show
  1. skfolio/distribution/multivariate/_vine_copula.py +35 -34
  2. skfolio/distribution/univariate/_base.py +20 -15
  3. skfolio/exceptions.py +5 -0
  4. skfolio/measures/__init__.py +2 -0
  5. skfolio/measures/_measures.py +390 -155
  6. skfolio/optimization/_base.py +21 -4
  7. skfolio/optimization/cluster/hierarchical/_base.py +16 -13
  8. skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
  9. skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
  10. skfolio/optimization/convex/_base.py +238 -144
  11. skfolio/optimization/convex/_distributionally_robust.py +32 -20
  12. skfolio/optimization/convex/_maximum_diversification.py +15 -18
  13. skfolio/optimization/convex/_mean_risk.py +35 -25
  14. skfolio/optimization/convex/_risk_budgeting.py +23 -21
  15. skfolio/optimization/ensemble/__init__.py +2 -4
  16. skfolio/optimization/ensemble/_stacking.py +1 -1
  17. skfolio/optimization/naive/_naive.py +2 -2
  18. skfolio/population/_population.py +30 -9
  19. skfolio/portfolio/_base.py +68 -26
  20. skfolio/portfolio/_multi_period_portfolio.py +5 -0
  21. skfolio/portfolio/_portfolio.py +5 -0
  22. skfolio/pre_selection/_select_non_expiring.py +1 -1
  23. skfolio/prior/__init__.py +6 -2
  24. skfolio/prior/_base.py +7 -3
  25. skfolio/prior/_black_litterman.py +14 -12
  26. skfolio/prior/_empirical.py +8 -7
  27. skfolio/prior/_entropy_pooling.py +1493 -0
  28. skfolio/prior/_factor_model.py +39 -22
  29. skfolio/prior/_opinion_pooling.py +475 -0
  30. skfolio/prior/_synthetic_data.py +10 -8
  31. skfolio/uncertainty_set/_bootstrap.py +4 -4
  32. skfolio/uncertainty_set/_empirical.py +6 -6
  33. skfolio/utils/equations.py +11 -5
  34. skfolio/utils/figure.py +185 -0
  35. skfolio/utils/tools.py +4 -2
  36. {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/METADATA +94 -5
  37. {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/RECORD +41 -39
  38. {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/WHEEL +1 -1
  39. skfolio/synthetic_returns/__init__.py +0 -1
  40. /skfolio/{optimization/ensemble/_base.py → utils/composition.py} +0 -0
  41. {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/licenses/LICENSE +0 -0
  42. {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.PriorModel`
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", "ref2" ... the assets names or the groups names provided
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
- prior_model = self.prior_estimator_.prior_model_
347
- n_observations, n_assets = prior_model.returns.shape
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 * (prior_model.returns @ w) * self._scale_constraints
382
- + cp.multiply(u, (1 + prior_model.returns)) @ ones * self._scale_constraints
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 * (prior_model.returns @ w) * self._scale_constraints
386
- + cp.multiply(v, (1 + prior_model.returns)) @ ones * self._scale_constraints
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
- + (1 / n_observations) * cp.sum(s) * self._scale_objective
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.PriorModel`
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:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
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", "ref2" ... the assets names or the groups names provided
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_.prior_model_.covariance
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.PriorModel`
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:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
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", "ref2" ... the assets names or the groups names provided
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
- prior_model = self.prior_estimator_.prior_model_
782
- n_observations, n_assets = prior_model.returns.shape
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(prior_model=prior_model, w=w)
878
- - self._cvx_transaction_cost(prior_model=prior_model, w=w, factor=factor)
879
- - self._cvx_management_fee(prior_model=prior_model, w=w)
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
- prior_model=prior_model, w=w, y=y, factor=factor
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 == "prior_model":
981
- args[arg_name] = prior_model
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=prior_model.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.PriorModel`
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:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
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", "ref2" ... the assets names or the groups names provided
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
- prior_model = self.prior_estimator_.prior_model_
471
- n_observations, n_assets = prior_model.returns.shape
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(prior_model=prior_model, w=w)
504
- - self._cvx_transaction_cost(prior_model=prior_model, w=w, factor=factor)
505
- - self._cvx_management_fee(prior_model=prior_model, w=w)
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 == "prior_model":
535
- args[arg_name] = prior_model
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
- BaseComposition,
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.optimization.ensemble._base import BaseComposition
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.PriorModel`
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_.prior_model_.covariance
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(self) -> go.Figure:
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
- for ptf in self:
953
- lower = np.percentile(ptf.returns, 1e-1)
954
- upper = np.percentile(ptf.returns, 100 - 1e-1)
955
- x = np.linspace(lower, upper, 500)
956
- y = st.gaussian_kde(ptf.returns)(x)
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
- go.Scatter(x=x, y=y, mode="lines", fill="tozeroy", name=ptf.name)
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",