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
skfolio/optimization/_base.py
CHANGED
@@ -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(
|
131
|
+
return Portfolio(weights=self.weights_, name=name, **ptf_kwargs)
|
117
132
|
|
118
|
-
def score(
|
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,
|
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.
|
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:`
|
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
|
-
|
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
|
-
|
294
|
-
The
|
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=
|
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(
|
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,
|
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
|
-
|
323
|
-
The
|
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 =
|
333
|
+
n_assets = return_distribution.returns.shape[1]
|
331
334
|
risks = [
|
332
|
-
self._risk(weights=weights,
|
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.
|
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:`
|
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
|
-
|
372
|
-
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(
|
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,
|
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.
|
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:`
|
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
|
-
|
324
|
-
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(
|
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(
|
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
|