skfolio 0.2.3__py3-none-any.whl → 0.3.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/datasets/__init__.py +2 -0
- skfolio/datasets/_base.py +51 -0
- skfolio/distance/_distance.py +15 -4
- skfolio/model_selection/_combinatorial.py +2 -2
- skfolio/model_selection/_validation.py +70 -15
- skfolio/model_selection/_walk_forward.py +3 -3
- skfolio/moments/__init__.py +2 -0
- skfolio/moments/covariance/__init__.py +11 -11
- skfolio/moments/covariance/_base.py +10 -9
- skfolio/moments/covariance/_denoise_covariance.py +181 -0
- skfolio/moments/covariance/_detone_covariance.py +158 -0
- skfolio/moments/covariance/_empirical_covariance.py +100 -0
- skfolio/moments/covariance/_ew_covariance.py +109 -0
- skfolio/moments/covariance/_gerber_covariance.py +157 -0
- skfolio/moments/covariance/_graphical_lasso_cv.py +194 -0
- skfolio/moments/covariance/_implied_covariance.py +462 -0
- skfolio/moments/covariance/_ledoit_wolf.py +140 -0
- skfolio/moments/covariance/_oas.py +115 -0
- skfolio/moments/covariance/_shrunk_covariance.py +104 -0
- skfolio/moments/expected_returns/__init__.py +4 -7
- skfolio/moments/expected_returns/_empirical_mu.py +63 -0
- skfolio/moments/expected_returns/_equilibrium_mu.py +124 -0
- skfolio/moments/expected_returns/_ew_mu.py +69 -0
- skfolio/moments/expected_returns/{_expected_returns.py → _shrunk_mu.py} +22 -200
- skfolio/optimization/cluster/_nco.py +46 -8
- skfolio/optimization/cluster/hierarchical/_base.py +21 -1
- skfolio/optimization/cluster/hierarchical/_herc.py +18 -4
- skfolio/optimization/cluster/hierarchical/_hrp.py +13 -4
- skfolio/optimization/convex/_base.py +10 -1
- skfolio/optimization/convex/_distributionally_robust.py +12 -2
- skfolio/optimization/convex/_maximum_diversification.py +9 -2
- skfolio/optimization/convex/_mean_risk.py +33 -6
- skfolio/optimization/convex/_risk_budgeting.py +5 -2
- skfolio/optimization/ensemble/_stacking.py +32 -9
- skfolio/optimization/naive/_naive.py +20 -2
- skfolio/population/_population.py +2 -0
- skfolio/prior/_base.py +1 -1
- skfolio/prior/_black_litterman.py +20 -2
- skfolio/prior/_empirical.py +38 -5
- skfolio/prior/_factor_model.py +44 -7
- skfolio/uncertainty_set/_base.py +30 -9
- skfolio/uncertainty_set/_bootstrap.py +26 -10
- skfolio/uncertainty_set/_empirical.py +25 -10
- skfolio/utils/stats.py +24 -3
- skfolio/utils/tools.py +213 -79
- {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/METADATA +3 -2
- skfolio-0.3.1.dist-info/RECORD +91 -0
- {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/WHEEL +1 -1
- skfolio/moments/covariance/_covariance.py +0 -1114
- skfolio-0.2.3.dist-info/RECORD +0 -79
- {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/LICENSE +0 -0
- {skfolio-0.2.3.dist-info → skfolio-0.3.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Expected
|
1
|
+
"""Shrinkage Expected Returns (Mu) Estimators."""
|
2
2
|
|
3
3
|
# Copyright (c) 2023
|
4
4
|
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
@@ -12,209 +12,13 @@ from enum import auto
|
|
12
12
|
|
13
13
|
import numpy as np
|
14
14
|
import numpy.typing as npt
|
15
|
-
import
|
15
|
+
import sklearn.utils.metadata_routing as skm
|
16
16
|
|
17
17
|
from skfolio.moments.covariance import BaseCovariance, EmpiricalCovariance
|
18
18
|
from skfolio.moments.expected_returns._base import BaseMu
|
19
19
|
from skfolio.utils.tools import AutoEnum, check_estimator
|
20
20
|
|
21
21
|
|
22
|
-
class EmpiricalMu(BaseMu):
|
23
|
-
"""Empirical Expected Returns (Mu) estimator.
|
24
|
-
|
25
|
-
Estimates the expected returns with the historical mean.
|
26
|
-
|
27
|
-
Parameters
|
28
|
-
----------
|
29
|
-
window_size : int, optional
|
30
|
-
Window size. The model is fitted on the last `window_size` observations.
|
31
|
-
The default (`None`) is to use all the data.
|
32
|
-
|
33
|
-
Attributes
|
34
|
-
----------
|
35
|
-
mu_ : ndarray of shape (n_assets,)
|
36
|
-
Estimated expected returns of the assets.
|
37
|
-
|
38
|
-
n_features_in_ : int
|
39
|
-
Number of assets seen during `fit`.
|
40
|
-
|
41
|
-
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
42
|
-
Names of assets seen during `fit`. Defined only when `X`
|
43
|
-
has assets names that are all strings.
|
44
|
-
"""
|
45
|
-
|
46
|
-
def __init__(self, window_size: int | None = None):
|
47
|
-
self.window_size = window_size
|
48
|
-
|
49
|
-
def fit(self, X: npt.ArrayLike, y=None) -> "EmpiricalMu":
|
50
|
-
"""Fit the Mu Empirical estimator model.
|
51
|
-
|
52
|
-
Parameters
|
53
|
-
----------
|
54
|
-
X : array-like of shape (n_observations, n_assets)
|
55
|
-
Price returns of the assets.
|
56
|
-
|
57
|
-
y : Ignored
|
58
|
-
Not used, present for API consistency by convention.
|
59
|
-
|
60
|
-
Returns
|
61
|
-
-------
|
62
|
-
self : EmpiricalMu
|
63
|
-
Fitted estimator.
|
64
|
-
"""
|
65
|
-
X = self._validate_data(X)
|
66
|
-
if self.window_size is not None:
|
67
|
-
X = X[-self.window_size :]
|
68
|
-
self.mu_ = np.mean(X, axis=0)
|
69
|
-
return self
|
70
|
-
|
71
|
-
|
72
|
-
class EWMu(BaseMu):
|
73
|
-
r"""Exponentially Weighted Expected Returns (Mu) estimator.
|
74
|
-
|
75
|
-
Estimates the expected returns with the exponentially weighted mean (EWM).
|
76
|
-
|
77
|
-
Parameters
|
78
|
-
----------
|
79
|
-
window_size : int, optional
|
80
|
-
Window size. The model is fitted on the last `window_size` observations.
|
81
|
-
The default (`None`) is to use all the data.
|
82
|
-
|
83
|
-
alpha : float, default=0.2
|
84
|
-
Exponential smoothing factor. The default value is `0.2`.
|
85
|
-
|
86
|
-
:math:`0 < \alpha \leq 1`.
|
87
|
-
|
88
|
-
Attributes
|
89
|
-
----------
|
90
|
-
mu_ : ndarray of shape (n_assets,)
|
91
|
-
Estimated expected returns of the assets.
|
92
|
-
|
93
|
-
n_features_in_ : int
|
94
|
-
Number of assets seen during `fit`.
|
95
|
-
|
96
|
-
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
97
|
-
Names of assets seen during `fit`. Defined only when `X`
|
98
|
-
has assets names that are all strings.
|
99
|
-
"""
|
100
|
-
|
101
|
-
def __init__(self, window_size: int | None = None, alpha: float = 0.2):
|
102
|
-
self.window_size = window_size
|
103
|
-
self.alpha = alpha
|
104
|
-
|
105
|
-
def fit(self, X: npt.ArrayLike, y=None) -> "EWMu":
|
106
|
-
"""Fit the EWMu estimator model.
|
107
|
-
|
108
|
-
Parameters
|
109
|
-
----------
|
110
|
-
X : array-like of shape (n_observations, n_assets)
|
111
|
-
Price returns of the assets.
|
112
|
-
|
113
|
-
y : Ignored
|
114
|
-
Not used, present for API consistency by convention.
|
115
|
-
|
116
|
-
Returns
|
117
|
-
-------
|
118
|
-
self : EWMu
|
119
|
-
Fitted estimator.
|
120
|
-
"""
|
121
|
-
X = self._validate_data(X)
|
122
|
-
if self.window_size is not None:
|
123
|
-
X = X[-self.window_size :]
|
124
|
-
self.mu_ = pd.DataFrame(X).ewm(alpha=self.alpha).mean().iloc[-1, :].to_numpy()
|
125
|
-
return self
|
126
|
-
|
127
|
-
|
128
|
-
class EquilibriumMu(BaseMu):
|
129
|
-
r"""Equilibrium Expected Returns (Mu) estimator.
|
130
|
-
|
131
|
-
The Equilibrium is defined as:
|
132
|
-
|
133
|
-
.. math:: risk\_aversion \times \Sigma \cdot w^T
|
134
|
-
|
135
|
-
For Market Cap Equilibrium, the weights are the assets Market Caps.
|
136
|
-
For Equal-weighted Equilibrium, the weights are equal-weighted (1/N).
|
137
|
-
|
138
|
-
Parameters
|
139
|
-
----------
|
140
|
-
risk_aversion : float, default=1.0
|
141
|
-
Risk aversion factor.
|
142
|
-
The default value is `1.0`.
|
143
|
-
|
144
|
-
weights : array-like of shape (n_assets,), optional
|
145
|
-
Asset weights used to compute the Expected Return Equilibrium.
|
146
|
-
The default is to use the equal-weighted equilibrium (1/N).
|
147
|
-
For a Market Cap weighted equilibrium, you must provide the asset Market Caps.
|
148
|
-
|
149
|
-
covariance_estimator : BaseCovariance, optional
|
150
|
-
:ref:`Covariance estimator <covariance_estimator>` used to estimate the
|
151
|
-
covariance in the equilibrium formula.
|
152
|
-
The default (`None`) is to use :class:`~skfolio.moments.EmpiricalCovariance`.
|
153
|
-
|
154
|
-
Attributes
|
155
|
-
----------
|
156
|
-
mu_ : ndarray of shape (n_assets,)
|
157
|
-
Estimated expected returns of the assets.
|
158
|
-
|
159
|
-
covariance_estimator_ : BaseCovariance
|
160
|
-
Fitted `covariance_estimator`.
|
161
|
-
|
162
|
-
n_features_in_ : int
|
163
|
-
Number of assets seen during `fit`.
|
164
|
-
|
165
|
-
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
166
|
-
Names of assets seen during `fit`. Defined only when `X`
|
167
|
-
has assets names that are all strings.
|
168
|
-
"""
|
169
|
-
|
170
|
-
covariance_estimator_: BaseCovariance
|
171
|
-
|
172
|
-
def __init__(
|
173
|
-
self,
|
174
|
-
risk_aversion: float = 1,
|
175
|
-
weights: np.ndarray | None = None,
|
176
|
-
covariance_estimator: BaseCovariance | None = None,
|
177
|
-
):
|
178
|
-
self.risk_aversion = risk_aversion
|
179
|
-
self.weights = weights
|
180
|
-
self.covariance_estimator = covariance_estimator
|
181
|
-
|
182
|
-
def fit(self, X: npt.ArrayLike, y=None) -> "EquilibriumMu":
|
183
|
-
"""Fit the EquilibriumMu estimator model.
|
184
|
-
|
185
|
-
Parameters
|
186
|
-
----------
|
187
|
-
X : array-like of shape (n_observations, n_assets)
|
188
|
-
Price returns of the assets.
|
189
|
-
|
190
|
-
y : Ignored
|
191
|
-
Not used, present for API consistency by convention.
|
192
|
-
|
193
|
-
Returns
|
194
|
-
-------
|
195
|
-
self : EquilibriumMu
|
196
|
-
Fitted estimator.
|
197
|
-
"""
|
198
|
-
# fitting estimators
|
199
|
-
self.covariance_estimator_ = check_estimator(
|
200
|
-
self.covariance_estimator,
|
201
|
-
default=EmpiricalCovariance(),
|
202
|
-
check_type=BaseCovariance,
|
203
|
-
)
|
204
|
-
self.covariance_estimator_.fit(X)
|
205
|
-
|
206
|
-
# we validate and convert to numpy after all models have been fitted to keep
|
207
|
-
# features names information.
|
208
|
-
X = self._validate_data(X)
|
209
|
-
n_assets = X.shape[1]
|
210
|
-
if self.weights is None:
|
211
|
-
weights = np.ones(n_assets) / n_assets
|
212
|
-
else:
|
213
|
-
weights = np.asarray(self.weights)
|
214
|
-
self.mu_ = self.risk_aversion * self.covariance_estimator_.covariance_ @ weights
|
215
|
-
return self
|
216
|
-
|
217
|
-
|
218
22
|
class ShrunkMuMethods(AutoEnum):
|
219
23
|
"""Shrinkage methods for the ShrunkMu estimator
|
220
24
|
|
@@ -336,7 +140,15 @@ class ShrunkMu(BaseMu):
|
|
336
140
|
self.vol_weighted_target = vol_weighted_target
|
337
141
|
self.method = method
|
338
142
|
|
339
|
-
def
|
143
|
+
def get_metadata_routing(self):
|
144
|
+
# noinspection PyTypeChecker
|
145
|
+
router = skm.MetadataRouter(owner=self.__class__.__name__).add(
|
146
|
+
covariance_estimator=self.covariance_estimator,
|
147
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
148
|
+
)
|
149
|
+
return router
|
150
|
+
|
151
|
+
def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "ShrunkMu":
|
340
152
|
"""Fit the ShrunkMu estimator model.
|
341
153
|
|
342
154
|
Parameters
|
@@ -347,11 +159,20 @@ class ShrunkMu(BaseMu):
|
|
347
159
|
y : Ignored
|
348
160
|
Not used, present for API consistency by convention.
|
349
161
|
|
162
|
+
**fit_params : dict
|
163
|
+
Parameters to pass to the underlying estimators.
|
164
|
+
Only available if `enable_metadata_routing=True`, which can be
|
165
|
+
set by using ``sklearn.set_config(enable_metadata_routing=True)``.
|
166
|
+
See :ref:`Metadata Routing User Guide <metadata_routing>` for
|
167
|
+
more details.
|
168
|
+
|
350
169
|
Returns
|
351
170
|
-------
|
352
171
|
self : ShrunkMu
|
353
172
|
Fitted estimator.
|
354
173
|
"""
|
174
|
+
routed_params = skm.process_routing(self, "fit", **fit_params)
|
175
|
+
|
355
176
|
if not isinstance(self.method, ShrunkMuMethods):
|
356
177
|
raise ValueError(
|
357
178
|
"`method` must be of type ShrunkMuMethods, got"
|
@@ -363,7 +184,8 @@ class ShrunkMu(BaseMu):
|
|
363
184
|
default=EmpiricalCovariance(),
|
364
185
|
check_type=BaseCovariance,
|
365
186
|
)
|
366
|
-
|
187
|
+
# noinspection PyArgumentList
|
188
|
+
self.covariance_estimator_.fit(X, y, **routed_params.covariance_estimator.fit)
|
367
189
|
|
368
190
|
# we validate and convert to numpy after all models have been fitted to keep
|
369
191
|
# features names information.
|
@@ -15,7 +15,8 @@ import numpy.typing as npt
|
|
15
15
|
import pandas as pd
|
16
16
|
import sklearn as sk
|
17
17
|
import sklearn.base as skb
|
18
|
-
import sklearn.model_selection as
|
18
|
+
import sklearn.model_selection as sks
|
19
|
+
import sklearn.utils.metadata_routing as skm
|
19
20
|
import sklearn.utils.parallel as skp
|
20
21
|
|
21
22
|
import skfolio.typing as skt
|
@@ -174,7 +175,7 @@ class NestedClustersOptimization(BaseOptimization):
|
|
174
175
|
outer_estimator: BaseOptimization | None = None,
|
175
176
|
distance_estimator: BaseDistance | None = None,
|
176
177
|
clustering_estimator: skb.BaseEstimator | None = None,
|
177
|
-
cv:
|
178
|
+
cv: sks.BaseCrossValidator | BaseCombinatorialCV | str | int | None = None,
|
178
179
|
quantile: float = 0.5,
|
179
180
|
quantile_measure: skt.Measure = RatioMeasure.SHARPE_RATIO,
|
180
181
|
n_jobs: int | None = None,
|
@@ -192,8 +193,27 @@ class NestedClustersOptimization(BaseOptimization):
|
|
192
193
|
self.n_jobs = n_jobs
|
193
194
|
self.verbose = verbose
|
194
195
|
|
196
|
+
def get_metadata_routing(self):
|
197
|
+
# noinspection PyTypeChecker
|
198
|
+
router = (
|
199
|
+
skm.MetadataRouter(owner=self.__class__.__name__)
|
200
|
+
.add(
|
201
|
+
distance_estimator=self.distance_estimator,
|
202
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
203
|
+
)
|
204
|
+
.add(
|
205
|
+
clustering_estimator=self.clustering_estimator,
|
206
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
207
|
+
)
|
208
|
+
.add(
|
209
|
+
inner_estimator=self.inner_estimator,
|
210
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
211
|
+
)
|
212
|
+
)
|
213
|
+
return router
|
214
|
+
|
195
215
|
def fit(
|
196
|
-
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
|
216
|
+
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
|
197
217
|
) -> "NestedClustersOptimization":
|
198
218
|
"""Fit the Nested Clusters Optimization estimator.
|
199
219
|
|
@@ -206,11 +226,20 @@ class NestedClustersOptimization(BaseOptimization):
|
|
206
226
|
Price returns of factors or a target benchmark.
|
207
227
|
The default is `None`.
|
208
228
|
|
229
|
+
**fit_params : dict
|
230
|
+
Parameters to pass to the underlying estimators.
|
231
|
+
Only available if `enable_metadata_routing=True`, which can be
|
232
|
+
set by using ``sklearn.set_config(enable_metadata_routing=True)``.
|
233
|
+
See :ref:`Metadata Routing User Guide <metadata_routing>` for
|
234
|
+
more details.
|
235
|
+
|
209
236
|
Returns
|
210
237
|
-------
|
211
238
|
self : NestedClustersOptimization
|
212
239
|
Fitted estimator.
|
213
240
|
"""
|
241
|
+
routed_params = skm.process_routing(self, "fit", **fit_params)
|
242
|
+
|
214
243
|
self.distance_estimator_ = check_estimator(
|
215
244
|
self.distance_estimator,
|
216
245
|
default=PearsonDistance(),
|
@@ -232,7 +261,8 @@ class NestedClustersOptimization(BaseOptimization):
|
|
232
261
|
check_type=BaseOptimization,
|
233
262
|
)
|
234
263
|
|
235
|
-
|
264
|
+
# noinspection PyArgumentList
|
265
|
+
self.distance_estimator_.fit(X, y, **routed_params.distance_estimator.fit)
|
236
266
|
distance = self.distance_estimator_.distance_
|
237
267
|
n_assets = distance.shape[0]
|
238
268
|
|
@@ -241,7 +271,9 @@ class NestedClustersOptimization(BaseOptimization):
|
|
241
271
|
distance = pd.DataFrame(distance, columns=X.columns)
|
242
272
|
|
243
273
|
# noinspection PyUnresolvedReferences
|
244
|
-
self.clustering_estimator_.fit(
|
274
|
+
self.clustering_estimator_.fit(
|
275
|
+
X=distance, y=None, **routed_params.clustering_estimator.fit
|
276
|
+
)
|
245
277
|
# noinspection PyUnresolvedReferences
|
246
278
|
labels = self.clustering_estimator_.labels_
|
247
279
|
n_clusters = max(labels) + 1
|
@@ -254,7 +286,12 @@ class NestedClustersOptimization(BaseOptimization):
|
|
254
286
|
# noinspection PyCallingNonCallable
|
255
287
|
fitted_inner_estimators = skp.Parallel(n_jobs=self.n_jobs)(
|
256
288
|
skp.delayed(fit_single_estimator)(
|
257
|
-
sk.clone(_inner_estimator),
|
289
|
+
sk.clone(_inner_estimator),
|
290
|
+
X,
|
291
|
+
y,
|
292
|
+
routed_params.inner_estimator.fit,
|
293
|
+
indices=cluster_ids,
|
294
|
+
axis=1,
|
258
295
|
)
|
259
296
|
for cluster_ids in clusters
|
260
297
|
if len(cluster_ids) != 1
|
@@ -288,7 +325,7 @@ class NestedClustersOptimization(BaseOptimization):
|
|
288
325
|
cv_predictions = None
|
289
326
|
test_indices = slice(None)
|
290
327
|
else:
|
291
|
-
cv =
|
328
|
+
cv = sks.check_cv(self.cv)
|
292
329
|
if hasattr(cv, "random_state") and cv.random_state is None:
|
293
330
|
cv.random_state = np.random.RandomState()
|
294
331
|
# noinspection PyCallingNonCallable
|
@@ -302,6 +339,7 @@ class NestedClustersOptimization(BaseOptimization):
|
|
302
339
|
verbose=self.verbose,
|
303
340
|
column_indices=cluster_ids,
|
304
341
|
method="predict",
|
342
|
+
params=routed_params.inner_estimator.fit,
|
305
343
|
)
|
306
344
|
for cluster_ids in clusters
|
307
345
|
if len(cluster_ids) != 1
|
@@ -347,7 +385,7 @@ class NestedClustersOptimization(BaseOptimization):
|
|
347
385
|
else:
|
348
386
|
assert not any(cv_predictions), "cv_predictions iterator must be empty"
|
349
387
|
|
350
|
-
fit_single_estimator(self.outer_estimator_,
|
388
|
+
fit_single_estimator(self.outer_estimator_, X_pred, y_pred, fit_params={})
|
351
389
|
outer_weights = self.outer_estimator_.weights_
|
352
390
|
self.weights_ = outer_weights @ inner_weights
|
353
391
|
return self
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
12
12
|
|
13
13
|
import numpy as np
|
14
14
|
import numpy.typing as npt
|
15
|
+
import sklearn.utils.metadata_routing as skm
|
15
16
|
|
16
17
|
import skfolio.typing as skt
|
17
18
|
from skfolio.cluster import HierarchicalClustering
|
@@ -438,6 +439,25 @@ class BaseHierarchicalOptimization(BaseOptimization, ABC):
|
|
438
439
|
)
|
439
440
|
return alpha
|
440
441
|
|
442
|
+
def get_metadata_routing(self):
|
443
|
+
# noinspection PyTypeChecker
|
444
|
+
router = (
|
445
|
+
skm.MetadataRouter(owner=self.__class__.__name__)
|
446
|
+
.add(
|
447
|
+
prior_estimator=self.prior_estimator,
|
448
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
449
|
+
)
|
450
|
+
.add(
|
451
|
+
distance_estimator=self.distance_estimator,
|
452
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
453
|
+
)
|
454
|
+
.add(
|
455
|
+
hierarchical_clustering_estimator=self.hierarchical_clustering_estimator,
|
456
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
457
|
+
)
|
458
|
+
)
|
459
|
+
return router
|
460
|
+
|
441
461
|
@abstractmethod
|
442
|
-
def fit(self, X: npt.ArrayLike, y: None = None):
|
462
|
+
def fit(self, X: npt.ArrayLike, y: None = None, **fit_params):
|
443
463
|
pass
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
10
10
|
import numpy.typing as npt
|
11
11
|
import pandas as pd
|
12
12
|
import scipy.cluster.hierarchy as sch
|
13
|
+
import sklearn.utils.metadata_routing as skm
|
13
14
|
|
14
15
|
import skfolio.typing as skt
|
15
16
|
from skfolio.cluster import HierarchicalClustering
|
@@ -269,7 +270,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
|
|
269
270
|
)
|
270
271
|
|
271
272
|
def fit(
|
272
|
-
self, X: npt.ArrayLike, y: None = None
|
273
|
+
self, X: npt.ArrayLike, y: None = None, **fit_params
|
273
274
|
) -> "HierarchicalEqualRiskContribution":
|
274
275
|
"""Fit the Hierarchical Equal Risk Contribution estimator.
|
275
276
|
|
@@ -281,11 +282,20 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
|
|
281
282
|
y : Ignored
|
282
283
|
Not used, present for API consistency by convention.
|
283
284
|
|
285
|
+
**fit_params : dict
|
286
|
+
Parameters to pass to the underlying estimators.
|
287
|
+
Only available if `enable_metadata_routing=True`, which can be
|
288
|
+
set by using ``sklearn.set_config(enable_metadata_routing=True)``.
|
289
|
+
See :ref:`Metadata Routing User Guide <metadata_routing>` for
|
290
|
+
more details.
|
291
|
+
|
284
292
|
Returns
|
285
293
|
-------
|
286
294
|
self : HierarchicalEqualRiskContribution
|
287
295
|
Fitted estimator.
|
288
296
|
"""
|
297
|
+
routed_params = skm.process_routing(self, "fit", **fit_params)
|
298
|
+
|
289
299
|
# Validate
|
290
300
|
if not isinstance(self.risk_measure, RiskMeasure | ExtraRiskMeasure):
|
291
301
|
raise TypeError(
|
@@ -308,7 +318,7 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
|
|
308
318
|
)
|
309
319
|
|
310
320
|
# Fit the estimators
|
311
|
-
self.prior_estimator_.fit(X, y)
|
321
|
+
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
312
322
|
prior_model = self.prior_estimator_.prior_model_
|
313
323
|
returns = prior_model.returns
|
314
324
|
|
@@ -316,14 +326,18 @@ class HierarchicalEqualRiskContribution(BaseHierarchicalOptimization):
|
|
316
326
|
if isinstance(X, pd.DataFrame):
|
317
327
|
returns = pd.DataFrame(returns, columns=X.columns)
|
318
328
|
|
319
|
-
|
329
|
+
# noinspection PyArgumentList
|
330
|
+
self.distance_estimator_.fit(returns, y, **routed_params.distance_estimator.fit)
|
320
331
|
distance = self.distance_estimator_.distance_
|
321
332
|
|
322
333
|
# To keep the asset_names
|
323
334
|
if isinstance(X, pd.DataFrame):
|
324
335
|
distance = pd.DataFrame(distance, columns=X.columns)
|
325
336
|
|
326
|
-
|
337
|
+
# noinspection PyArgumentList
|
338
|
+
self.hierarchical_clustering_estimator_.fit(
|
339
|
+
X=distance, y=None, **routed_params.hierarchical_clustering_estimator.fit
|
340
|
+
)
|
327
341
|
|
328
342
|
n_clusters = self.hierarchical_clustering_estimator_.n_clusters_
|
329
343
|
labels = self.hierarchical_clustering_estimator_.labels_
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
10
10
|
import numpy.typing as npt
|
11
11
|
import pandas as pd
|
12
12
|
import scipy.cluster.hierarchy as sch
|
13
|
+
import sklearn.utils.metadata_routing as skm
|
13
14
|
|
14
15
|
import skfolio.typing as skt
|
15
16
|
from skfolio.cluster import HierarchicalClustering
|
@@ -270,7 +271,9 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
|
|
270
271
|
portfolio_params=portfolio_params,
|
271
272
|
)
|
272
273
|
|
273
|
-
def fit(
|
274
|
+
def fit(
|
275
|
+
self, X: npt.ArrayLike, y: None = None, **fit_params
|
276
|
+
) -> "HierarchicalRiskParity":
|
274
277
|
"""Fit the Hierarchical Risk Parity Optimization estimator.
|
275
278
|
|
276
279
|
Parameters
|
@@ -286,6 +289,8 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
|
|
286
289
|
self : HierarchicalRiskParity
|
287
290
|
Fitted estimator.
|
288
291
|
"""
|
292
|
+
routed_params = skm.process_routing(self, "fit", **fit_params)
|
293
|
+
|
289
294
|
# Validate
|
290
295
|
if not isinstance(self.risk_measure, RiskMeasure | ExtraRiskMeasure):
|
291
296
|
raise TypeError(
|
@@ -308,7 +313,7 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
|
|
308
313
|
)
|
309
314
|
|
310
315
|
# Fit the estimators
|
311
|
-
self.prior_estimator_.fit(X, y)
|
316
|
+
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
312
317
|
prior_model = self.prior_estimator_.prior_model_
|
313
318
|
returns = prior_model.returns
|
314
319
|
|
@@ -316,14 +321,18 @@ class HierarchicalRiskParity(BaseHierarchicalOptimization):
|
|
316
321
|
if isinstance(X, pd.DataFrame):
|
317
322
|
returns = pd.DataFrame(returns, columns=X.columns)
|
318
323
|
|
319
|
-
|
324
|
+
# noinspection PyArgumentList
|
325
|
+
self.distance_estimator_.fit(returns, y, **routed_params.distance_estimator.fit)
|
320
326
|
distance = self.distance_estimator_.distance_
|
321
327
|
|
322
328
|
# To keep the asset_names
|
323
329
|
if isinstance(X, pd.DataFrame):
|
324
330
|
distance = pd.DataFrame(distance, columns=X.columns)
|
325
331
|
|
326
|
-
|
332
|
+
# noinspection PyArgumentList
|
333
|
+
self.hierarchical_clustering_estimator_.fit(
|
334
|
+
X=distance, y=None, **routed_params.hierarchical_clustering_estimator.fit
|
335
|
+
)
|
327
336
|
|
328
337
|
X = self._validate_data(X)
|
329
338
|
n_assets = X.shape[1]
|
@@ -17,6 +17,7 @@ import numpy as np
|
|
17
17
|
import numpy.typing as npt
|
18
18
|
import scipy as sc
|
19
19
|
import scipy.sparse.linalg as scl
|
20
|
+
import sklearn.utils.metadata_routing as skm
|
20
21
|
|
21
22
|
import skfolio.typing as skt
|
22
23
|
from skfolio.measures import RiskMeasure, owa_gmd_weights
|
@@ -1959,6 +1960,14 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
1959
1960
|
]
|
1960
1961
|
return risk, constraints
|
1961
1962
|
|
1963
|
+
def get_metadata_routing(self):
|
1964
|
+
# noinspection PyTypeChecker
|
1965
|
+
router = skm.MetadataRouter(owner=self.__class__.__name__).add(
|
1966
|
+
prior_estimator=self.prior_estimator,
|
1967
|
+
method_mapping=skm.MethodMapping().add(caller="fit", callee="fit"),
|
1968
|
+
)
|
1969
|
+
return router
|
1970
|
+
|
1962
1971
|
@abstractmethod
|
1963
|
-
def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
|
1972
|
+
def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params):
|
1964
1973
|
pass
|
@@ -7,6 +7,7 @@
|
|
7
7
|
import cvxpy as cp
|
8
8
|
import numpy as np
|
9
9
|
import numpy.typing as npt
|
10
|
+
import sklearn.utils.metadata_routing as skm
|
10
11
|
|
11
12
|
import skfolio.typing as skt
|
12
13
|
from skfolio.measures import RiskMeasure
|
@@ -303,7 +304,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
303
304
|
self.wasserstein_ball_radius = wasserstein_ball_radius
|
304
305
|
|
305
306
|
def fit(
|
306
|
-
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
|
307
|
+
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
|
307
308
|
) -> "DistributionallyRobustCVaR":
|
308
309
|
"""Fit the Distributionally Robust CVaR Optimization estimator.
|
309
310
|
|
@@ -316,11 +317,20 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
316
317
|
Price returns of factors.
|
317
318
|
The default is `None`.
|
318
319
|
|
320
|
+
**fit_params : dict
|
321
|
+
Parameters to pass to the underlying estimators.
|
322
|
+
Only available if `enable_metadata_routing=True`, which can be
|
323
|
+
set by using ``sklearn.set_config(enable_metadata_routing=True)``.
|
324
|
+
See :ref:`Metadata Routing User Guide <metadata_routing>` for
|
325
|
+
more details.
|
326
|
+
|
319
327
|
Returns
|
320
328
|
-------
|
321
329
|
self : DistributionallyRobustCVaR
|
322
330
|
Fitted estimator.
|
323
331
|
"""
|
332
|
+
routed_params = skm.process_routing(self, "fit", **fit_params)
|
333
|
+
|
324
334
|
self._check_feature_names(X, reset=True)
|
325
335
|
# Used to avoid adding multiple times similar constrains linked to identical
|
326
336
|
# risk models
|
@@ -329,7 +339,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
329
339
|
default=EmpiricalPrior(),
|
330
340
|
check_type=BasePrior,
|
331
341
|
)
|
332
|
-
self.prior_estimator_.fit(X, y)
|
342
|
+
self.prior_estimator_.fit(X, y, **routed_params.prior_estimator.fit)
|
333
343
|
prior_model = self.prior_estimator_.prior_model_
|
334
344
|
n_observations, n_assets = prior_model.returns.shape
|
335
345
|
|
@@ -398,7 +398,7 @@ class MaximumDiversification(MeanRisk):
|
|
398
398
|
)
|
399
399
|
|
400
400
|
def fit(
|
401
|
-
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
|
401
|
+
self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params
|
402
402
|
) -> "MaximumDiversification":
|
403
403
|
"""Fit the Maximum Diversification Optimization estimator.
|
404
404
|
|
@@ -411,6 +411,13 @@ class MaximumDiversification(MeanRisk):
|
|
411
411
|
Price returns of factors or a target benchmark.
|
412
412
|
The default is `None`.
|
413
413
|
|
414
|
+
**fit_params : dict
|
415
|
+
Parameters to pass to the underlying estimators.
|
416
|
+
Only available if `enable_metadata_routing=True`, which can be
|
417
|
+
set by using ``sklearn.set_config(enable_metadata_routing=True)``.
|
418
|
+
See :ref:`Metadata Routing User Guide <metadata_routing>` for
|
419
|
+
more details.
|
420
|
+
|
414
421
|
Returns
|
415
422
|
-------
|
416
423
|
self : MaximumDiversification
|
@@ -424,5 +431,5 @@ class MaximumDiversification(MeanRisk):
|
|
424
431
|
return np.sqrt(np.diag(covariance)) @ w
|
425
432
|
|
426
433
|
self.overwrite_expected_return = func
|
427
|
-
super().fit(X, y)
|
434
|
+
super().fit(X, y, **fit_params)
|
428
435
|
return self
|