skfolio 0.0.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.
Files changed (79) hide show
  1. skfolio/__init__.py +29 -0
  2. skfolio/cluster/__init__.py +8 -0
  3. skfolio/cluster/_hierarchical.py +387 -0
  4. skfolio/datasets/__init__.py +20 -0
  5. skfolio/datasets/_base.py +389 -0
  6. skfolio/datasets/data/__init__.py +0 -0
  7. skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  8. skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  9. skfolio/datasets/data/sp500_index.csv.gz +0 -0
  10. skfolio/distance/__init__.py +26 -0
  11. skfolio/distance/_base.py +55 -0
  12. skfolio/distance/_distance.py +574 -0
  13. skfolio/exceptions.py +30 -0
  14. skfolio/measures/__init__.py +76 -0
  15. skfolio/measures/_enums.py +355 -0
  16. skfolio/measures/_measures.py +607 -0
  17. skfolio/metrics/__init__.py +3 -0
  18. skfolio/metrics/_scorer.py +121 -0
  19. skfolio/model_selection/__init__.py +18 -0
  20. skfolio/model_selection/_combinatorial.py +407 -0
  21. skfolio/model_selection/_validation.py +194 -0
  22. skfolio/model_selection/_walk_forward.py +221 -0
  23. skfolio/moments/__init__.py +41 -0
  24. skfolio/moments/covariance/__init__.py +29 -0
  25. skfolio/moments/covariance/_base.py +101 -0
  26. skfolio/moments/covariance/_covariance.py +1108 -0
  27. skfolio/moments/expected_returns/__init__.py +21 -0
  28. skfolio/moments/expected_returns/_base.py +31 -0
  29. skfolio/moments/expected_returns/_expected_returns.py +415 -0
  30. skfolio/optimization/__init__.py +36 -0
  31. skfolio/optimization/_base.py +147 -0
  32. skfolio/optimization/cluster/__init__.py +13 -0
  33. skfolio/optimization/cluster/_nco.py +348 -0
  34. skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
  35. skfolio/optimization/cluster/hierarchical/_base.py +440 -0
  36. skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
  37. skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
  38. skfolio/optimization/convex/__init__.py +16 -0
  39. skfolio/optimization/convex/_base.py +1944 -0
  40. skfolio/optimization/convex/_distributionally_robust.py +392 -0
  41. skfolio/optimization/convex/_maximum_diversification.py +417 -0
  42. skfolio/optimization/convex/_mean_risk.py +974 -0
  43. skfolio/optimization/convex/_risk_budgeting.py +560 -0
  44. skfolio/optimization/ensemble/__init__.py +6 -0
  45. skfolio/optimization/ensemble/_base.py +87 -0
  46. skfolio/optimization/ensemble/_stacking.py +326 -0
  47. skfolio/optimization/naive/__init__.py +3 -0
  48. skfolio/optimization/naive/_naive.py +173 -0
  49. skfolio/population/__init__.py +3 -0
  50. skfolio/population/_population.py +883 -0
  51. skfolio/portfolio/__init__.py +13 -0
  52. skfolio/portfolio/_base.py +1096 -0
  53. skfolio/portfolio/_multi_period_portfolio.py +610 -0
  54. skfolio/portfolio/_portfolio.py +842 -0
  55. skfolio/pre_selection/__init__.py +7 -0
  56. skfolio/pre_selection/_pre_selection.py +342 -0
  57. skfolio/preprocessing/__init__.py +3 -0
  58. skfolio/preprocessing/_returns.py +114 -0
  59. skfolio/prior/__init__.py +18 -0
  60. skfolio/prior/_base.py +63 -0
  61. skfolio/prior/_black_litterman.py +238 -0
  62. skfolio/prior/_empirical.py +163 -0
  63. skfolio/prior/_factor_model.py +268 -0
  64. skfolio/typing.py +50 -0
  65. skfolio/uncertainty_set/__init__.py +23 -0
  66. skfolio/uncertainty_set/_base.py +108 -0
  67. skfolio/uncertainty_set/_bootstrap.py +281 -0
  68. skfolio/uncertainty_set/_empirical.py +237 -0
  69. skfolio/utils/__init__.py +0 -0
  70. skfolio/utils/bootstrap.py +115 -0
  71. skfolio/utils/equations.py +350 -0
  72. skfolio/utils/sorting.py +117 -0
  73. skfolio/utils/stats.py +466 -0
  74. skfolio/utils/tools.py +567 -0
  75. skfolio-0.0.1.dist-info/LICENSE +29 -0
  76. skfolio-0.0.1.dist-info/METADATA +568 -0
  77. skfolio-0.0.1.dist-info/RECORD +79 -0
  78. skfolio-0.0.1.dist-info/WHEEL +5 -0
  79. skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ """Expected returns module."""
