skfolio 0.4.3__tar.gz → 0.5.1__tar.gz
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-0.4.3/src/skfolio.egg-info → skfolio-0.5.1}/PKG-INFO +1 -1
- {skfolio-0.4.3 → skfolio-0.5.1}/pyproject.toml +1 -1
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_implied_covariance.py +1 -1
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/_base.py +12 -1
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/_risk_budgeting.py +5 -12
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/population/_population.py +1 -1
- skfolio-0.5.1/src/skfolio/pre_selection/__init__.py +13 -0
- skfolio-0.5.1/src/skfolio/pre_selection/_drop_correlated.py +108 -0
- skfolio-0.5.1/src/skfolio/pre_selection/_select_complete.py +116 -0
- skfolio-0.5.1/src/skfolio/pre_selection/_select_k_extremes.py +100 -0
- skfolio-0.5.1/src/skfolio/pre_selection/_select_non_dominated.py +161 -0
- skfolio-0.5.1/src/skfolio/pre_selection/_select_non_expiring.py +148 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/preprocessing/_returns.py +9 -3
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/stats.py +2 -2
- {skfolio-0.4.3 → skfolio-0.5.1/src/skfolio.egg-info}/PKG-INFO +1 -1
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio.egg-info/SOURCES.txt +5 -1
- skfolio-0.4.3/src/skfolio/pre_selection/__init__.py +0 -7
- skfolio-0.4.3/src/skfolio/pre_selection/_pre_selection.py +0 -343
- {skfolio-0.4.3 → skfolio-0.5.1}/LICENSE +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/MANIFEST.in +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/README.rst +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/setup.cfg +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/cluster/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/cluster/_hierarchical.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/data/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/data/factors_dataset.csv.gz +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/datasets/data/sp500_index.csv.gz +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/distance/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/distance/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/distance/_distance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/exceptions.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/measures/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/measures/_enums.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/measures/_measures.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/metrics/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/metrics/_scorer.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/model_selection/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/model_selection/_combinatorial.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/model_selection/_validation.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/model_selection/_walk_forward.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_denoise_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_detone_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_empirical_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_ew_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_gerber_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_graphical_lasso_cv.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_ledoit_wolf.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_oas.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/covariance/_shrunk_covariance.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/_empirical_mu.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/_equilibrium_mu.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/_ew_mu.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/moments/expected_returns/_shrunk_mu.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/_nco.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/hierarchical/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/hierarchical/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/hierarchical/_herc.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/cluster/hierarchical/_hrp.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/_distributionally_robust.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/_maximum_diversification.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/convex/_mean_risk.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/ensemble/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/ensemble/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/ensemble/_stacking.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/naive/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/optimization/naive/_naive.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/population/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/portfolio/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/portfolio/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/portfolio/_multi_period_portfolio.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/portfolio/_portfolio.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/preprocessing/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/prior/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/prior/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/prior/_black_litterman.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/prior/_empirical.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/prior/_factor_model.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/typing.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/uncertainty_set/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/uncertainty_set/_base.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/uncertainty_set/_bootstrap.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/uncertainty_set/_empirical.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/__init__.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/bootstrap.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/equations.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/sorting.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio/utils/tools.py +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio.egg-info/dependency_links.txt +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio.egg-info/requires.txt +0 -0
- {skfolio-0.4.3 → skfolio-0.5.1}/src/skfolio.egg-info/top_level.txt +0 -0
@@ -259,7 +259,7 @@ class ImpliedCovariance(BaseCovariance):
|
|
259
259
|
if assets_names is not None:
|
260
260
|
vol_assets_names = get_feature_names(implied_vol)
|
261
261
|
if vol_assets_names is not None:
|
262
|
-
missing_assets = assets_names[~np.
|
262
|
+
missing_assets = assets_names[~np.isin(assets_names, vol_assets_names)]
|
263
263
|
if len(missing_assets) > 0:
|
264
264
|
raise ValueError(
|
265
265
|
f"The following assets are missing from "
|
@@ -622,7 +622,11 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
622
622
|
self._cvx_cache = {}
|
623
623
|
|
624
624
|
def _get_weight_constraints(
|
625
|
-
self,
|
625
|
+
self,
|
626
|
+
n_assets: int,
|
627
|
+
w: cp.Variable,
|
628
|
+
factor: skt.Factor,
|
629
|
+
allow_negative_weights: bool = True,
|
626
630
|
) -> list[cpc.Constraint]:
|
627
631
|
"""Compute weight constraints from input parameters.
|
628
632
|
|
@@ -651,6 +655,13 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
651
655
|
fill_value=0,
|
652
656
|
name="min_weights",
|
653
657
|
)
|
658
|
+
|
659
|
+
if not allow_negative_weights and np.any(min_weights < 0):
|
660
|
+
raise ValueError(
|
661
|
+
f"{self.__class__.__name__} must have non negative `min_weights` "
|
662
|
+
f"constraint otherwise the problem becomes non-convex."
|
663
|
+
)
|
664
|
+
|
654
665
|
constraints.append(
|
655
666
|
w * self._scale_constraints
|
656
667
|
>= min_weights * factor * self._scale_constraints
|
@@ -432,15 +432,6 @@ class RiskBudgeting(ConvexOptimization):
|
|
432
432
|
self.min_return = min_return
|
433
433
|
self.risk_budget = risk_budget
|
434
434
|
|
435
|
-
def _validation(self) -> None:
|
436
|
-
if not isinstance(self.risk_measure, RiskMeasure):
|
437
|
-
raise TypeError("risk_measure must be of type `RiskMeasure`")
|
438
|
-
if self.min_weights < 0:
|
439
|
-
raise ValueError(
|
440
|
-
"Risk Budgeting must have non negative `min_weights` constraint"
|
441
|
-
" otherwise the problem becomes non-convex."
|
442
|
-
)
|
443
|
-
|
444
435
|
def fit(self, X: npt.ArrayLike, y=None, **fit_params) -> "RiskBudgeting":
|
445
436
|
"""Fit the Risk Budgeting Optimization estimator.
|
446
437
|
|
@@ -462,8 +453,10 @@ class RiskBudgeting(ConvexOptimization):
|
|
462
453
|
routed_params = skm.process_routing(self, "fit", **fit_params)
|
463
454
|
|
464
455
|
self._check_feature_names(X, reset=True)
|
465
|
-
|
466
|
-
self.
|
456
|
+
|
457
|
+
if not isinstance(self.risk_measure, RiskMeasure):
|
458
|
+
raise TypeError("risk_measure must be of type `RiskMeasure`")
|
459
|
+
|
467
460
|
# Used to avoid adding multiple times similar constrains linked to identical
|
468
461
|
# risk models
|
469
462
|
self.prior_estimator_ = check_estimator(
|
@@ -518,7 +511,7 @@ class RiskBudgeting(ConvexOptimization):
|
|
518
511
|
|
519
512
|
# weight constraints
|
520
513
|
constraints += self._get_weight_constraints(
|
521
|
-
n_assets=n_assets, w=w, factor=factor
|
514
|
+
n_assets=n_assets, w=w, factor=factor, allow_negative_weights=False
|
522
515
|
)
|
523
516
|
|
524
517
|
parameters_values = []
|
@@ -653,7 +653,7 @@ class Population(list):
|
|
653
653
|
spacing: float | None = None,
|
654
654
|
display_sub_ptf_name: bool = True,
|
655
655
|
) -> go.Figure:
|
656
|
-
"""Plot the contribution of each asset to a given measure of the portfolios
|
656
|
+
r"""Plot the contribution of each asset to a given measure of the portfolios
|
657
657
|
in the population.
|
658
658
|
|
659
659
|
Parameters
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from skfolio.pre_selection._drop_correlated import DropCorrelated
|
2
|
+
from skfolio.pre_selection._select_complete import SelectComplete
|
3
|
+
from skfolio.pre_selection._select_k_extremes import SelectKExtremes
|
4
|
+
from skfolio.pre_selection._select_non_dominated import SelectNonDominated
|
5
|
+
from skfolio.pre_selection._select_non_expiring import SelectNonExpiring
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"DropCorrelated",
|
9
|
+
"SelectKExtremes",
|
10
|
+
"SelectNonDominated",
|
11
|
+
"SelectComplete",
|
12
|
+
"SelectNonExpiring",
|
13
|
+
]
|
@@ -0,0 +1,108 @@
|
|
1
|
+
"""Pre-selection DropCorrelated module"""
|
2
|
+
|
3
|
+
# Copyright (c) 2023
|
4
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
5
|
+
# License: BSD 3 clause
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import numpy.typing as npt
|
9
|
+
import sklearn.base as skb
|
10
|
+
import sklearn.feature_selection as skf
|
11
|
+
import sklearn.utils.validation as skv
|
12
|
+
|
13
|
+
|
14
|
+
class DropCorrelated(skf.SelectorMixin, skb.BaseEstimator):
|
15
|
+
"""Transformer for dropping highly correlated assets.
|
16
|
+
|
17
|
+
Simply removing all correlation pairs above the threshold will remove more assets
|
18
|
+
than necessary and a naive sequential removal is suboptimal and depends on the
|
19
|
+
initial assets ordering.
|
20
|
+
|
21
|
+
Let's suppose X,Y,Z are three random variables with corr(X,Y) and corr(X,Z) above
|
22
|
+
the threshold and corr(Y,Z) below.
|
23
|
+
The first approach would remove X,Y,Z and the second approach would remove either
|
24
|
+
Y and Z or X depending on the initial ordering.
|
25
|
+
|
26
|
+
To avoid these shortcomings, we implement the below algorithm:
|
27
|
+
|
28
|
+
* Step 1: select all correlation pairs above the threshold.
|
29
|
+
* Step 2: sort all the selected correlation pairs from highest to lowest.
|
30
|
+
* Step 3: for each pair, if none of the two assets has been removed, keep the
|
31
|
+
asset with the lowest average correlation against the other assets.
|
32
|
+
|
33
|
+
Parameters
|
34
|
+
----------
|
35
|
+
threshold : float, default=0.95
|
36
|
+
Correlation threshold. The default value is `0.95`.
|
37
|
+
|
38
|
+
absolute : bool, default=False
|
39
|
+
If this is set to True, we take the absolute value of the correlation. This has
|
40
|
+
for effect to also include negatively correlated assets.
|
41
|
+
|
42
|
+
Attributes
|
43
|
+
----------
|
44
|
+
to_keep_ : ndarray of shape (n_assets, )
|
45
|
+
Boolean array indicating which assets are remaining.
|
46
|
+
|
47
|
+
n_features_in_ : int
|
48
|
+
Number of assets seen during `fit`.
|
49
|
+
|
50
|
+
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
51
|
+
Names of assets seen during `fit`. Defined only when `X`
|
52
|
+
has assets names that are all strings.
|
53
|
+
"""
|
54
|
+
|
55
|
+
to_keep_: np.ndarray
|
56
|
+
|
57
|
+
def __init__(self, threshold: float = 0.95, absolute: bool = False):
|
58
|
+
self.threshold = threshold
|
59
|
+
self.absolute = absolute
|
60
|
+
|
61
|
+
def fit(self, X: npt.ArrayLike, y=None):
|
62
|
+
"""Run the correlation transformer and get the appropriate assets.
|
63
|
+
|
64
|
+
Parameters
|
65
|
+
----------
|
66
|
+
X : array-like of shape (n_observations, n_assets)
|
67
|
+
Price returns of the assets.
|
68
|
+
|
69
|
+
y : Ignored
|
70
|
+
Not used, present for API consistency by convention.
|
71
|
+
|
72
|
+
Returns
|
73
|
+
-------
|
74
|
+
self : DropCorrelated
|
75
|
+
Fitted estimator.
|
76
|
+
"""
|
77
|
+
X = self._validate_data(X)
|
78
|
+
if not -1 <= self.threshold <= 1:
|
79
|
+
raise ValueError("`threshold` must be between -1 and 1")
|
80
|
+
|
81
|
+
n_assets = X.shape[1]
|
82
|
+
corr = np.corrcoef(X.T)
|
83
|
+
mean_corr = corr.mean(axis=0)
|
84
|
+
|
85
|
+
triu_idx = np.triu_indices(n_assets, 1)
|
86
|
+
|
87
|
+
# select all correlation pairs above the threshold
|
88
|
+
selected_idx = np.argwhere(corr[triu_idx] > self.threshold).flatten()
|
89
|
+
|
90
|
+
# sort all the selected correlation pairs from highest to lowest
|
91
|
+
selected_idx = selected_idx[np.argsort(-corr[triu_idx][selected_idx])]
|
92
|
+
|
93
|
+
# for each pair, if none of the two assets has been removed, keep the asset with
|
94
|
+
# the lowest average correlation with other assets
|
95
|
+
to_remove = set()
|
96
|
+
for idx in selected_idx:
|
97
|
+
i, j = triu_idx[0][idx], triu_idx[1][idx]
|
98
|
+
if i not in to_remove and j not in to_remove:
|
99
|
+
if mean_corr[i] > mean_corr[j]:
|
100
|
+
to_remove.add(i)
|
101
|
+
else:
|
102
|
+
to_remove.add(j)
|
103
|
+
self.to_keep_ = ~np.isin(np.arange(n_assets), list(to_remove))
|
104
|
+
return self
|
105
|
+
|
106
|
+
def _get_support_mask(self):
|
107
|
+
skv.check_is_fitted(self)
|
108
|
+
return self.to_keep_
|
@@ -0,0 +1,116 @@
|
|
1
|
+
"""pre-selection SelectComplete module"""
|
2
|
+
|
3
|
+
# Copyright (c) 2023
|
4
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
5
|
+
# License: BSD 3 clause
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import numpy.typing as npt
|
9
|
+
import sklearn.base as skb
|
10
|
+
import sklearn.feature_selection as skf
|
11
|
+
import sklearn.utils.validation as skv
|
12
|
+
|
13
|
+
|
14
|
+
class SelectComplete(skf.SelectorMixin, skb.BaseEstimator):
|
15
|
+
"""
|
16
|
+
Transformer to select assets with complete data across the entire observation
|
17
|
+
period.
|
18
|
+
|
19
|
+
This transformer removes assets (columns) that have missing values (NaNs) at the
|
20
|
+
beginning or end of the period.
|
21
|
+
|
22
|
+
This transformer is especially useful for financial datasets where assets
|
23
|
+
(e.g., stocks, bonds) may have data gaps due to late inception (assets that started
|
24
|
+
trading later), early expiry or default (assets that stopped trading before the
|
25
|
+
end of the period).
|
26
|
+
|
27
|
+
If missing values are not at the beginning or end but occur between non-missing
|
28
|
+
values, the asset is not removed unless `drop_assets_with_internal_nan` is set to
|
29
|
+
`True`.
|
30
|
+
|
31
|
+
Parameters
|
32
|
+
----------
|
33
|
+
drop_assets_with_internal_nan : bool, default=False
|
34
|
+
If set to True, assets with missing values (NaNs) that appear between
|
35
|
+
non-missing values (i.e., internal NaNs) will also be removed. By default,
|
36
|
+
only assets with leading or trailing NaNs are removed.
|
37
|
+
|
38
|
+
Attributes
|
39
|
+
----------
|
40
|
+
to_keep_ : ndarray of shape (n_assets, )
|
41
|
+
Boolean array indicating which assets are remaining.
|
42
|
+
|
43
|
+
n_features_in_ : int
|
44
|
+
Number of assets seen during `fit`.
|
45
|
+
|
46
|
+
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
47
|
+
Names of features seen during `fit`. Defined only when `X`
|
48
|
+
has feature names that are all strings.
|
49
|
+
|
50
|
+
Examples
|
51
|
+
--------
|
52
|
+
|
53
|
+
>>> import numpy as np
|
54
|
+
>>> import pandas as pd
|
55
|
+
>>> from skfolio.pre_selection import SelectComplete
|
56
|
+
>>> X = pd.DataFrame({
|
57
|
+
... 'asset1': [np.nan, np.nan, 2, 3, 4], # Starts late (inception)
|
58
|
+
... 'asset2': [1, 2, 3, 4, 5], # Complete data
|
59
|
+
... 'asset3': [1, 2, 3, np.nan, 5], # Missing values within data
|
60
|
+
... 'asset4': [1, 2, 3, 4, np.nan] # Ends early (expiration)
|
61
|
+
... })
|
62
|
+
>>> selector = SelectComplete()
|
63
|
+
>>> selector.fit_transform(X)
|
64
|
+
array([[ 1., 1.],
|
65
|
+
[ 2., 2.],
|
66
|
+
[ 3., 3.],
|
67
|
+
[ 4., nan],
|
68
|
+
[ 5., 5.]])
|
69
|
+
>>> selector = SelectComplete(drop_assets_with_internal_nan=True)
|
70
|
+
>>> selector.fit_transform(X)
|
71
|
+
array([[1.],
|
72
|
+
[2.],
|
73
|
+
[3.],
|
74
|
+
[4.],
|
75
|
+
[5.]])
|
76
|
+
"""
|
77
|
+
|
78
|
+
to_keep_: np.ndarray
|
79
|
+
|
80
|
+
def __init__(self, drop_assets_with_internal_nan: bool = False):
|
81
|
+
self.drop_assets_with_internal_nan = drop_assets_with_internal_nan
|
82
|
+
|
83
|
+
def fit(self, X: npt.ArrayLike, y=None) -> "SelectComplete":
|
84
|
+
"""Run the SelectComplete transformer and get the appropriate assets.
|
85
|
+
|
86
|
+
Parameters
|
87
|
+
----------
|
88
|
+
X : array-like of shape (n_observations, n_assets)
|
89
|
+
Returns of the assets.
|
90
|
+
|
91
|
+
y : Ignored
|
92
|
+
Not used, present for API consistency by convention.
|
93
|
+
|
94
|
+
Returns
|
95
|
+
-------
|
96
|
+
self : SelectComplete
|
97
|
+
Fitted estimator.
|
98
|
+
"""
|
99
|
+
# Validate by allowing NaNs
|
100
|
+
X = self._validate_data(X, force_all_finite="allow-nan")
|
101
|
+
|
102
|
+
if self.drop_assets_with_internal_nan:
|
103
|
+
# Identify columns with any NaNs
|
104
|
+
self.to_keep_ = ~np.isnan(X).any(axis=0)
|
105
|
+
else:
|
106
|
+
# Identify columns with no leading or trailing NaNs
|
107
|
+
self.to_keep_ = ~np.isnan(X[0, :]) & ~np.isnan(X[-1, :])
|
108
|
+
|
109
|
+
return self
|
110
|
+
|
111
|
+
def _get_support_mask(self):
|
112
|
+
skv.check_is_fitted(self)
|
113
|
+
return self.to_keep_
|
114
|
+
|
115
|
+
def _more_tags(self):
|
116
|
+
return {"allow_nan": True}
|
@@ -0,0 +1,100 @@
|
|
1
|
+
"""Pre-selection SelectKExtremes module"""
|
2
|
+
|
3
|
+
# Copyright (c) 2023
|
4
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
5
|
+
# License: BSD 3 clause
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import numpy.typing as npt
|
9
|
+
import sklearn.base as skb
|
10
|
+
import sklearn.feature_selection as skf
|
11
|
+
import sklearn.utils.validation as skv
|
12
|
+
|
13
|
+
import skfolio.typing as skt
|
14
|
+
from skfolio.measures import RatioMeasure
|
15
|
+
from skfolio.population import Population
|
16
|
+
from skfolio.portfolio import Portfolio
|
17
|
+
|
18
|
+
|
19
|
+
class SelectKExtremes(skf.SelectorMixin, skb.BaseEstimator):
|
20
|
+
"""Transformer for selecting the `k` best or worst assets.
|
21
|
+
|
22
|
+
Keep the `k` best or worst assets according to a given measure.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
k : int, default=10
|
27
|
+
Number of assets to select. If `k` is higher than the number of assets, all
|
28
|
+
assets are selected.
|
29
|
+
|
30
|
+
measure : Measure, default=RatioMeasure.SHARPE_RATIO
|
31
|
+
The :ref:`measure <measures_ref>` used to sort the assets.
|
32
|
+
The default is `RatioMeasure.SHARPE_RATIO`.
|
33
|
+
|
34
|
+
highest : bool, default=True
|
35
|
+
If this is set to True, the `k` assets with the highest `measure` are selected,
|
36
|
+
otherwise it is the `k` lowest.
|
37
|
+
|
38
|
+
Attributes
|
39
|
+
----------
|
40
|
+
to_keep_ : ndarray of shape (n_assets, )
|
41
|
+
Boolean array indicating which assets are remaining.
|
42
|
+
|
43
|
+
n_features_in_ : int
|
44
|
+
Number of assets seen during `fit`.
|
45
|
+
|
46
|
+
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
47
|
+
Names of features seen during `fit`. Defined only when `X`
|
48
|
+
has feature names that are all strings.
|
49
|
+
"""
|
50
|
+
|
51
|
+
to_keep_: np.ndarray
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
k: int = 10,
|
56
|
+
measure: skt.Measure = RatioMeasure.SHARPE_RATIO,
|
57
|
+
highest: bool = True,
|
58
|
+
):
|
59
|
+
self.k = k
|
60
|
+
self.measure = measure
|
61
|
+
self.highest = highest
|
62
|
+
|
63
|
+
def fit(self, X: npt.ArrayLike, y=None) -> "SelectKExtremes":
|
64
|
+
"""Run the SelectKExtremes transformer and get the appropriate assets.
|
65
|
+
|
66
|
+
Parameters
|
67
|
+
----------
|
68
|
+
X : array-like of shape (n_observations, n_assets)
|
69
|
+
Price returns of the assets.
|
70
|
+
|
71
|
+
y : Ignored
|
72
|
+
Not used, present for API consistency by convention.
|
73
|
+
|
74
|
+
Returns
|
75
|
+
-------
|
76
|
+
self : SelectKExtremes
|
77
|
+
Fitted estimator.
|
78
|
+
"""
|
79
|
+
X = self._validate_data(X)
|
80
|
+
k = int(self.k)
|
81
|
+
if k <= 0:
|
82
|
+
raise ValueError("`k` must be strictly positive")
|
83
|
+
n_assets = X.shape[1]
|
84
|
+
# Build a population of single assets portfolio
|
85
|
+
population = Population([])
|
86
|
+
for i in range(n_assets):
|
87
|
+
weights = np.zeros(n_assets)
|
88
|
+
weights[i] = 1
|
89
|
+
population.append(Portfolio(X=X, weights=weights))
|
90
|
+
|
91
|
+
selected = population.sort_measure(measure=self.measure, reverse=self.highest)[
|
92
|
+
:k
|
93
|
+
]
|
94
|
+
selected_idx = [x.nonzero_assets_index[0] for x in selected]
|
95
|
+
self.to_keep_ = np.isin(np.arange(n_assets), selected_idx)
|
96
|
+
return self
|
97
|
+
|
98
|
+
def _get_support_mask(self):
|
99
|
+
skv.check_is_fitted(self)
|
100
|
+
return self.to_keep_
|
@@ -0,0 +1,161 @@
|
|
1
|
+
"""Pre-selection SelectNonDominated module"""
|
2
|
+
|
3
|
+
# Copyright (c) 2023
|
4
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
5
|
+
# License: BSD 3 clause
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import numpy.typing as npt
|
9
|
+
import sklearn.base as skb
|
10
|
+
import sklearn.feature_selection as skf
|
11
|
+
import sklearn.utils.validation as skv
|
12
|
+
|
13
|
+
import skfolio.typing as skt
|
14
|
+
from skfolio.population import Population
|
15
|
+
from skfolio.portfolio import Portfolio
|
16
|
+
|
17
|
+
|
18
|
+
class SelectNonDominated(skf.SelectorMixin, skb.BaseEstimator):
|
19
|
+
"""Transformer for selecting non dominated assets.
|
20
|
+
|
21
|
+
Pre-selection based on the Assets Preselection Process 2 [1]_.
|
22
|
+
|
23
|
+
Good single asset (for example with high return and low risk) is likely to
|
24
|
+
contribute to the final optimized portfolio. Each asset is considered as a portfolio
|
25
|
+
and these assets are ranked using the non-domination sorting method. The selection
|
26
|
+
is based on the ranks assigned to each asset based on their fitness until the number
|
27
|
+
of selected assets reaches the user-defined number.
|
28
|
+
|
29
|
+
Considering only the fitness of individual asset is insufficient because a pair of
|
30
|
+
negatively correlated assets has the potential to reduce the risk. Therefore,
|
31
|
+
negatively correlated pairs of assets are also considered.
|
32
|
+
|
33
|
+
Parameters
|
34
|
+
----------
|
35
|
+
min_n_assets : int, optional
|
36
|
+
The minimum number of assets to select. If `min_n_assets` is reached before the
|
37
|
+
end of the current non-dominated front, we return the remaining assets of this
|
38
|
+
front. This is because all assets in the same front have same rank.
|
39
|
+
The default (`None`) is to select the first front.
|
40
|
+
|
41
|
+
threshold : float, default=0.0
|
42
|
+
Asset pair with a correlation below this threshold are included in the
|
43
|
+
non-domination sorting. The default value is `0.0`.
|
44
|
+
|
45
|
+
fitness_measures : list[Measure], optional
|
46
|
+
A list of :ref:`measure <measures_ref>` used to compute the portfolio fitness.
|
47
|
+
The fitness is used to compare portfolios in terms of domination, compute the
|
48
|
+
pareto fronts and run the portfolio selection using non-denominated sorting.
|
49
|
+
The default (`None`) is to use the list [PerfMeasure.MEAN, RiskMeasure.VARIANCE]
|
50
|
+
|
51
|
+
Attributes
|
52
|
+
----------
|
53
|
+
to_keep_ : ndarray of shape (n_assets, )
|
54
|
+
Boolean array indicating which assets are remaining.
|
55
|
+
|
56
|
+
n_features_in_ : int
|
57
|
+
Number of assets seen during `fit`.
|
58
|
+
|
59
|
+
feature_names_in_ : ndarray of shape (`n_features_in_`,)
|
60
|
+
Names of features seen during `fit`. Defined only when `X`
|
61
|
+
has feature names that are all strings.
|
62
|
+
|
63
|
+
References
|
64
|
+
----------
|
65
|
+
.. [1] "Large-Scale Portfolio Optimization Using Multi-objective Evolutionary
|
66
|
+
Algorithms and Preselection Methods",
|
67
|
+
B.Y. Qu and Q.Zhou (2017).
|
68
|
+
"""
|
69
|
+
|
70
|
+
to_keep_: np.ndarray
|
71
|
+
|
72
|
+
def __init__(
|
73
|
+
self,
|
74
|
+
min_n_assets: int | None = None,
|
75
|
+
threshold: float = -0.5,
|
76
|
+
fitness_measures: list[skt.Measure] | None = None,
|
77
|
+
):
|
78
|
+
self.min_n_assets = min_n_assets
|
79
|
+
self.threshold = threshold
|
80
|
+
self.fitness_measures = fitness_measures
|
81
|
+
|
82
|
+
def fit(self, X: npt.ArrayLike, y=None):
|
83
|
+
"""Run the Non Dominated transformer and get the appropriate assets.
|
84
|
+
|
85
|
+
Parameters
|
86
|
+
----------
|
87
|
+
X : array-like of shape (n_observations, n_assets)
|
88
|
+
Price returns of the assets.
|
89
|
+
|
90
|
+
y : Ignored
|
91
|
+
Not used, present for API consistency by convention.
|
92
|
+
|
93
|
+
Returns
|
94
|
+
-------
|
95
|
+
self : SelectNonDominated
|
96
|
+
Fitted estimator.
|
97
|
+
"""
|
98
|
+
X = self._validate_data(X)
|
99
|
+
if not -1 <= self.threshold <= 1:
|
100
|
+
raise ValueError("`threshold` must be between -1 and 1")
|
101
|
+
n_assets = X.shape[1]
|
102
|
+
|
103
|
+
if self.min_n_assets is not None and self.min_n_assets >= n_assets:
|
104
|
+
self.to_keep_ = np.full(n_assets, True)
|
105
|
+
return self
|
106
|
+
|
107
|
+
# Build a population of portfolio
|
108
|
+
population = Population([])
|
109
|
+
# Add single assets
|
110
|
+
for i in range(n_assets):
|
111
|
+
weights = np.zeros(n_assets)
|
112
|
+
weights[i] = 1
|
113
|
+
population.append(
|
114
|
+
Portfolio(X=X, weights=weights, fitness_measures=self.fitness_measures)
|
115
|
+
)
|
116
|
+
|
117
|
+
# Add pairs with correlation below threshold with minimum variance
|
118
|
+
# ptf_variance = sigma1^2 w1^2 + sigma2^2 w2^2 + 2 sigma12 w1 w2 (1)
|
119
|
+
# with w1 + w2 = 1
|
120
|
+
# To find the minimum we substitute w2 = 1 - w1 in (1) and differentiate with
|
121
|
+
# respect to w1 and set to zero.
|
122
|
+
# By solving the obtained equation, we get:
|
123
|
+
# w1 = (sigma2^2 - sigma12) / (sigma1^2 + sigma2^2 - 2 sigma12)
|
124
|
+
# w2 = 1 - w1
|
125
|
+
|
126
|
+
corr = np.corrcoef(X.T)
|
127
|
+
covariance = np.cov(X.T)
|
128
|
+
for i, j in zip(*np.triu_indices(n_assets, 1), strict=True):
|
129
|
+
if corr[i, j] < self.threshold:
|
130
|
+
cov = covariance[i, j]
|
131
|
+
var1 = covariance[i, i]
|
132
|
+
var2 = covariance[j, j]
|
133
|
+
weights = np.zeros(n_assets)
|
134
|
+
weights[i] = (var2 - cov) / (var1 + var2 - 2 * cov)
|
135
|
+
weights[j] = 1 - weights[i]
|
136
|
+
population.append(
|
137
|
+
Portfolio(
|
138
|
+
X=X, weights=weights, fitness_measures=self.fitness_measures
|
139
|
+
)
|
140
|
+
)
|
141
|
+
|
142
|
+
fronts = population.non_denominated_sort(
|
143
|
+
first_front_only=self.min_n_assets is None
|
144
|
+
)
|
145
|
+
new_assets_idx = set()
|
146
|
+
i = 0
|
147
|
+
while i < len(fronts):
|
148
|
+
if (
|
149
|
+
self.min_n_assets is not None
|
150
|
+
and len(new_assets_idx) > self.min_n_assets
|
151
|
+
):
|
152
|
+
break
|
153
|
+
for idx in fronts[i]:
|
154
|
+
new_assets_idx.update(population[idx].nonzero_assets_index)
|
155
|
+
i += 1
|
156
|
+
self.to_keep_ = np.isin(np.arange(n_assets), list(new_assets_idx))
|
157
|
+
return self
|
158
|
+
|
159
|
+
def _get_support_mask(self):
|
160
|
+
skv.check_is_fitted(self)
|
161
|
+
return self.to_keep_
|