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
skfolio/typing.py ADDED
@@ -0,0 +1,50 @@
1
+ """Custom typing module."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from collections.abc import Callable
7
+
8
+ import cvxpy as cp
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ import plotly.graph_objects as go
12
+
13
+ from skfolio.measures import ExtraRiskMeasure, PerfMeasure, RatioMeasure, RiskMeasure
14
+
15
+ __all__ = [
16
+ "Groups",
17
+ "Inequality",
18
+ "LinearConstraints",
19
+ "MultiInput",
20
+ "Target",
21
+ "ParametersValues",
22
+ "Factor",
23
+ "Result",
24
+ "RiskResult",
25
+ "ExpressionFunction",
26
+ "Measure",
27
+ "CvxMeasure",
28
+ "Names",
29
+ "Tags",
30
+ ]
31
+
32
+ Measure = PerfMeasure | RiskMeasure | ExtraRiskMeasure | RatioMeasure
33
+ CvxMeasure = PerfMeasure | RiskMeasure | RatioMeasure
34
+ MultiInput = float | dict[str, float] | npt.ArrayLike
35
+ Groups = dict[str, list[str]] | np.ndarray | list[list[str]]
36
+ LinearConstraints = np.ndarray | list[str]
37
+ Inequality = np.ndarray | list
38
+ Target = float | np.ndarray
39
+ ParametersValues = list[tuple[cp.Parameter, float | np.ndarray]]
40
+ Factor = cp.Variable | cp.Constant
41
+ Result = np.ndarray | tuple[float | tuple[float, float] | np.ndarray, np.ndarray]
42
+ RiskResult = tuple[
43
+ cp.Expression | cp.Variable | cp.trace, list[cp.Expression | cp.SOC | cp.PSD]
44
+ ]
45
+ ExpressionFunction = Callable[[cp.Variable, any], cp.Expression]
46
+ Figure = go.Figure
47
+
48
+ # Population
49
+ Names = str | list[str]
50
+ Tags = str | list[str]
@@ -0,0 +1,23 @@
1
+ from skfolio.uncertainty_set._base import (
2
+ BaseCovarianceUncertaintySet,
3
+ BaseMuUncertaintySet,
4
+ UncertaintySet,
5
+ )
6
+ from skfolio.uncertainty_set._bootstrap import (
7
+ BootstrapCovarianceUncertaintySet,
8
+ BootstrapMuUncertaintySet,
9
+ )
10
+ from skfolio.uncertainty_set._empirical import (
11
+ EmpiricalCovarianceUncertaintySet,
12
+ EmpiricalMuUncertaintySet,
13
+ )
14
+
15
+ __all__ = [
16
+ "UncertaintySet",
17
+ "BaseMuUncertaintySet",
18
+ "BaseCovarianceUncertaintySet",
19
+ "EmpiricalMuUncertaintySet",
20
+ "EmpiricalCovarianceUncertaintySet",
21
+ "BootstrapMuUncertaintySet",
22
+ "BootstrapCovarianceUncertaintySet",
23
+ ]
@@ -0,0 +1,108 @@
1
+ """Base Uncertainty estimator"""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ import sklearn.base as skb
12
+
13
+
14
+ # frozen=True with eq=False will lead to an id-based hashing which is needed for
15
+ # caching CVX models in Optimization without impacting performance
16
+ @dataclass(frozen=True, eq=False)
17
+ class UncertaintySet:
18
+ r"""Ellipsoidal uncertainty set dataclass.
19
+
20
+ An ellipsoidal uncertainty set is defined by its size :math:`\kappa` and
21
+ shape :math:`S`. Ellipsoidal uncertainty set can be used with both expected returns
22
+ and covariance:
23
+
24
+ Expected returns ellipsoidal uncertainty set:
25
+
26
+ .. math:: U_{\mu}=\left\{\mu\,|\left(\mu-\hat{\mu}\right)S^{-1}\left(\mu-\hat{\mu}\right)^{T}\leq\kappa^{2}\right\}
27
+
28
+ Covariance ellipsoidal uncertainty set:
29
+
30
+ .. math:: U_{\Sigma}=\left\{\Sigma\,|\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)S^{-1}\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)^{T}\leq k^{2}\,,\,\Sigma\succeq 0\right\}
31
+
32
+ Attributes
33
+ ----------
34
+ k : float
35
+ Size of the ellipsoid :math:`\kappa` that defines the confidence region
36
+
37
+ sigma : ndarray of shape (n_assets)
38
+ Shape of the ellipsoid :math:`S`
39
+ """
40
+ k: float
41
+ sigma: np.ndarray
42
+
43
+
44
+ class BaseMuUncertaintySet(skb.BaseEstimator, ABC):
45
+ """Base class for all Mu Uncertainty Set estimators in `skfolio`.
46
+
47
+ Notes
48
+ -----
49
+ All estimators should specify all the parameters that can be set
50
+ at the class level in their ``__init__`` as explicit keyword
51
+ arguments (no ``*args`` or ``**kwargs``).
52
+ """
53
+
54
+ uncertainty_set_: UncertaintySet
55
+
56
+ @abstractmethod
57
+ def __init__(self):
58
+ pass
59
+
60
+ @abstractmethod
61
+ def fit(self, X: npt.ArrayLike, y=None):
62
+ pass
63
+
64
+
65
+ class BaseCovarianceUncertaintySet(skb.BaseEstimator, ABC):
66
+ """Base class for all Covariance Uncertainty Set estimators in `skfolio`.
67
+
68
+ Notes
69
+ -----
70
+ All estimators should specify all the parameters that can be set
71
+ at the class level in their ``__init__`` as explicit keyword
72
+ arguments (no ``*args`` or ``**kwargs``).
73
+ """
74
+
75
+ uncertainty_set_: UncertaintySet
76
+
77
+ @abstractmethod
78
+ def __init__(self):
79
+ pass
80
+
81
+ @abstractmethod
82
+ def fit(self, X: npt.ArrayLike, y=None):
83
+ pass
84
+
85
+ def _validate_X_y(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None):
86
+ """Validate X and y if provided.
87
+
88
+ Parameters
89
+ ----------
90
+ X : array-like of shape (n_observations, n_assets)
91
+ Price returns of the assets.
92
+
93
+ y : array-like of shape (n_observations, n_targets), optional
94
+ Price returns of factors or a target benchmark.
95
+ The default is `None`.
96
+
97
+ Returns
98
+ -------
99
+ X : ndarray of shape (n_observations, n_assets)
100
+ Validated price returns of the assets.
101
+ y : ndarray of shape (n_observations, n_targets), optional
102
+ Validated price returns of factors or a target benchmark if provided.
103
+ """
104
+ if y is None:
105
+ X = self._validate_data(X)
106
+ else:
107
+ X, y = self._validate_data(X, y, multi_output=True)
108
+ return X, y
@@ -0,0 +1,281 @@
1
+ """Bootstrap Uncertainty Set estimators."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+ import scipy.stats as st
9
+
10
+ from skfolio.prior import BasePrior, EmpiricalPrior
11
+ from skfolio.uncertainty_set._base import (
12
+ BaseCovarianceUncertaintySet,
13
+ BaseMuUncertaintySet,
14
+ UncertaintySet,
15
+ )
16
+ from skfolio.utils.bootstrap import stationary_bootstrap
17
+ from skfolio.utils.tools import check_estimator
18
+
19
+
20
+ class BootstrapMuUncertaintySet(BaseMuUncertaintySet):
21
+ r"""Bootstrap Mu Uncertainty set.
22
+
23
+ Compute the expected returns ellipsoidal uncertainty set using circular bootstrap:
24
+
25
+ .. math:: U_{\mu}=\left\{\mu\,|\left(\mu-\hat{\mu}\right)S^{-1}\left(\mu-\hat{\mu}\right)^{T}\leq\kappa^{2}\right\}
26
+
27
+ The size of the ellipsoid :math:`\kappa` (confidence region), is computed using:
28
+
29
+ .. math:: \kappa^2 = \chi^2_{n\_assets} (\beta)
30
+
31
+ with :math:`\chi^2_{n\_assets}(\beta)` the inverse cumulative distribution function
32
+ of the chi-squared distribution with `n_assets` degrees of freedom at the
33
+ :math:`\beta` confidence level.
34
+
35
+ The Shape of the ellipsoid :math:`S` is computed using stationary bootstrap with
36
+ the option to force the non-diagonal elements of the covariance matrix to zero.
37
+
38
+ Parameters
39
+ ----------
40
+ prior_estimator : BasePrior, optional
41
+ The :ref:`prior estimator <prior>` used to estimate the assets covariance
42
+ matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
43
+
44
+ confidence_level : float , default=0.95
45
+ Confidence level :math:`\beta` of the inverse cumulative distribution function
46
+ of the chi-squared distribution. The default value is `0.95`.
47
+
48
+ diagonal : bool, default=True
49
+ If this is set to True, the non-diagonal elements of the covariance matrix are
50
+ set to zero.
51
+
52
+ n_bootstrap_samples : int, default=1000
53
+ Number of bootstrap samples to generate. The default value is `1000`.
54
+
55
+ block_size : float, optional
56
+ Bootstrap block size. The default (`None`) is to estimate the optimal block size
57
+ using Politis & White algorithm for all individual assets.
58
+
59
+ seed : int, optional
60
+ Random seed used to initialize the pseudo-random number generator.
61
+
62
+ Attributes
63
+ ----------
64
+ uncertainty_set_ : UncertaintySet
65
+ Mu Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`.
66
+
67
+ prior_estimator_ : BasePrior
68
+ Fitted `prior_estimator`.
69
+
70
+ References
71
+ ----------
72
+ .. [1] "Robustness properties of mean-variance portfolios",
73
+ Optimization: A Journal of Mathematical Programming and Operations Research,
74
+ Schöttle & Werner (2009).
75
+
76
+ .. [2] "Automatic Block-Length Selection for the Dependent Bootstrap",
77
+ Politis & White (2004).
78
+
79
+ .. [3] "Correction to Automatic Block-Length Selection for the Dependent
80
+ Bootstrap",
81
+ Patton, Politis & White (2009).
82
+ """
83
+ prior_estimator_: BasePrior
84
+
85
+ def __init__(
86
+ self,
87
+ prior_estimator: BasePrior | None = None,
88
+ confidence_level: float = 0.95,
89
+ diagonal: bool = True,
90
+ n_bootstrap_samples: int = 1000,
91
+ block_size: float | None = None,
92
+ seed: int | None = None,
93
+ ):
94
+ self.prior_estimator = prior_estimator
95
+ self.confidence_level = confidence_level
96
+ self.diagonal = diagonal
97
+ self.n_bootstrap_samples = n_bootstrap_samples
98
+ self.block_size = block_size
99
+ self.seed = seed
100
+
101
+ def fit(
102
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
103
+ ) -> "BootstrapMuUncertaintySet":
104
+ """Fit the Bootstrap Mu Uncertainty set estimator.
105
+
106
+ Parameters
107
+ ----------
108
+ X : array-like of shape (n_observations, n_assets)
109
+ Price returns of the assets.
110
+
111
+ y : array-like of shape (n_observations, n_factors), optional
112
+ Price returns of factors.
113
+ The default is `None`.
114
+
115
+ Returns
116
+ -------
117
+ self : BootstrapMuUncertaintySet
118
+ Fitted estimator.
119
+ """
120
+ self.prior_estimator_ = check_estimator(
121
+ self.prior_estimator,
122
+ default=EmpiricalPrior(),
123
+ check_type=BasePrior,
124
+ )
125
+ # fitting estimators
126
+ self.prior_estimator_.fit(X, y)
127
+ mu = self.prior_estimator_.prior_model_.mu
128
+ returns = self.prior_estimator_.prior_model_.returns
129
+ n_assets = returns.shape[1]
130
+ k = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets))
131
+ samples = stationary_bootstrap(
132
+ returns=returns,
133
+ block_size=self.block_size,
134
+ n_bootstrap_samples=self.n_bootstrap_samples,
135
+ seed=self.seed,
136
+ )
137
+ mus = np.mean(samples, axis=1)
138
+ covs = np.zeros((self.n_bootstrap_samples, n_assets, n_assets))
139
+ for i in range(self.n_bootstrap_samples):
140
+ covs[i] = np.cov(samples[i].T)
141
+
142
+ sigma = np.cov((mus - mu).T)
143
+ if self.diagonal:
144
+ sigma = np.diag(np.diag(sigma))
145
+
146
+ self.uncertainty_set_ = UncertaintySet(k=k, sigma=sigma)
147
+ return self
148
+
149
+
150
+ class BootstrapCovarianceUncertaintySet(BaseCovarianceUncertaintySet):
151
+ r"""Bootstrap Covariance Uncertainty set.
152
+
153
+ Compute the covariance ellipsoidal uncertainty set using circular bootstrap:
154
+
155
+ .. math:: U_{\Sigma}=\left\{\Sigma\,|\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)S^{-1}\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)^{T}\leq k^{2}\,,\,\Sigma\succeq 0\right\}
156
+
157
+ The size of the ellipsoid :math:`\kappa` (confidence region), is computed using:
158
+
159
+ .. math:: \kappa^2 = \chi^2_{n\_assets^2} (\beta)
160
+
161
+ with :math:`\chi^2_{n\_assets^2}(\beta)` the inverse cumulative distribution
162
+ function of the chi-squared distribution with `n_assets` degrees of freedom at the
163
+ :math:`\beta` confidence level.
164
+
165
+ The Shape of the ellipsoid :math:`S` is computed using stationary bootstrap with the
166
+ option to force the non-diagonal elements of the covariance matrix to zero.
167
+
168
+ Parameters
169
+ ----------
170
+ prior_estimator : BasePrior, optional
171
+ The :ref:`prior estimator <prior>` used to estimate the assets covariance
172
+ matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
173
+
174
+ confidence_level : float , default=0.95
175
+ Confidence level :math:`\beta` of the inverse cumulative distribution function
176
+ of the chi-squared distribution. The default value is `0.95`.
177
+
178
+ diagonal : bool, default=True
179
+ If this is set to True, the non-diagonal elements of the covariance matrix are
180
+ set to zero.
181
+
182
+ n_bootstrap_samples : int, default=1000
183
+ Number of bootstrap samples to generate. The default value is `1000`.
184
+
185
+ block_size : float, optional
186
+ Bootstrap block size. The default (`None`) is to estimate the optimal block size
187
+ using Politis & White algorithm for all individual assets.
188
+
189
+ seed : int, optional
190
+ Random seed used to initialize the pseudo-random number generator
191
+
192
+ Attributes
193
+ ----------
194
+ uncertainty_set_ : UncertaintySet
195
+ Covariance Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`.
196
+
197
+ prior_estimator_ : BasePrior
198
+ Fitted `prior_estimator`.
199
+
200
+ References
201
+ ----------
202
+ .. [1] "Robustness properties of mean-variance portfolios",
203
+ Optimization: A Journal of Mathematical Programming and Operations Research,
204
+ Schöttle & Werner (2009).
205
+
206
+ .. [2] "Automatic Block-Length Selection for the Dependent Bootstrap",
207
+ Politis & White (2004).
208
+
209
+ .. [3] "Correction to Automatic Block-Length Selection for the Dependent
210
+ Bootstrap",
211
+ Patton, Politis & White (2009).
212
+ """
213
+
214
+ prior_estimator_: BasePrior
215
+
216
+ def __init__(
217
+ self,
218
+ prior_estimator: BasePrior | None = None,
219
+ confidence_level: float = 0.95,
220
+ diagonal: bool = True,
221
+ n_bootstrap_samples: int = 1000,
222
+ block_size: float | None = None,
223
+ seed: int | None = None,
224
+ ):
225
+ self.prior_estimator = prior_estimator
226
+ self.confidence_level = confidence_level
227
+ self.diagonal = diagonal
228
+ self.n_bootstrap_samples = n_bootstrap_samples
229
+ self.block_size = block_size
230
+ self.seed = seed
231
+
232
+ def fit(self, X: npt.ArrayLike, y=None) -> "BootstrapCovarianceUncertaintySet":
233
+ """Fit the Bootstrap Covariance Uncertainty set estimator.
234
+
235
+ Parameters
236
+ ----------
237
+ X : array-like of shape (n_observations, n_assets)
238
+ Price returns of the assets.
239
+
240
+ y : array-like of shape (n_observations, n_factors), optional
241
+ Price returns of factors.
242
+ The default is `None`.
243
+
244
+ Returns
245
+ -------
246
+ self : EmpiricalCovarianceUncertaintySet
247
+ Fitted estimator.
248
+ """
249
+
250
+ self.prior_estimator_ = check_estimator(
251
+ self.prior_estimator,
252
+ default=EmpiricalPrior(),
253
+ check_type=BasePrior,
254
+ )
255
+ # fitting estimators
256
+ self.prior_estimator_.fit(X, y)
257
+ covariance = self.prior_estimator_.prior_model_.covariance
258
+ returns = self.prior_estimator_.prior_model_.returns
259
+ n_assets = returns.shape[1]
260
+ k = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets**2))
261
+
262
+ samples = stationary_bootstrap(
263
+ returns=returns,
264
+ block_size=self.block_size,
265
+ n_bootstrap_samples=self.n_bootstrap_samples,
266
+ seed=self.seed,
267
+ )
268
+ covs = np.zeros((self.n_bootstrap_samples, n_assets, n_assets))
269
+ for i in range(self.n_bootstrap_samples):
270
+ covs[i] = np.cov(samples[i].T)
271
+
272
+ sigma = np.cov(
273
+ (covs - covariance)
274
+ .reshape((self.n_bootstrap_samples, n_assets**2), order="F")
275
+ .T
276
+ )
277
+ if self.diagonal:
278
+ sigma = np.diag(np.diag(sigma))
279
+
280
+ self.uncertainty_set_ = UncertaintySet(k=k, sigma=sigma)
281
+ return self
@@ -0,0 +1,237 @@
1
+ """Empirical Uncertainty Set estimators."""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+ import scipy.stats as st
9
+
10
+ from skfolio.prior import BasePrior, EmpiricalPrior
11
+ from skfolio.uncertainty_set._base import (
12
+ BaseCovarianceUncertaintySet,
13
+ BaseMuUncertaintySet,
14
+ UncertaintySet,
15
+ )
16
+ from skfolio.utils.stats import commutation_matrix
17
+ from skfolio.utils.tools import check_estimator
18
+
19
+
20
+ class EmpiricalMuUncertaintySet(BaseMuUncertaintySet):
21
+ r"""Empirical Mu Uncertainty Set.
22
+
23
+ Compute the expected returns ellipsoidal uncertainty set [1]_:
24
+
25
+ .. math:: U_{\mu}=\left\{\mu\,|\left(\mu-\hat{\mu}\right)S^{-1}\left(\mu-\hat{\mu}\right)^{T}\leq\kappa^{2}\right\}
26
+
27
+ Under the assumption that :math:`\Sigma` is given, the distribution of the sample
28
+ estimator :math:`\hat{\mu}` based on an i.i.d. sample
29
+ :math:`R_{t}\sim N(\mu, \Sigma), t=1,...,T` is given by
30
+ :math:`\hat{\mu}\sim N(\mu, \frac{1}{T}\Sigma)`.
31
+
32
+ The size of the ellipsoid :math:`\kappa` (confidence region), is computed using:
33
+
34
+ .. math:: \kappa^2 = \chi^2_{n\_assets} (\beta)
35
+
36
+ with :math:`\chi^2_{n\_assets}(\beta)` the inverse cumulative distribution function
37
+ of the chi-squared distribution with `n_assets` degrees of freedom at the
38
+ :math:`\beta` confidence level.
39
+
40
+ The Shape of the ellipsoid :math:`S` is computed using:
41
+
42
+ .. math:: S = \frac{1}{T}\Sigma
43
+
44
+ with the option to force the non-diagonal elements of the covariance matrix to zero.
45
+
46
+ Parameters
47
+ ----------
48
+ prior_estimator : BasePrior, optional
49
+ The :ref:`prior estimator <prior>` used to estimate the assets covariance
50
+ matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
51
+
52
+ confidence_level : float , default=0.95
53
+ Confidence level :math:`\beta` of the inverse cumulative distribution function
54
+ of the chi-squared distribution. The default value is `0.95`.
55
+
56
+ diagonal : bool, default=True
57
+ If this is set to True, the non-diagonal elements of the covariance matrix are
58
+ set to zero.
59
+
60
+ Attributes
61
+ ----------
62
+ uncertainty_set_ : UncertaintySet
63
+ Mu Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`.
64
+
65
+ prior_estimator_ : BasePrior
66
+ Fitted `prior_estimator`.
67
+
68
+ References
69
+ ----------
70
+ .. [1] "Robustness properties of mean-variance portfolios",
71
+ Optimization: A Journal of Mathematical Programming and Operations Research,
72
+ Schöttle & Werner (2009).
73
+ """
74
+ prior_estimator_: BasePrior
75
+
76
+ def __init__(
77
+ self,
78
+ prior_estimator: BasePrior | None = None,
79
+ confidence_level: float = 0.95,
80
+ diagonal: bool = True,
81
+ ):
82
+ self.prior_estimator = prior_estimator
83
+ self.confidence_level = confidence_level
84
+ self.diagonal = diagonal
85
+
86
+ def fit(
87
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
88
+ ) -> "EmpiricalMuUncertaintySet":
89
+ """Fit the Empirical Mu Uncertainty set estimator.
90
+
91
+ Parameters
92
+ ----------
93
+ X : array-like of shape (n_observations, n_assets)
94
+ Price returns of the assets.
95
+
96
+ y : array-like of shape (n_observations, n_factors), optional
97
+ Price returns of factors.
98
+ The default is `None`.
99
+
100
+ Returns
101
+ -------
102
+ self : EmpiricalMuUncertaintySet
103
+ Fitted estimator.
104
+ """
105
+ self.prior_estimator_ = check_estimator(
106
+ self.prior_estimator,
107
+ default=EmpiricalPrior(),
108
+ check_type=BasePrior,
109
+ )
110
+ # fitting estimators
111
+ self.prior_estimator_.fit(X, y)
112
+
113
+ prior_model = self.prior_estimator_.prior_model_
114
+ n_observations, n_assets = prior_model.returns.shape
115
+ k = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets))
116
+
117
+ sigma = prior_model.covariance / n_observations
118
+ if self.diagonal:
119
+ sigma = np.diag(np.diag(sigma))
120
+
121
+ self.uncertainty_set_ = UncertaintySet(k=k, sigma=sigma)
122
+ return self
123
+
124
+
125
+ class EmpiricalCovarianceUncertaintySet(BaseCovarianceUncertaintySet):
126
+ r"""Empirical Covariance Uncertainty set.
127
+
128
+ Compute the covariance ellipsoidal uncertainty set [1]_:
129
+
130
+ .. math:: U_{\Sigma}=\left\{\Sigma\,|\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)S^{-1}\left(\text{vec}(\Sigma)-\text{vec}(\hat{\Sigma})\right)^{T}\leq k^{2}\,,\,\Sigma\succeq 0\right\}
131
+
132
+ We consider the Wishart distribution for the covariance matrix:
133
+
134
+ .. math:: \hat{\Sigma}\sim W(\frac{1}{T-1}\Sigma, T-1)
135
+
136
+ The size of the ellipsoid :math:`\kappa` (confidence region), is computed using:
137
+
138
+ .. math:: \kappa^2 = \chi^2_{n\_assets^2} (\beta)
139
+
140
+ with :math:`\chi^2_{n\_assets^2}(\beta)` the inverse cumulative distribution
141
+ function of the chi-squared distribution with `n_assets` degrees of freedom at the
142
+ :math:`\beta` confidence level.
143
+
144
+ The Shape of the ellipsoid :math:`S` is based on a closed form solution of the
145
+ covariance matrix of the Wishart distributed random variable by using the vector
146
+ notation :math:`vec(x)`:
147
+
148
+ .. math:: Cov[vec(\hat{\Sigma})]=\frac{1}{T-1}(I_{n^2} + K_{nn})(\Sigma \otimes \Sigma)
149
+
150
+ with :math:`K_{nn}` denoting a commutation matrix and :math:`\otimes` representing
151
+ the Kronecker product.
152
+
153
+ Parameters
154
+ ----------
155
+ prior_estimator : BasePrior, optional
156
+ The :ref:`prior estimator <prior>` used to estimate the assets covariance
157
+ matrix. The default (`None`) is to use :class:`~skfolio.prior.EmpiricalPrior`.
158
+
159
+ confidence_level : float , default=0.95
160
+ Confidence level :math:`\beta` of the inverse cumulative distribution function
161
+ of the chi-squared distribution. The default value is `0.95`.
162
+
163
+ diagonal : bool, default=True
164
+ If this is set to True, the non-diagonal elements of the covariance matrix are
165
+ set to zero.
166
+
167
+ Attributes
168
+ ----------
169
+ uncertainty_set_ : UncertaintySet
170
+ Covariance Uncertainty set :class:`~skfolio.uncertainty_set.UncertaintySet`.
171
+
172
+ prior_estimator_ : BasePrior
173
+ Fitted `prior_estimator`.
174
+
175
+ References
176
+ ----------
177
+ .. [1] "Robustness properties of mean-variance portfolios",
178
+ Optimization: A Journal of Mathematical Programming and Operations Research,
179
+ Schöttle & Werner (2009).
180
+ """
181
+ prior_estimator_: BasePrior
182
+
183
+ def __init__(
184
+ self,
185
+ prior_estimator: BasePrior | None = None,
186
+ confidence_level: float = 0.95,
187
+ diagonal: bool = True,
188
+ ):
189
+ self.prior_estimator = prior_estimator
190
+ self.confidence_level = confidence_level
191
+ self.diagonal = diagonal
192
+
193
+ def fit(
194
+ self, X: npt.ArrayLike, y: npt.ArrayLike | None = None
195
+ ) -> "EmpiricalCovarianceUncertaintySet":
196
+ """Fit the Empirical Covariance Uncertainty set estimator.
197
+
198
+ Parameters
199
+ ----------
200
+ X : array-like of shape (n_observations, n_assets)
201
+ Price returns of the assets.
202
+
203
+ y : array-like of shape (n_observations, n_factors), optional
204
+ Price returns of factors.
205
+ The default is `None`.
206
+
207
+ Returns
208
+ -------
209
+ self : EmpiricalCovarianceUncertaintySet
210
+ Fitted estimator.
211
+ """
212
+ self.prior_estimator_ = check_estimator(
213
+ self.prior_estimator,
214
+ default=EmpiricalPrior(),
215
+ check_type=BasePrior,
216
+ )
217
+ # fitting estimators
218
+ self.prior_estimator_.fit(X, y)
219
+
220
+ prior_model = self.prior_estimator_.prior_model_
221
+ n_observations, n_assets = prior_model.returns.shape
222
+ k = np.sqrt(st.chi2.ppf(q=self.confidence_level, df=n_assets**2))
223
+
224
+ sigma = prior_model.covariance / n_observations
225
+ if self.diagonal:
226
+ sigma = np.diag(np.diag(sigma))
227
+
228
+ sigma = np.diag(
229
+ np.diag(
230
+ n_observations
231
+ * (np.identity(n_assets**2) + commutation_matrix(sigma))
232
+ @ np.kron(sigma, sigma)
233
+ )
234
+ )
235
+
236
+ self.uncertainty_set_ = UncertaintySet(k=k, sigma=sigma)
237
+ return self
File without changes