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.
- skfolio/__init__.py +29 -0
- skfolio/cluster/__init__.py +8 -0
- skfolio/cluster/_hierarchical.py +387 -0
- skfolio/datasets/__init__.py +20 -0
- skfolio/datasets/_base.py +389 -0
- skfolio/datasets/data/__init__.py +0 -0
- skfolio/datasets/data/factors_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_index.csv.gz +0 -0
- skfolio/distance/__init__.py +26 -0
- skfolio/distance/_base.py +55 -0
- skfolio/distance/_distance.py +574 -0
- skfolio/exceptions.py +30 -0
- skfolio/measures/__init__.py +76 -0
- skfolio/measures/_enums.py +355 -0
- skfolio/measures/_measures.py +607 -0
- skfolio/metrics/__init__.py +3 -0
- skfolio/metrics/_scorer.py +121 -0
- skfolio/model_selection/__init__.py +18 -0
- skfolio/model_selection/_combinatorial.py +407 -0
- skfolio/model_selection/_validation.py +194 -0
- skfolio/model_selection/_walk_forward.py +221 -0
- skfolio/moments/__init__.py +41 -0
- skfolio/moments/covariance/__init__.py +29 -0
- skfolio/moments/covariance/_base.py +101 -0
- skfolio/moments/covariance/_covariance.py +1108 -0
- skfolio/moments/expected_returns/__init__.py +21 -0
- skfolio/moments/expected_returns/_base.py +31 -0
- skfolio/moments/expected_returns/_expected_returns.py +415 -0
- skfolio/optimization/__init__.py +36 -0
- skfolio/optimization/_base.py +147 -0
- skfolio/optimization/cluster/__init__.py +13 -0
- skfolio/optimization/cluster/_nco.py +348 -0
- skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
- skfolio/optimization/cluster/hierarchical/_base.py +440 -0
- skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
- skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
- skfolio/optimization/convex/__init__.py +16 -0
- skfolio/optimization/convex/_base.py +1944 -0
- skfolio/optimization/convex/_distributionally_robust.py +392 -0
- skfolio/optimization/convex/_maximum_diversification.py +417 -0
- skfolio/optimization/convex/_mean_risk.py +974 -0
- skfolio/optimization/convex/_risk_budgeting.py +560 -0
- skfolio/optimization/ensemble/__init__.py +6 -0
- skfolio/optimization/ensemble/_base.py +87 -0
- skfolio/optimization/ensemble/_stacking.py +326 -0
- skfolio/optimization/naive/__init__.py +3 -0
- skfolio/optimization/naive/_naive.py +173 -0
- skfolio/population/__init__.py +3 -0
- skfolio/population/_population.py +883 -0
- skfolio/portfolio/__init__.py +13 -0
- skfolio/portfolio/_base.py +1096 -0
- skfolio/portfolio/_multi_period_portfolio.py +610 -0
- skfolio/portfolio/_portfolio.py +842 -0
- skfolio/pre_selection/__init__.py +7 -0
- skfolio/pre_selection/_pre_selection.py +342 -0
- skfolio/preprocessing/__init__.py +3 -0
- skfolio/preprocessing/_returns.py +114 -0
- skfolio/prior/__init__.py +18 -0
- skfolio/prior/_base.py +63 -0
- skfolio/prior/_black_litterman.py +238 -0
- skfolio/prior/_empirical.py +163 -0
- skfolio/prior/_factor_model.py +268 -0
- skfolio/typing.py +50 -0
- skfolio/uncertainty_set/__init__.py +23 -0
- skfolio/uncertainty_set/_base.py +108 -0
- skfolio/uncertainty_set/_bootstrap.py +281 -0
- skfolio/uncertainty_set/_empirical.py +237 -0
- skfolio/utils/__init__.py +0 -0
- skfolio/utils/bootstrap.py +115 -0
- skfolio/utils/equations.py +350 -0
- skfolio/utils/sorting.py +117 -0
- skfolio/utils/stats.py +466 -0
- skfolio/utils/tools.py +567 -0
- skfolio-0.0.1.dist-info/LICENSE +29 -0
- skfolio-0.0.1.dist-info/METADATA +568 -0
- skfolio-0.0.1.dist-info/RECORD +79 -0
- skfolio-0.0.1.dist-info/WHEEL +5 -0
- 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
|
+
]
|