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
@@ -7,12 +7,14 @@ from abc import ABC, abstractmethod
7
7
 
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
+ import pandas as pd
10
11
  import sklearn.base as skb
11
12
  from sklearn.utils.validation import check_is_fitted
12
13
 
13
14
  from skfolio.measures import RatioMeasure
14
15
  from skfolio.population import Population
15
16
  from skfolio.portfolio import Portfolio
17
+ from skfolio.prior import ReturnDistribution
16
18
 
17
19
  # Copyright (c) 2023
18
20
  # Author: Hugo Delatte <delatte.hugo@gmail.com>
@@ -45,6 +47,8 @@ class BaseOptimization(skb.BaseEstimator, ABC):
45
47
  """
46
48
 
47
49
  weights_: np.ndarray
50
+ n_features_in_: int
51
+ feature_names_in_: np.ndarray
48
52
 
49
53
  @abstractmethod
50
54
  def __init__(self, portfolio_params: dict | None = None):
@@ -54,7 +58,7 @@ class BaseOptimization(skb.BaseEstimator, ABC):
54
58
  def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
55
59
  pass
56
60
 
57
- def predict(self, X: npt.ArrayLike) -> Portfolio | Population:
61
+ def predict(self, X: npt.ArrayLike | ReturnDistribution) -> Portfolio | Population:
58
62
  """Predict the `Portfolio` or `Population` of `Portfolio` on `X` based on the
59
63
  fitted weights.
60
64
 
@@ -83,6 +87,18 @@ class BaseOptimization(skb.BaseEstimator, ABC):
83
87
  else:
84
88
  ptf_kwargs = self.portfolio_params.copy()
85
89
 
90
+ # Set X and sample_weight
91
+ if isinstance(X, ReturnDistribution):
92
+ ptf_kwargs["sample_weight"] = X.sample_weight
93
+ if hasattr(self, "feature_names_in_"):
94
+ ptf_kwargs["X"] = pd.DataFrame(
95
+ X.returns, columns=self.feature_names_in_
96
+ )
97
+ else:
98
+ ptf_kwargs["X"] = X.returns
99
+ else:
100
+ ptf_kwargs["X"] = X
101
+
86
102
  # Set the default portfolio parameters equal to the optimization parameters