2
+
3
+ from skfolio.moments.expected_returns._base import (
4
+ BaseMu,
5
+ )
6
+ from skfolio.moments.expected_returns._expected_returns import (
7
+ EWMu,
8
+ EmpiricalMu,
9
+ EquilibriumMu,
10
+ ShrunkMu,
11
+ ShrunkMuMethods,
12
+ )
13
+
14
+ __all__ = [
15
+ "BaseMu",
16
+ "EmpiricalMu",
17
+ "EWMu",
18
+ "ShrunkMu",
19
+ "EquilibriumMu",
20
+ "ShrunkMuMethods",
21
+ ]
@@ -0,0 +1,31 @@
1
+ """Base Expected returns estimators."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import sklearn.base as skb
11
+
12
+
13
+ class BaseMu(skb.BaseEstimator, ABC):
14
+ """Base class for all expected returns estimators in skfolio.
15
+
16
+ Notes
17
+ -----
18
+ All estimators should specify all the parameters that can be set
19
+ at the class level in their ``__init__`` as explicit keyword
20
+ arguments (no ``*args`` or ``**kwargs``).
21
+ """
22
+
23
+ mu_: np.ndarray
24
+
25
+ @abstractmethod
26
+ def __init__(self):
27
+ pass
28
+
29
+ @abstractmethod
30
+ def fit(self, X: npt.ArrayLike, y=None):
31
+ pass
@@ -0,0 +1,415 @@
1
+ """Expected returns estimators."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from enum import auto
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import pandas as pd
11
+
12
+ from skfolio.moments.covariance import BaseCovariance, EmpiricalCovariance
13
+ from skfolio.moments.expected_returns._base import BaseMu
14
+ from skfolio.utils.tools import AutoEnum, check_estimator
15
+
16
+
17
+ class EmpiricalMu(BaseMu):
18
+ """Empirical Expected Returns (Mu) estimator.
19
+
20
+ Estimates the expected returns with the historical mean.
21
+
22
+ Parameters
23
+ ----------
24
+ window_size : int, optional
25
+ Window size. The model is fitted on the last `window_size` observations.
26
+ The default (`None`) is to use all the data.
27
+
28
+ Attributes
29
+ ----------
30
+ mu_ : ndarray of shape (n_assets,)
31
+ Estimated expected returns of the assets.
32
+
33
+ n_features_in_ : int
34
+ Number of assets seen during `fit`.
35
+
36
+ feature_names_in_ : ndarray of shape (`n_features_in_`,)
37
+ Names of assets seen during `fit`. Defined only when `X`
38
+ has assets names that are all strings.
39
+ """
40
+
41
+ def __init__(self, window_size: int | None = None):
42
+ self.window_size = window_size
43
+
44
+ def fit(self, X: npt.ArrayLike, y=None) -> "EmpiricalMu":
45
+ """Fit the Mu Empirical estimator model.
46
+
47
+ Parameters
48
+ ----------
49
+ X : array-like of shape (n_observations, n_assets)
50
+ Price returns of the assets.
51
+
52
+ y : Ignored
53
+ Not used, present for API consistency by convention.
54
+
55
+ Returns
56
+ -------
57
+ self : EmpiricalMu
58
+ Fitted estimator.
59
+ """
60
+ X = self._validate_data(X)
61
+ if self.window_size is not None:
62
+ X = X[-self.window_size :]
63
+ self.mu_ = np.mean(X, axis=0)
64
+ return self
65
+
66
+
67
+ class EWMu(BaseMu):
68
+ r"""Exponentially Weighted Expected Returns (Mu) estimator.
69
+
70
+ Estimates the expected returns with the exponentially weighted mean (EWM).
71
+
72
+ Parameters
73
+ ----------
74
+ window_size : int, optional
75
+ Window size. The model is fitted on the last `window_size` observations.
76
+ The default (`None`) is to use all the data.
77
+
78
+ alpha : float, default=0.2
79
+ Exponential smoothing factor. The default value is `0.2`.
80
+
81
+ :math:`0 < \alpha \leq 1`.
82
+
83
+ Attributes
84
+ ----------
85
+ mu_ : ndarray of shape (n_assets,)
86
+ Estimated expected returns of the assets.
87
+
88
+ n_features_in_ : int
89
+ Number of assets seen during `fit`.
90
+
91
+ feature_names_in_ : ndarray of shape (`n_features_in_`,)
92
+ Names of assets seen during `fit`. Defined only when `X`
93
+ has assets names that are all strings.
94
+ """
95
+
96
+ def __init__(self, window_size: int | None = None, alpha: float = 0.2):
97
+ self.window_size = window_size
98
+ self.alpha = alpha
99
+
100
+ def fit(self, X: npt.ArrayLike, y=None) -> "EWMu":
101
+ """Fit the EWMu estimator model.
102
+
103
+ Parameters
104
+ ----------
105
+ X : array-like of shape (n_observations, n_assets)
106
+ Price returns of the assets.
107
+
108
+ y : Ignored
109
+ Not used, present for API consistency by convention.
110
+
111
+ Returns
112
+ -------
113
+ self : EWMu
114
+ Fitted estimator.
115
+ """
116
+ X = self._validate_data(X)
117
+ if self.window_size is not None:
118
+ X = X[-self.window_size :]
119
+ self.mu_ = pd.DataFrame(X).ewm(alpha=self.alpha).mean().iloc[-1, :].to_numpy()
120
+ return self
121
+
122
+
123
+ class EquilibriumMu(BaseMu):
124
+ r"""Equilibrium Expected Returns (Mu) estimator.
125
+
126
+ The Equilibrium is defined as:
127
+
128
+ .. math:: risk\_aversion \times \Sigma \cdot w^T
129
+
130
+ For Market Cap Equilibrium, the weights are the assets Market Caps.
131
+ For Equal-weighted Equilibrium, the weights are equal-weighted (1/N).
132
+
133
+ Parameters
134
+ ----------
135
+ risk_aversion : float, default=1.0
136
+ Risk aversion factor.
137
+ The default value is `1.0`.
138
+
139
+ weights : array-like of shape (n_assets,), optional
140
+ Asset weights used to compute the Expected Return Equilibrium.
141
+ The default is to use the equal-weighted equilibrium (1/N).
142
+ For a Market Cap weighted equilibrium, you must provide the asset Market Caps.
143
+
144
+ covariance_estimator : BaseCovariance, optional
145
+ :ref:`Covariance estimator <covariance_estimator>` used to estimate the
146
+ covariance in the equilibrium formula.
147
+ The default (`None`) is to use :class:`~skfolio.moments.EmpiricalCovariance`.
148
+
149
+ Attributes
150
+ ----------
151
+ mu_ : ndarray of shape (n_assets,)
152
+ Estimated expected returns of the assets.
153
+
154
+ covariance_estimator_ : BaseCovariance
155
+ Fitted `covariance_estimator`.
156
+
157
+ n_features_in_ : int
158
+ Number of assets seen during `fit`.
159
+
160
+ feature_names_in_ : ndarray of shape (`n_features_in_`,)
161
+ Names of assets seen during `fit`. Defined only when `X`
162
+ has assets names that are all strings.
163
+ """
164
+
165
+ covariance_estimator_: BaseCovariance
166
+
167
+ def __init__(
168
+ self,
169
+ risk_aversion: float = 1,
170
+ weights: np.ndarray | None = None,
171
+ covariance_estimator: BaseCovariance | None = None,
172
+ ):
173
+ self.risk_aversion = risk_aversion
174
+ self.weights = weights
175
+ self.covariance_estimator = covariance_estimator
176
+
177
+ def fit(self, X: npt.ArrayLike, y=None) -> "EquilibriumMu":
178
+ """Fit the EquilibriumMu estimator model.
179
+
180
+ Parameters
181
+ ----------
182
+ X : array-like of shape (n_observations, n_assets)
183
+ Price returns of the assets.
184
+
185
+ y : Ignored
186
+ Not used, present for API consistency by convention.
187
+
188
+ Returns
189
+ -------
190
+ self : EquilibriumMu
191
+ Fitted estimator.
192
+ """
193
+ # fitting estimators
194
+ self.covariance_estimator_ = check_estimator(
195
+ self.covariance_estimator,
196
+ default=EmpiricalCovariance(),
197
+ check_type=BaseCovariance,
198
+ )
199
+ self.covariance_estimator_.fit(X)
200
+
201
+ # we validate and convert to numpy after all models have been fitted to keep
202
+ # features names information.
203
+ X = self._validate_data(X)
204
+ n_assets = X.shape[1]
205
+ if self.weights is None:
206
+ weights = np.ones(n_assets) / n_assets
207
+ else:
208
+ weights = np.asarray(self.weights)
209
+ self.mu_ = self.risk_aversion * self.covariance_estimator_.covariance_ @ weights
210
+ return self
211
+
212
+
213
+ class ShrunkMuMethods(AutoEnum):
214
+ """Shrinkage methods for the ShrunkMu estimator
215
+
216
+ Parameters
217
+ ----------
218
+ JAMES_STEIN : str
219
+ James-Stein method
220
+
221
+ BAYES_STEIN : str
222
+ Bayes-Stein method
223
+
224
+ BODNAR_OKHRIN : str
225
+ Bodnar Okhrin Parolya method
226
+ """
227
+
228
+ JAMES_STEIN = auto()
229
+ BAYES_STEIN = auto()
230
+ BODNAR_OKHRIN = auto()
231
+
232
+
233
+ class ShrunkMu(BaseMu):
234
+ r"""Shrinkage Expected Returns (Mu) estimator.
235
+
236
+ Estimates the expected returns using shrinkage.
237
+
238
+ The sample mean estimator is unbiased but has high variance.
239
+ Stein (1955) proved that it's possible to find an estimator with reduced total
240
+ error using shrinkage by trading a small bias against high variance.
241
+
242
+ The estimator shrinks the sample mean toward a target vector:
243
+
244
+ .. math:: \hat{\mu} = \alpha\bar{\mu}+\beta \mu_{target}
245
+
246
+ with :math:`\bar{\mu}` the sample mean, :math:`\mu_{target}` the target vector
247
+ and :math:`\alpha` and :math:`\beta` two constants to determine.
248
+
249
+ There are two choices for the target vector :math:`\mu_{target}` :
250
+
251
+ * Grand Mean: constant vector of the mean of the sample mean
252
+ * Volatility-Weighted Grand Mean: volatility-weighted sample mean
253
+
254
+ And three methods for :math:`\alpha` and :math:`\beta` :
255
+
256
+ * James-Stein
257
+ * Bayes-Stein
258
+ * Bodnar Okhrin Parolya
259
+
260
+ Parameters
261
+ ----------
262
+ covariance_estimator : BaseCovariance, optional
263
+ :ref:`Covariance estimator <covariance_estimator>` used to estimate the
264
+ covariance in the shrinkage formulae.
265
+ The default (`None`) is to use :class:`~skfolio.moments.EmpiricalCovariance`.
266
+
267
+ vol_weighted_target : bool, default=False
268
+ If this is set to True, the target vector :math:`\mu_{target}` is the
269
+ Volatility-Weighted Grand Mean otherwise it is the Grand Mean.
270
+ The default is `False`.
271
+
272
+ method : ShrunkMuMethods, default=ShrunkMuMethods.JAMES_STEIN
273
+ Shrinkage method :class:`ShrunkMuMethods`.
274
+
275
+ Possible values are:
276
+
277
+ * JAMES_STEIN
278
+ * BAYES_STEIN
279
+ * BODNAR_OKHRIN
280
+
281
+ The default value is `ShrunkMuMethods.JAMES_STEIN`.
282
+
283
+ Attributes
284
+ ----------
285
+ mu_ : ndarray of shape (n_assets,)
286
+ Estimated expected returns of the assets.
287
+
288
+ covariance_estimator_ : BaseCovariance
289
+ Fitted `covariance_estimator`.
290
+
291
+ mu_target_ : ndarray of shape (n_assets,)
292
+ Target vector :math:`\mu_{target}`.
293
+
294
+ alpha_ : float
295
+ Alpha value :math:`\alpha`.
296
+
297
+ beta_ : float
298
+ Beta value :math:`\beta`.
299
+
300
+ n_features_in_ : int
301
+ Number of assets seen during `fit`.
302
+
303
+ feature_names_in_ : ndarray of shape (`n_features_in_`,)
304
+ Names of assets seen during `fit`. Defined only when `X`
305
+ has assets names that are all strings.
306
+
307
+ References
308
+ ----------
309
+ .. [1] "Risk and Asset Allocation",
310
+ Attilio Meucci (2005)
311
+
312
+ .. [2] "Bayes-stein estimation for portfolio analysis",
313
+ Philippe Jorion (1986)
314
+
315
+ .. [3] "Optimal shrinkage estimator for high-dimensional mean vector"
316
+ Bodnar, Okhrin and Parolya (2019)
317
+ """
318
+ covariance_estimator_: BaseCovariance
319
+ mu_target_: np.ndarray
320
+ alpha_: float
321
+ beta_: float
322
+
323
+ def __init__(
324
+ self,
325
+ covariance_estimator: BaseCovariance | None = None,
326
+ vol_weighted_target: bool = False,
327
+ method: ShrunkMuMethods = ShrunkMuMethods.JAMES_STEIN,
328
+ ):
329
+ self.covariance_estimator = covariance_estimator
330
+ self.vol_weighted_target = vol_weighted_target
331
+ self.method = method
332
+
333
+ def fit(self, X: npt.ArrayLike, y=None) -> "ShrunkMu":
334
+ """Fit the ShrunkMu estimator model.
335
+
336
+ Parameters
337
+ ----------
338
+ X : array-like of shape (n_observations, n_assets)
339
+ Price returns of the assets.
340
+
341
+ y : Ignored
342
+ Not used, present for API consistency by convention.
343
+
344
+ Returns
345
+ -------
346
+ self : ShrunkMu
347
+ Fitted estimator.
348
+ """
349
+ if not isinstance(self.method, ShrunkMuMethods):
350
+ raise ValueError(
351
+ "`method` must be of type ShrunkMuMethods, got"
352
+ f" {type(self.method).__name__}"
353
+ )
354
+ # fitting estimators
355
+ self.covariance_estimator_ = check_estimator(
356
+ self.covariance_estimator,
357
+ default=EmpiricalCovariance(),
358
+ check_type=BaseCovariance,
359
+ )
360
+ self.covariance_estimator_.fit(X)
361
+
362
+ # we validate and convert to numpy after all models have been fitted to keep
363
+ # features names information.
364
+ X = self._validate_data(X)
365
+ n_observations, n_assets = X.shape
366
+
367
+ covariance = self.covariance_estimator_.covariance_
368
+
369
+ sample_mu = np.mean(X, axis=0)
370
+ cov_inv = None
371
+
372
+ # Calculate target vector
373
+ if self.vol_weighted_target:
374
+ cov_inv = np.linalg.inv(covariance)
375
+ self.mu_target_ = np.sum(cov_inv, axis=1) @ sample_mu / np.sum(cov_inv)
376
+ else:
377
+ self.mu_target_ = np.mean(sample_mu)
378
+ self.mu_target_ *= np.ones(n_assets)
379
+
380
+ # Calculate Estimators
381
+ match self.method:
382
+ case ShrunkMuMethods.JAMES_STEIN:
383
+ eigenvalues = np.linalg.eigvals(covariance)
384
+ self.beta_ = (
385
+ (np.sum(eigenvalues) - 2 * np.max(eigenvalues))
386
+ / np.sum((sample_mu - self.mu_target_) ** 2)
387
+ / n_observations
388
+ )
389
+ self.alpha_ = 1 - self.beta_
390
+ case ShrunkMuMethods.BAYES_STEIN:
391
+ if cov_inv is None:
392
+ cov_inv = np.linalg.inv(covariance)
393
+ self.beta_ = (n_assets + 2) / (
394
+ n_observations
395
+ * (sample_mu - self.mu_target_).T
396
+ @ cov_inv
397
+ @ (sample_mu - self.mu_target_)
398
+ + (n_assets + 2)
399
+ )
400
+ self.alpha_ = 1 - self.beta_
401
+ case ShrunkMuMethods.BODNAR_OKHRIN:
402
+ if cov_inv is None:
403
+ cov_inv = np.linalg.inv(covariance)
404
+ u = sample_mu.T @ cov_inv @ sample_mu
405
+ v = sample_mu.T @ cov_inv @ self.mu_target_
406
+ w = self.mu_target_.T @ cov_inv @ self.mu_target_
407
+ self.alpha_ = (
408
+ (u - n_assets / (n_observations - n_assets)) * w - v**2
409
+ ) / (u * w - v**2)
410
+ self.beta_ = (1 - self.alpha_) * v / u
411
+ case _:
412
+ raise ValueError(f"method {self.method} is not valid")
413
+
414
+ self.mu_ = self.alpha_ * sample_mu + self.beta_ * self.mu_target_
415
+ return self
@@ -0,0 +1,36 @@
1
+ from skfolio.optimization._base import BaseOptimization
2
+ from skfolio.optimization.cluster import (
3
+ BaseHierarchicalOptimization,
4
+ HierarchicalEqualRiskContribution,
5
+ HierarchicalRiskParity,
6
+ NestedClustersOptimization,
7
+ )
8
+ from skfolio.optimization.convex import (
9
+ ConvexOptimization,
10
+ DistributionallyRobustCVaR,
11
+ MaximumDiversification,
12
+ MeanRisk,
13
+ ObjectiveFunction,
14
+ RiskBudgeting,
15
+ )
16
+ from skfolio.optimization.ensemble import BaseComposition, StackingOptimization
17
+ from skfolio.optimization.naive import EqualWeighted, InverseVolatility, Random
18
+
19
+ __all__ = [
20
+ "BaseOptimization",
21
+ "InverseVolatility",
22
+ "EqualWeighted",
23
+ "Random",
24
+ "ObjectiveFunction",
25
+ "ConvexOptimization",
26
+ "MeanRisk",
27
+ "RiskBudgeting",
28
+ "DistributionallyRobustCVaR",
29
+ "MaximumDiversification",
30
+ "BaseHierarchicalOptimization",
31
+ "HierarchicalRiskParity",
32
+ "HierarchicalEqualRiskContribution",
33
+ "NestedClustersOptimization",
34
+ "BaseComposition",
35
+ "StackingOptimization",
36
+ ]
@@ -0,0 +1,147 @@
1
+ """Base Optimization estimator."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import sklearn.base as skb
11
+ from sklearn.utils.validation import check_is_fitted
12
+
13
+ from skfolio.measures import RatioMeasure
14
+ from skfolio.population import Population
15
+ from skfolio.portfolio import Portfolio
16
+
17
+
18
+ class BaseOptimization(skb.BaseEstimator, ABC):
19
+ """Base class for all optimization estimators in skfolio.
20
+
21
+ portfolio_params : dict, optional
22
+ Portfolio parameters passed to the portfolio evaluated by the `predict` and
23
+ `score` methods. If not provided, the `name`, `transaction_costs`,
24
+ `management_fees` and `previous_weights` are copied from the optimization
25
+ model and systematically passed to the portfolio.
26
+
27
+ Attributes
28
+ ----------
29
+ weights_ : ndarray of shape (n_assets,) or (n_optimizations, n_assets)
30
+ Weights of the assets.
31
+
32
+ Notes
33
+ -----
34
+ All estimators should specify all the parameters that can be set
35
+ at the class level in their `__init__` as explicit keyword
36
+ arguments (no `*args` or `**kwargs`).
37
+ """
38
+
39
+ weights_: np.ndarray
40
+
41
+ @abstractmethod
42
+ def __init__(self, portfolio_params: dict | None = None):
43
+ self.portfolio_params = portfolio_params
44
+
45
+ @abstractmethod
46
+ def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
47
+ pass
48
+
49
+ def predict(self, X: npt.ArrayLike) -> Portfolio | Population:
50
+ """Predict the `Portfolio` or `Population` of `Portfolio` on `X` based on the
51
+ fitted weights.
52
+
53
+ Optimization estimators can return a 1D or a 2D array of `weights`.
54
+ For a 1D array, the prediction returns a `Portfolio`.
55
+ For a 2D array, the prediction returns a `Population` of `Portfolio`.
56
+
57
+ If `name` is not provided in the portfolio arguments, we use the first
58
+ 500 characters of the estimator name.
59
+
60
+ Parameters
61
+ ----------
62
+ X : array-like of shape (n_observations, n_assets)
63
+ Price returns of the assets.
64
+
65
+ Returns
66
+ -------
67
+ prediction : Portfolio | Population
68
+ `Portfolio` or `Population` of `Portfolio` estimated on `X` based on the
69
+ fitted `weights`.
70
+ """
71
+ check_is_fitted(self, "weights_")
72
+
73
+ if self.portfolio_params is None:
74
+ ptf_kwargs = {}
75
+ else:
76
+ ptf_kwargs = self.portfolio_params.copy()
77
+
78
+ # Set the default portfolio parameters equal to the optimization parameters
79
+ for param in ["transaction_costs", "management_fees", "previous_weights"]:
80
+ if param not in ptf_kwargs and hasattr(self, param):
81
+ ptf_kwargs[param] = getattr(self, param)
82
+
83
+ # If 'name' is not provided in the portfolio arguments, we use the first
84
+ # 500 characters of the optimization estimator's name
85
+ name = ptf_kwargs.pop("name", type(self).__name__)
86
+
87
+ # Optimization estimators can return a 1D or a 2D array of weights.
88
+ # For a 1D array we return a portfolio.
89
+ # For a 2D array we return a population of portfolios.
90
+ if self.weights_.ndim == 2:
91
+ n_portfolios = self.weights_.shape[0]
92
+ return Population(
93
+ [
94
+ Portfolio(
95
+ X=X,
96
+ weights=self.weights_[i],
97
+ name=f"ptf{i} - {name}",
98
+ **ptf_kwargs,
99
+ )
100
+ for i in range(n_portfolios)
101
+ ]
102
+ )
103
+ return Portfolio(X=X, weights=self.weights_, name=name, **ptf_kwargs)
104
+
105
+ def score(self, X: npt.ArrayLike, y: npt.ArrayLike = None) -> float:
106
+ """Prediction score.
107
+ If the prediction is a single `Portfolio`, the score is the Sharpe Ratio.
108
+ If the prediction is a `Population` of `Portfolio`, the score is the mean of all
109
+ the portfolios Sharpe Ratios in the population.
110
+
111
+ Parameters
112
+ ----------
113
+ X : array-like of shape (n_observations, n_assets)
114
+ Price returns of the assets.
115
+
116
+ y : Ignored
117
+ Not used, present here for API consistency by convention.
118
+
119
+ Returns
120
+ -------
121
+ score : float
122
+ The Sharpe Ratio of the portfolio if the prediction is a single `Portfolio`
123
+ or the mean of all the portfolios Sharpe Ratios if the prediction is a
124
+ `Population` of `Portfolio`.
125
+ """
126
+ result = self.predict(X)
127
+ if isinstance(result, Population):
128
+ return result.measures_mean(RatioMeasure.SHARPE_RATIO)
129
+ return result.sharpe_ratio
130
+
131
+ def fit_predict(self, X):
132
+ """Perform `fit` on `X` and returns the predicted `Portfolio` or
133
+ `Population` of `Portfolio` on `X` based on the fitted `weights`.
134
+ For factor models, use `fit(X, y)` then `predict(X)` separately.
135
+
136
+ Parameters
137
+ ----------
138
+ X : array-like of shape (n_observations, n_assets)
139
+ Price returns of the assets.
140
+
141
+ Returns
142
+ -------
143
+ prediction : Portfolio | Population
144
+ `Portfolio` or `Population` of `Portfolio` estimated on `X` based on the
145
+ fitted `weights`.
146
+ """
147
+ return self.fit(X).predict(X)
@@ -0,0 +1,13 @@
1
+ from skfolio.optimization.cluster._nco import NestedClustersOptimization
2
+ from skfolio.optimization.cluster.hierarchical import (
3
+ BaseHierarchicalOptimization,
4
+ HierarchicalEqualRiskContribution,
5
+ HierarchicalRiskParity,
6
+ )
7
+
8
+ __all__ = [
9
+ "BaseHierarchicalOptimization",
10
+ "HierarchicalRiskParity",
11
+ "HierarchicalEqualRiskContribution",
12
+ "NestedClustersOptimization",
13
+ ]