87
103
  for param in [
88
104
  "transaction_costs",
@@ -105,7 +121,6 @@ class BaseOptimization(skb.BaseEstimator, ABC):
105
121
  return Population(
106
122
  [
107
123
  Portfolio(
108
- X=X,
109
124
  weights=self.weights_[i],
110
125
  name=f"ptf{i} - {name}",
111
126
  **ptf_kwargs,
@@ -113,9 +128,11 @@ class BaseOptimization(skb.BaseEstimator, ABC):
113
128
  for i in range(n_portfolios)
114
129
  ]
115
130
  )
116
- return Portfolio(X=X, weights=self.weights_, name=name, **ptf_kwargs)
131
+ return Portfolio(weights=self.weights_, name=name, **ptf_kwargs)
117
132
 
118
- def score(self, X: npt.ArrayLike, y: npt.ArrayLike = None) -> float:
133
+ def score(
134
+ self, X: npt.ArrayLike | ReturnDistribution, y: npt.ArrayLike = None
135
+ ) -> float:
119
136
  """Prediction score.
120
137
  If the prediction is a single `Portfolio`, the score is the Sharpe Ratio.
121
138
  If the prediction is a `Population` of `Portfolio`, the score is the mean of all
@@ -20,7 +20,7 @@ from skfolio.distance import BaseDistance
20
20
  from skfolio.measures import ExtraRiskMeasure, RiskMeasure
21
21
  from skfolio.optimization._base import BaseOptimization
22
22
  from skfolio.portfolio import Portfolio
23
- from skfolio.prior import BasePrior, PriorModel
23
+ from skfolio.prior import BasePrior, ReturnDistribution
24
24
  from skfolio.utils.tools import input_to_array
25
25
 
26
26
 
@@ -57,7 +57,7 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
57
57
 
58
58
  prior_estimator : BasePrior, optional
59
59
  :ref:`Prior estimator <prior>`.
60
- The prior estimator is used to estimate the :class:`~skfolio.prior.PriorModel`
60
+ The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
61
61
  containing the estimation of assets expected returns, covariance matrix and
62
62
  returns. The moments and returns estimations are used for the risk computation
63
63
  and the returns estimation are used by the distance matrix estimator.
@@ -135,7 +135,7 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
135
135
  needs to be homogenous to the periodicity of :math:`\mu`. For example, if
136
136
  the input `X` is composed of **daily** returns, the `transaction_costs` need
137
137
  to be expressed as **daily** costs.
138
- (See :ref:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
138
+ (See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
139
139
 
140
140
  management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
141
141
  Management fees of the assets. It is used to add linear management fees to the
@@ -280,7 +280,7 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
280
280
  def _risk(
281
281
  self,
282
282
  weights: np.ndarray,
283
- prior_model: PriorModel,
283
+ return_distribution: ReturnDistribution,
284
284
  ) -> float:
285
285
  """Compute the risk measure of a theoretical portfolio defined by the weights
286
286
  vector.
@@ -290,8 +290,8 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
290
290
  weights : ndarray of shape (n_assets,)
291
291
  The vector of weights.
292
292
 
293
- prior_model : PriorModel
294
- The prior model of the assets distribution.
293
+ return_distribution : ReturnDistribution
294
+ The assets return distribution.
295
295
 
296
296
  Returns
297
297
  -------
@@ -300,36 +300,39 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
300
300
  vector.
301
301
  """
302
302
  ptf = Portfolio(
303
- X=prior_model.returns,
303
+ X=return_distribution.returns,
304
+ sample_weight=return_distribution.sample_weight,
304
305
  weights=weights,
305
306
  transaction_costs=self.transaction_costs,
306
307
  management_fees=self.management_fees,
307
308
  previous_weights=self.previous_weights,
308
309
  )
309
310
  if self.risk_measure in [RiskMeasure.VARIANCE, RiskMeasure.STANDARD_DEVIATION]:
310
- risk = ptf.variance_from_assets(assets_covariance=prior_model.covariance)
311
+ risk = ptf.variance_from_assets(
312
+ assets_covariance=return_distribution.covariance
313
+ )
311
314
  if self.risk_measure == RiskMeasure.STANDARD_DEVIATION:
312
315
  risk = np.sqrt(risk)
313
316
  else:
314
317
  risk = getattr(ptf, str(self.risk_measure.value))
315
318
  return risk
316
319
 
317
- def _unitary_risks(self, prior_model: PriorModel) -> np.ndarray:
320
+ def _unitary_risks(self, return_distribution: ReturnDistribution) -> np.ndarray:
318
321
  """Compute the vector of risk measure for each single assets.
319
322
 
320
323
  Parameters
321
324
  ----------
322
- prior_model : PriorModel
323
- The prior model of the assets distribution.
325
+ return_distribution : ReturnDistribution
326
+ The asset returns distribution.
324
327
 
325
328
  Returns
326
329
  -------
327
330
  values: ndarray of shape (n_assets,)
328
331
  The risk measure of each asset.
329
332
  """
330
- n_assets = prior_model.returns.shape[1]
333
+ n_assets = return_distribution.returns.shape[1]
331
334
  risks = [
332
- self._risk(weights=weights, prior_model=prior_model)
335
+ self._risk(weights=weights, return_distribution=return_distribution)
333
336
  for weights in np.identity(n_assets)
334
337
  ]
335
338
  return np.array(risks)
@@ -102,7 +102,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
102
102
 
103
103
  prior_estimator : BasePrior, optional
104
104
  :ref:`Prior estimator <prior>`.
105
- The prior estimator is used to estimate the :class:`~skfolio.prior.PriorModel`
105
+ The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
106
106
  containing the estimation of assets expected returns, covariance matrix and
107
107
  returns. The moments and returns estimations are used for the risk computation
108
108
  and the returns estimation are used by the distance matrix estimator.
@@ -181,7 +181,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
181
181
  needs to be homogenous to the periodicity of :math:`\mu`. For example, if
182
182
  the input `X` is composed of **daily** returns, the `transaction_costs` need
183
183
  to be expressed as **daily** costs.
184
- (See :ref:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
184
+ (See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
185
185
 
186
186
  management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
187
187
  Management fees of the assets. It is used to add linear management fees to the
@@ -368,8 +368,8 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
368
368
 
369
369
  # Fit the estimators
370
370
  self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
371
- prior_model = self.prior_estimator_.prior_model_
372
- returns = prior_model.returns
371
+ return_distribution = self.prior_estimator_.return_distribution_
372
+ returns = return_distribution.returns
373
373
 
374
374
  # To keep the asset_names
375
375
  if isinstance(X, pd.DataFrame):
@@ -397,7 +397,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
397
397
 
398
398
  min_weights, max_weights = self._convert_weights_bounds(n_assets=n_assets)
399
399
 
400
- assets_risks = self._unitary_risks(prior_model=prior_model)
400
+ assets_risks = self._unitary_risks(return_distribution=return_distribution)
401
401
  weights = np.ones(n_assets)
402
402
  clusters_weights = np.ones(n_clusters)
403
403
 
@@ -411,7 +411,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
411
411
  inv_risk_w[cluster_ids] = 1 / assets_risks[cluster_ids]
412
412
  inv_risk_w /= inv_risk_w.sum()
413
413
  cluster_risks.append(
414
- self._risk(weights=inv_risk_w, prior_model=prior_model)
414
+ self._risk(weights=inv_risk_w, return_distribution=return_distribution)
415
415
  )
416
416
  weights[cluster_ids] = inv_risk_w[cluster_ids]
417
417
  cluster_risks = np.array(cluster_risks)
@@ -78,7 +78,7 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
78
78
 
79
79
  prior_estimator : BasePrior, optional
80
80
  :ref:`Prior estimator <prior>`.
81
- The prior estimator is used to estimate the :class:`~skfolio.prior.PriorModel`
81
+ The prior estimator is used to estimate the :class:`~skfolio.prior.ReturnDistribution`
82
82
  containing the estimation of assets expected returns, covariance matrix and
83
83
  returns. The moments and returns estimations are used for the risk computation
84
84
  and the returns estimation are used by the distance matrix estimator.
@@ -156,7 +156,7 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
156
156
  needs to be homogenous to the periodicity of :math:`\mu`. For example, if
157
157
  the input `X` is composed of **daily** returns, the `transaction_costs` need
158
158
  to be expressed as **daily** costs.
159
- (See :ref:`sphx_glr_auto_examples_1_mean_risk_plot_6_transaction_costs.py`)
159
+ (See :ref:`sphx_glr_auto_examples_mean_risk_plot_6_transaction_costs.py`)
160
160
 
161
161
  management_fees : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
162
162
  Management fees of the assets. It is used to add linear management fees to the
@@ -320,8 +320,8 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
320
320
 
321
321
  # Fit the estimators
322
322
  self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
323
- prior_model = self.prior_estimator_.prior_model_
324
- returns = prior_model.returns
323
+ return_distribution = self.prior_estimator_.return_distribution_
324
+ returns = return_distribution.returns
325
325
 
326
326
  # To keep the asset_names
327
327
  if isinstance(X, pd.DataFrame):
@@ -344,7 +344,7 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
344
344
  n_assets = X.shape[1]
345
345
 
346
346
  min_weights, max_weights = self._convert_weights_bounds(n_assets=n_assets)
347
- assets_risks = self._unitary_risks(prior_model=prior_model)
347
+ assets_risks = self._unitary_risks(return_distribution=return_distribution)
348
348
 
349
349
  ordered_linkage_matrix = sch.optimal_leaf_ordering(
350
350
  self.hierarchical_clustering_estimator_.linkage_matrix_,
@@ -365,7 +365,9 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
365
365
  inv_risk_w[ids] = 1 / assets_risks[ids]
366
366
  inv_risk_w /= inv_risk_w.sum()
367
367
  risks.append(
368
- self._risk(weights=inv_risk_w, prior_model=prior_model)
368
+ self._risk(
369
+ weights=inv_risk_w, return_distribution=return_distribution
370
+ )
369
371
  )
370
372
  left_risk, right_risk = risks
371
373
  left_cluster, right_cluster = clusters_ids