skfolio 0.4.0__py3-none-any.whl → 0.4.2__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/exceptions.py +5 -0
- skfolio/model_selection/_combinatorial.py +1 -1
- skfolio/optimization/convex/_base.py +13 -7
- skfolio/optimization/convex/_distributionally_robust.py +3 -3
- skfolio/optimization/convex/_maximum_diversification.py +3 -3
- skfolio/optimization/convex/_mean_risk.py +3 -3
- skfolio/optimization/convex/_risk_budgeting.py +4 -4
- skfolio/population/_population.py +2 -2
- skfolio/prior/_black_litterman.py +4 -1
- skfolio/utils/equations.py +166 -78
- {skfolio-0.4.0.dist-info → skfolio-0.4.2.dist-info}/METADATA +2 -2
- {skfolio-0.4.0.dist-info → skfolio-0.4.2.dist-info}/RECORD +15 -15
- {skfolio-0.4.0.dist-info → skfolio-0.4.2.dist-info}/WHEEL +1 -1
- {skfolio-0.4.0.dist-info → skfolio-0.4.2.dist-info}/LICENSE +0 -0
- {skfolio-0.4.0.dist-info → skfolio-0.4.2.dist-info}/top_level.txt +0 -0
skfolio/exceptions.py
CHANGED
@@ -12,6 +12,7 @@ __all__ = [
|
|
12
12
|
"EquationToMatrixError",
|
13
13
|
"GroupNotFoundError",
|
14
14
|
"NonPositiveVarianceError",
|
15
|
+
"DuplicateGroupsError",
|
15
16
|
]
|
16
17
|
|
17
18
|
|
@@ -27,5 +28,9 @@ class GroupNotFoundError(Exception):
|
|
27
28
|
"""Group name not found in the groups"""
|
28
29
|
|
29
30
|
|
31
|
+
class DuplicateGroupsError(Exception):
|
32
|
+
"""Group name appear in multiple group levels"""
|
33
|
+
|
34
|
+
|
30
35
|
class NonPositiveVarianceError(Exception):
|
31
36
|
"""Variance negative or null"""
|
@@ -377,7 +377,7 @@ class CombinatorialPurgedCV(BaseCombinatorialCV):
|
|
377
377
|
]
|
378
378
|
values = self.index_train_test_.T
|
379
379
|
values = np.insert(values, 0, np.arange(n_samples), axis=0)
|
380
|
-
fill_color = np.select(cond, ["green", "blue", "red"]).T
|
380
|
+
fill_color = np.select(cond, ["green", "blue", "red"], default="green").T
|
381
381
|
fill_color = fill_color.astype(object)
|
382
382
|
fill_color = np.insert(
|
383
383
|
fill_color, 0, np.array(["darkblue" for _ in range(n_samples)]), axis=0
|
@@ -290,7 +290,7 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
290
290
|
|
291
291
|
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
292
292
|
* "ref1 >= 2.9 * ref2"
|
293
|
-
* "ref1
|
293
|
+
* "ref1 == ref2"
|
294
294
|
* "ref1 >= ref1"
|
295
295
|
|
296
296
|
With "ref1", "ref2" ... the assets names or the groups names provided
|
@@ -302,8 +302,8 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
302
302
|
|
303
303
|
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
304
304
|
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
305
|
-
* "US
|
306
|
-
* "Equity
|
305
|
+
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
306
|
+
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
307
307
|
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
308
308
|
|
309
309
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
@@ -733,15 +733,21 @@ class ConvexOptimization(BaseOptimization, ABC):
|
|
733
733
|
),
|
734
734
|
name="groups",
|
735
735
|
)
|
736
|
-
|
736
|
+
a_eq, b_eq, a_ineq, b_ineq = equations_to_matrix(
|
737
737
|
groups=groups,
|
738
738
|
equations=self.linear_constraints,
|
739
739
|
raise_if_group_missing=False,
|
740
740
|
)
|
741
|
-
if
|
741
|
+
if len(a_eq) != 0:
|
742
742
|
constraints.append(
|
743
|
-
|
744
|
-
-
|
743
|
+
a_eq @ w * self._scale_constraints
|
744
|
+
- b_eq * factor * self._scale_constraints
|
745
|
+
== 0
|
746
|
+
)
|
747
|
+
if len(a_ineq) != 0:
|
748
|
+
constraints.append(
|
749
|
+
a_ineq @ w * self._scale_constraints
|
750
|
+
- b_ineq * factor * self._scale_constraints
|
745
751
|
<= 0
|
746
752
|
)
|
747
753
|
|
@@ -125,7 +125,7 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
125
125
|
|
126
126
|
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
127
127
|
* "ref1 >= 2.9 * ref2"
|
128
|
-
* "ref1
|
128
|
+
* "ref1 == ref2"
|
129
129
|
* "ref1 >= ref1"
|
130
130
|
|
131
131
|
With "ref1", "ref2" ... the assets names or the groups names provided
|
@@ -137,8 +137,8 @@ class DistributionallyRobustCVaR(ConvexOptimization):
|
|
137
137
|
|
138
138
|
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
139
139
|
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
140
|
-
* "US
|
141
|
-
* "Equity
|
140
|
+
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
141
|
+
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
142
142
|
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
143
143
|
|
144
144
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
@@ -201,7 +201,7 @@ class MaximumDiversification(MeanRisk):
|
|
201
201
|
|
202
202
|
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
203
203
|
* "ref1 >= 2.9 * ref2"
|
204
|
-
* "ref1
|
204
|
+
* "ref1 == ref2"
|
205
205
|
* "ref1 >= ref1"
|
206
206
|
|
207
207
|
With "ref1", "ref2" ... the assets names or the groups names provided
|
@@ -213,8 +213,8 @@ class MaximumDiversification(MeanRisk):
|
|
213
213
|
|
214
214
|
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
215
215
|
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
216
|
-
* "US
|
217
|
-
* "Equity
|
216
|
+
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
217
|
+
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
218
218
|
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
219
219
|
|
220
220
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
@@ -334,7 +334,7 @@ class MeanRisk(ConvexOptimization):
|
|
334
334
|
|
335
335
|
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
336
336
|
* "ref1 >= 2.9 * ref2"
|
337
|
-
* "ref1
|
337
|
+
* "ref1 == ref2"
|
338
338
|
* "ref1 >= ref1"
|
339
339
|
|
340
340
|
With "ref1", "ref2" ... the assets names or the groups names provided
|
@@ -346,8 +346,8 @@ class MeanRisk(ConvexOptimization):
|
|
346
346
|
|
347
347
|
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
348
348
|
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
349
|
-
* "US
|
350
|
-
* "Equity
|
349
|
+
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
350
|
+
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
351
351
|
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
352
352
|
|
353
353
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
@@ -212,12 +212,12 @@ class RiskBudgeting(ConvexOptimization):
|
|
212
212
|
The default (`None`) means no previous weights.
|
213
213
|
|
214
214
|
linear_constraints : array-like of shape (n_constraints,), optional
|
215
|
-
|
215
|
+
Linear constraints.
|
216
216
|
The linear constraints must match any of following patterns:
|
217
217
|
|
218
218
|
* "2.5 * ref1 + 0.10 * ref2 + 0.0013 <= 2.5 * ref3"
|
219
219
|
* "ref1 >= 2.9 * ref2"
|
220
|
-
* "ref1
|
220
|
+
* "ref1 == ref2"
|
221
221
|
* "ref1 >= ref1"
|
222
222
|
|
223
223
|
With "ref1", "ref2" ... the assets names or the groups names provided
|
@@ -229,8 +229,8 @@ class RiskBudgeting(ConvexOptimization):
|
|
229
229
|
|
230
230
|
* "SPX >= 0.10" --> SPX weight must be greater than 10% (note that you can also use `min_weights`)
|
231
231
|
* "SX5E + TLT >= 0.2" --> the sum of SX5E and TLT weights must be greater than 20%
|
232
|
-
* "US
|
233
|
-
* "Equity
|
232
|
+
* "US == 0.7" --> the sum of all US weights must be equal to 70%
|
233
|
+
* "Equity == 3 * Bond" --> the sum of all Equity weights must be equal to 3 times the sum of all Bond weights.
|
234
234
|
* "2*SPX + 3*Europe <= Bond + 0.05" --> mixing assets and group constraints
|
235
235
|
|
236
236
|
groups : dict[str, list[str]] or array-like of shape (n_groups, n_assets), optional
|
@@ -410,7 +410,7 @@ class Population(list):
|
|
410
410
|
spacing: float | None = None,
|
411
411
|
display_sub_ptf_name: bool = True,
|
412
412
|
) -> pd.DataFrame:
|
413
|
-
"""Contribution of each asset to a given measure of each portfolio in the
|
413
|
+
r"""Contribution of each asset to a given measure of each portfolio in the
|
414
414
|
population.
|
415
415
|
|
416
416
|
Parameters
|
@@ -420,7 +420,7 @@ class Population(list):
|
|
420
420
|
|
421
421
|
spacing : float, optional
|
422
422
|
Spacing "h" of the finite difference:
|
423
|
-
:math:`contribution(wi)= \frac{measure(wi-h) - measure(wi+h)}{2h}
|
423
|
+
:math:`contribution(wi)= \frac{measure(wi-h) - measure(wi+h)}{2h}`.
|
424
424
|
|
425
425
|
display_sub_ptf_name : bool, default=True
|
426
426
|
If this is set to True, each sub-portfolio name composing a multi-period
|
@@ -208,7 +208,7 @@ class BlackLitterman(BasePrior):
|
|
208
208
|
),
|
209
209
|
name="groups",
|
210
210
|
)
|
211
|
-
self.picking_matrix_, self.views_ = equations_to_matrix(
|
211
|
+
self.picking_matrix_, self.views_, a_ineq, b_ineq = equations_to_matrix(
|
212
212
|
groups=self.groups_,
|
213
213
|
equations=views,
|
214
214
|
sum_to_one=True,
|
@@ -216,6 +216,9 @@ class BlackLitterman(BasePrior):
|
|
216
216
|
names=("groups", "views"),
|
217
217
|
)
|
218
218
|
|
219
|
+
if len(a_ineq) != 0:
|
220
|
+
raise ValueError("Inequalities (<=, >=) are not supported in views")
|
221
|
+
|
219
222
|
if self.view_confidences is None:
|
220
223
|
omega = np.diag(
|
221
224
|
np.diag(
|
skfolio/utils/equations.py
CHANGED
@@ -10,10 +10,24 @@ import warnings
|
|
10
10
|
import numpy as np
|
11
11
|
import numpy.typing as npt
|
12
12
|
|
13
|
-
from skfolio.exceptions import
|
13
|
+
from skfolio.exceptions import (
|
14
|
+
DuplicateGroupsError,
|
15
|
+
EquationToMatrixError,
|
16
|
+
GroupNotFoundError,
|
17
|
+
)
|
14
18
|
|
15
19
|
__all__ = ["equations_to_matrix"]
|
16
20
|
|
21
|
+
_EQUALITY_OPERATORS = {"==", "="}
|
22
|
+
_INEQUALITY_OPERATORS = {">=", "<="}
|
23
|
+
_COMPARISON_OPERATORS = _EQUALITY_OPERATORS.union(_INEQUALITY_OPERATORS)
|
24
|
+
_SUB_ADD_OPERATORS = {"-", "+"}
|
25
|
+
_MUL_OPERATORS = {"*"}
|
26
|
+
_NON_MUL_OPERATORS = _COMPARISON_OPERATORS.union(_SUB_ADD_OPERATORS)
|
27
|
+
_OPERATORS = _NON_MUL_OPERATORS.union(_MUL_OPERATORS)
|
28
|
+
_COMPARISON_OPERATOR_SIGNS = {">=": -1, "<=": 1, "==": 1, "=": 1}
|
29
|
+
_SUB_ADD_OPERATOR_SIGNS = {"+": 1, "-": -1}
|
30
|
+
|
17
31
|
|
18
32
|
def equations_to_matrix(
|
19
33
|
groups: npt.ArrayLike,
|
@@ -21,9 +35,9 @@ def equations_to_matrix(
|
|
21
35
|
sum_to_one: bool = False,
|
22
36
|
raise_if_group_missing: bool = False,
|
23
37
|
names: tuple[str, str] = ("groups", "equations"),
|
24
|
-
) -> tuple[np.ndarray, np.ndarray]:
|
38
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
25
39
|
"""Convert a list of linear equations into the left and right matrices of the
|
26
|
-
inequality A <= B.
|
40
|
+
inequality A <= B and equality A == B.
|
27
41
|
|
28
42
|
Parameters
|
29
43
|
----------
|
@@ -44,9 +58,9 @@ def equations_to_matrix(
|
|
44
58
|
|
45
59
|
Example of valid equation patterns:
|
46
60
|
* "number_1 * group_1 + number_3 <= number_4 * group_3 + number_5"
|
47
|
-
* "group_1
|
61
|
+
* "group_1 == number * group_2"
|
48
62
|
* "group_1 <= number"
|
49
|
-
* "group_1
|
63
|
+
* "group_1 == number"
|
50
64
|
|
51
65
|
"group_1" and "group_2" are the group names defined in `groups`.
|
52
66
|
The second expression means that the sum of all assets in "group_1" should be
|
@@ -57,8 +71,8 @@ def equations_to_matrix(
|
|
57
71
|
"Equity <= 3 * Bond",
|
58
72
|
"US >= 1.5",
|
59
73
|
"Europe >= 0.5 * Japan",
|
60
|
-
"Japan
|
61
|
-
"3*SPX + 5*SX5E
|
74
|
+
"Japan == 1",
|
75
|
+
"3*SPX + 5*SX5E == 2*TLT + 3",
|
62
76
|
]
|
63
77
|
|
64
78
|
sum_to_one : bool
|
@@ -76,41 +90,104 @@ def equations_to_matrix(
|
|
76
90
|
|
77
91
|
Returns
|
78
92
|
-------
|
79
|
-
|
80
|
-
|
93
|
+
left_equality: ndarray of shape (n_equations_equality, n_assets)
|
94
|
+
right_equality: ndarray of shape (n_equations_equality,)
|
81
95
|
The left and right matrices of the inequality A <= B.
|
82
|
-
|
83
|
-
|
96
|
+
|
97
|
+
left_inequality: ndarray of shape (n_equations_inequality, n_assets)
|
98
|
+
right_inequality: ndarray of shape (n_equations_inequality,)
|
99
|
+
The left and right matrices of the equality A == B.
|
84
100
|
"""
|
85
|
-
groups =
|
86
|
-
equations =
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
f"`{names[1]}` must be a 1D array, got {equations.ndim}D array instead."
|
94
|
-
)
|
101
|
+
groups = _validate_groups(groups, name=names[0])
|
102
|
+
equations = _validate_equations(equations, name=names[1])
|
103
|
+
|
104
|
+
a_equality = []
|
105
|
+
b_equality = []
|
106
|
+
|
107
|
+
a_inequality = []
|
108
|
+
b_inequality = []
|
95
109
|
|
96
|
-
|
97
|
-
n_assets = groups.shape[1]
|
98
|
-
a = np.zeros((n_equations, n_assets))
|
99
|
-
b = np.zeros(n_equations)
|
100
|
-
for i, string in enumerate(equations):
|
110
|
+
for string in equations:
|
101
111
|
try:
|
102
|
-
left, right = _string_to_equation(
|
112
|
+
left, right, is_inequality = _string_to_equation(
|
103
113
|
groups=groups,
|
104
114
|
string=string,
|
105
115
|
sum_to_one=sum_to_one,
|
106
116
|
)
|
107
|
-
|
108
|
-
|
117
|
+
if is_inequality:
|
118
|
+
a_inequality.append(left)
|
119
|
+
b_inequality.append(right)
|
120
|
+
else:
|
121
|
+
a_equality.append(left)
|
122
|
+
b_equality.append(right)
|
109
123
|
except GroupNotFoundError as e:
|
110
124
|
if raise_if_group_missing:
|
111
125
|
raise
|
112
126
|
warnings.warn(str(e), stacklevel=2)
|
113
|
-
return
|
127
|
+
return (
|
128
|
+
np.array(a_equality),
|
129
|
+
np.array(b_equality),
|
130
|
+
np.array(a_inequality),
|
131
|
+
np.array(b_inequality),
|
132
|
+
)
|
133
|
+
|
134
|
+
|
135
|
+
def _validate_groups(groups: npt.ArrayLike, name: str = "groups") -> np.ndarray:
|
136
|
+
"""Validate groups by checking its dim and if group names don't appear in multiple
|
137
|
+
levels and convert to numpy array.
|
138
|
+
|
139
|
+
Parameters
|
140
|
+
----------
|
141
|
+
groups : array-like of shape (n_groups, n_assets)
|
142
|
+
2D-array of strings.
|
143
|
+
|
144
|
+
Returns
|
145
|
+
-------
|
146
|
+
groups : ndarray of shape (n_groups, n_assets)
|
147
|
+
2D-array of strings.
|
148
|
+
"""
|
149
|
+
groups = np.asarray(groups)
|
150
|
+
if groups.ndim != 2:
|
151
|
+
raise ValueError(
|
152
|
+
f"`{name} must be a 2D array, got {groups.ndim}D array instead."
|
153
|
+
)
|
154
|
+
n = len(groups)
|
155
|
+
group_sets = [set(groups[i]) for i in range(n)]
|
156
|
+
for i in range(n - 1):
|
157
|
+
for e in group_sets[i]:
|
158
|
+
for j in range(i + 1, n):
|
159
|
+
if e in group_sets[j]:
|
160
|
+
raise DuplicateGroupsError(
|
161
|
+
f"'{e}' appear in two levels: {list(groups[i])} "
|
162
|
+
f"and {list(groups[i])}. "
|
163
|
+
f"{name} must be in only one level."
|
164
|
+
)
|
165
|
+
|
166
|
+
return groups
|
167
|
+
|
168
|
+
|
169
|
+
def _validate_equations(
|
170
|
+
equations: npt.ArrayLike, name: str = "equations"
|
171
|
+
) -> np.ndarray:
|
172
|
+
"""Validate equations by checking its dim and convert to numpy array.
|
173
|
+
|
174
|
+
Parameters
|
175
|
+
----------
|
176
|
+
equations : array-like of shape (n_equations,)
|
177
|
+
1D array of equations.
|
178
|
+
|
179
|
+
Returns
|
180
|
+
-------
|
181
|
+
equations : ndarray of shape (n_equations,)
|
182
|
+
1D array of equations.
|
183
|
+
"""
|
184
|
+
equations = np.asarray(equations)
|
185
|
+
|
186
|
+
if equations.ndim != 1:
|
187
|
+
raise ValueError(
|
188
|
+
f"`{name}` must be a 1D array, got {equations.ndim}D array instead."
|
189
|
+
)
|
190
|
+
return equations
|
114
191
|
|
115
192
|
|
116
193
|
def _matching_array(values: np.ndarray, key: str, sum_to_one: bool) -> np.ndarray:
|
@@ -145,11 +222,7 @@ def _matching_array(values: np.ndarray, key: str, sum_to_one: bool) -> np.ndarra
|
|
145
222
|
return arr / s
|
146
223
|
|
147
224
|
|
148
|
-
|
149
|
-
_operator_signs = {"+": 1, "-": -1}
|
150
|
-
|
151
|
-
|
152
|
-
def _inequality_operator_sign(operator: str) -> int:
|
225
|
+
def _comparison_operator_sign(operator: str) -> int:
|
153
226
|
"""Convert the operators '>=', "==" and '<=' into the corresponding integer
|
154
227
|
values -1, 1 and 1, respectively.
|
155
228
|
|
@@ -164,14 +237,14 @@ def _inequality_operator_sign(operator: str) -> int:
|
|
164
237
|
Operator sign: 1 or -1.
|
165
238
|
"""
|
166
239
|
try:
|
167
|
-
return
|
240
|
+
return _COMPARISON_OPERATOR_SIGNS[operator]
|
168
241
|
except KeyError:
|
169
242
|
raise EquationToMatrixError(
|
170
243
|
f"operator '{operator}' is not valid. It should be '<=' or '>='"
|
171
244
|
) from None
|
172
245
|
|
173
246
|
|
174
|
-
def
|
247
|
+
def _sub_add_operator_sign(operator: str) -> int:
|
175
248
|
"""Convert the operators '+' and '-' into 1 or -1
|
176
249
|
|
177
250
|
Parameters
|
@@ -185,7 +258,7 @@ def _operator_sign(operator: str) -> int:
|
|
185
258
|
Operator sign: 1 or -1.
|
186
259
|
"""
|
187
260
|
try:
|
188
|
-
return
|
261
|
+
return _SUB_ADD_OPERATOR_SIGNS[operator]
|
189
262
|
except KeyError:
|
190
263
|
raise EquationToMatrixError(
|
191
264
|
f"operator '{operator}' is not valid. It should be be '+' or '-'"
|
@@ -211,13 +284,41 @@ def _string_to_float(string: str) -> float:
|
|
211
284
|
raise EquationToMatrixError(f"Unable to convert {string} into float") from None
|
212
285
|
|
213
286
|
|
287
|
+
def _split_equation_string(string: str) -> list[str]:
|
288
|
+
"""Split an equation strings by operators"""
|
289
|
+
comp_pattern = "(?=" + "|".join([".+\\" + e for e in _COMPARISON_OPERATORS]) + ")"
|
290
|
+
if not bool(re.match(comp_pattern, string)):
|
291
|
+
raise EquationToMatrixError(
|
292
|
+
f"The string must contains a comparison operator: "
|
293
|
+
f"{list(_COMPARISON_OPERATORS)}"
|
294
|
+
)
|
295
|
+
|
296
|
+
# Regex to match only '>' and '<' but not '<=' or '>='
|
297
|
+
invalid_pattern = r"(?<!<)(?<!<=)>(?!=)|(?<!>)<(?!=)"
|
298
|
+
invalid_matches = re.findall(invalid_pattern, string)
|
299
|
+
|
300
|
+
if len(invalid_matches) > 0:
|
301
|
+
raise EquationToMatrixError(
|
302
|
+
f"{invalid_matches[0]} is an invalid comparison operator. "
|
303
|
+
f"Valid comparison operators are: {list(_COMPARISON_OPERATORS)}"
|
304
|
+
)
|
305
|
+
|
306
|
+
# '==' needs to be before '='
|
307
|
+
operators = sorted(_OPERATORS, reverse=True)
|
308
|
+
pattern = "((?:" + "|".join(["\\" + e for e in operators]) + "))"
|
309
|
+
res = [x.strip() for x in re.split(pattern, string)]
|
310
|
+
res = [x for x in res if x != ""]
|
311
|
+
return res
|
312
|
+
|
313
|
+
|
214
314
|
def _string_to_equation(
|
215
315
|
groups: np.ndarray,
|
216
316
|
string: str,
|
217
317
|
sum_to_one: bool,
|
218
|
-
) -> tuple[np.ndarray, float]:
|
318
|
+
) -> tuple[np.ndarray, float, bool]:
|
219
319
|
"""Convert a string to a left 1D-array and right float of the form:
|
220
|
-
`groups @ left <= right
|
320
|
+
`groups @ left <= right` or `groups @ left == right` and return whether it's an
|
321
|
+
equality or inequality.
|
221
322
|
|
222
323
|
Parameters
|
223
324
|
----------
|
@@ -232,20 +333,14 @@ def _string_to_equation(
|
|
232
333
|
|
233
334
|
Returns
|
234
335
|
-------
|
235
|
-
left: 1D-array of shape (n_assets,)
|
236
|
-
right: float
|
336
|
+
left : 1D-array of shape (n_assets,)
|
337
|
+
right : float
|
338
|
+
is_inequality : bool
|
237
339
|
"""
|
238
340
|
n = groups.shape[1]
|
239
|
-
operators = ["-", "+", "*", ">=", "<=", "==", "="]
|
240
|
-
invalid_operators = [">", "<"]
|
241
|
-
pattern = re.compile(r"((?:" + "|\\".join(operators) + r"))")
|
242
|
-
invalid_pattern = re.compile(r"((?:" + "|\\".join(invalid_operators) + r"))")
|
243
341
|
err_msg = f"Wrong pattern encountered while converting the string '{string}'"
|
244
342
|
|
245
|
-
|
246
|
-
res = [x.strip() for x in res]
|
247
|
-
res = [x for x in res if x != ""]
|
248
|
-
iterator = iter(res)
|
343
|
+
iterator = iter(_split_equation_string(string))
|
249
344
|
group_names = set(groups.flatten())
|
250
345
|
|
251
346
|
def is_group(name: str) -> bool:
|
@@ -254,7 +349,8 @@ def _string_to_equation(
|
|
254
349
|
left = np.zeros(n)
|
255
350
|
right = 0
|
256
351
|
main_sign = 1
|
257
|
-
|
352
|
+
comparison_sign = None
|
353
|
+
is_inequality = None
|
258
354
|
e = next(iterator, None)
|
259
355
|
i = 0
|
260
356
|
while True:
|
@@ -264,23 +360,27 @@ def _string_to_equation(
|
|
264
360
|
if e is None:
|
265
361
|
break
|
266
362
|
sign = 1
|
267
|
-
if e in
|
363
|
+
if e in _COMPARISON_OPERATORS:
|
364
|
+
if e in _INEQUALITY_OPERATORS:
|
365
|
+
is_inequality = True
|
366
|
+
else:
|
367
|
+
is_inequality = False
|
268
368
|
main_sign = -1
|
269
|
-
|
369
|
+
comparison_sign = _comparison_operator_sign(e)
|
270
370
|
e = next(iterator, None)
|
271
|
-
if e in
|
272
|
-
sign *=
|
371
|
+
if e in _SUB_ADD_OPERATORS:
|
372
|
+
sign *= _sub_add_operator_sign(e)
|
273
373
|
e = next(iterator, None)
|
274
|
-
elif e in
|
275
|
-
sign *=
|
374
|
+
elif e in _SUB_ADD_OPERATORS:
|
375
|
+
sign *= _sub_add_operator_sign(e)
|
276
376
|
e = next(iterator, None)
|
277
|
-
elif e
|
377
|
+
elif e in _MUL_OPERATORS:
|
278
378
|
raise EquationToMatrixError(
|
279
379
|
f"{err_msg}: the character '{e}' is wrongly positioned"
|
280
380
|
)
|
281
381
|
sign *= main_sign
|
282
382
|
# next can only be a number or a group
|
283
|
-
if e is None or e in
|
383
|
+
if e is None or e in _OPERATORS:
|
284
384
|
raise EquationToMatrixError(
|
285
385
|
f"{err_msg}: the character '{e}' is wrongly positioned"
|
286
386
|
)
|
@@ -288,20 +388,14 @@ def _string_to_equation(
|
|
288
388
|
arr = _matching_array(values=groups, key=e, sum_to_one=sum_to_one)
|
289
389
|
# next can only be a '*' or an ['-', '+', '>=', '<=', '==', '='] or None
|
290
390
|
e = next(iterator, None)
|
291
|
-
if e is None or e in
|
391
|
+
if e is None or e in _NON_MUL_OPERATORS:
|
292
392
|
left += sign * arr
|
293
|
-
elif e
|
393
|
+
elif e in _MUL_OPERATORS:
|
294
394
|
# next can only a number
|
295
395
|
e = next(iterator, None)
|
296
396
|
try:
|
297
397
|
number = float(e)
|
298
398
|
except ValueError:
|
299
|
-
invalid_ops = invalid_pattern.findall(e)
|
300
|
-
if len(invalid_ops) > 0:
|
301
|
-
raise EquationToMatrixError(
|
302
|
-
f"{invalid_ops[0]} is an invalid operator. Valid operators"
|
303
|
-
f" are: {operators}"
|
304
|
-
) from None
|
305
399
|
raise GroupNotFoundError(
|
306
400
|
f"{err_msg}: the group '{e}' is missing from the groups"
|
307
401
|
f" {groups}"
|
@@ -317,18 +411,12 @@ def _string_to_equation(
|
|
317
411
|
try:
|
318
412
|
number = float(e)
|
319
413
|
except ValueError:
|
320
|
-
invalid_ops = invalid_pattern.findall(e)
|
321
|
-
if len(invalid_ops) > 0:
|
322
|
-
raise EquationToMatrixError(
|
323
|
-
f"{invalid_ops[0]} is an invalid operator. Valid operators are:"
|
324
|
-
f" {operators}"
|
325
|
-
) from None
|
326
414
|
raise GroupNotFoundError(
|
327
415
|
f"{err_msg}: the group '{e}' is missing from the groups {groups}"
|
328
416
|
) from None
|
329
417
|
# next can only be a '*' or an operator or None
|
330
418
|
e = next(iterator, None)
|
331
|
-
if e
|
419
|
+
if e in _MUL_OPERATORS:
|
332
420
|
# next can only a group
|
333
421
|
e = next(iterator, None)
|
334
422
|
if not is_group(e):
|
@@ -338,14 +426,14 @@ def _string_to_equation(
|
|
338
426
|
arr = _matching_array(values=groups, key=e, sum_to_one=sum_to_one)
|
339
427
|
left += number * sign * arr
|
340
428
|
e = next(iterator, None)
|
341
|
-
elif e is None or e in
|
429
|
+
elif e is None or e in _NON_MUL_OPERATORS:
|
342
430
|
right += number * sign
|
343
431
|
else:
|
344
432
|
raise EquationToMatrixError(
|
345
433
|
f"{err_msg}: the character '{e}' is wrongly positioned"
|
346
434
|
)
|
347
435
|
|
348
|
-
left *=
|
349
|
-
right *= -
|
436
|
+
left *= comparison_sign
|
437
|
+
right *= -comparison_sign
|
350
438
|
|
351
|
-
return left, right
|
439
|
+
return left, right, is_inequality
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: skfolio
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.2
|
4
4
|
Summary: Portfolio optimization built on top of scikit-learn
|
5
5
|
Author-email: Hugo Delatte <delatte.hugo@gmail.com>
|
6
6
|
Maintainer-email: Hugo Delatte <delatte.hugo@gmail.com>
|
@@ -56,7 +56,7 @@ Classifier: Topic :: Software Development
|
|
56
56
|
Requires-Python: >=3.10
|
57
57
|
Description-Content-Type: text/x-rst
|
58
58
|
License-File: LICENSE
|
59
|
-
Requires-Dist: numpy
|
59
|
+
Requires-Dist: numpy >=1.23.4
|
60
60
|
Requires-Dist: scipy >=1.8.0
|
61
61
|
Requires-Dist: pandas >=1.4.1
|
62
62
|
Requires-Dist: cvxpy >=1.4.1
|
@@ -1,5 +1,5 @@
|
|
1
1
|
skfolio/__init__.py,sha256=5pn5LpTz6v2j2sxGkY97cVRrSPsN3Yav9b6Uw08boEI,618
|
2
|
-
skfolio/exceptions.py,sha256
|
2
|
+
skfolio/exceptions.py,sha256=3LCxKlxgEaIMPQPCHjo1UiL7rlJnD15dNRMyBeYyKcc,784
|
3
3
|
skfolio/typing.py,sha256=yEZiCZ6UIyfYUqtfj9Kf2KA9mrjUbmxyzpH9uqVboJs,1378
|
4
4
|
skfolio/cluster/__init__.py,sha256=4g-PFB_ld9BhiQ1ZPvvAorpFbRwd_p_DkeRlulDv2Hk,251
|
5
5
|
skfolio/cluster/_hierarchical.py,sha256=16INBe5HB7ALODO3RNI8ZjOYALtMZa3U_7EP1aEIxp8,12819
|
@@ -18,7 +18,7 @@ skfolio/measures/_measures.py,sha256=Z7XHSyM9xfecDgOqm-lJQJhvZxasF018-oFS4QjC4g0
|
|
18
18
|
skfolio/metrics/__init__.py,sha256=MomHJ5_bgjq4qUwGS2bfhNmG_ld0oQ4wK6y0Yy_Eonc,75
|
19
19
|
skfolio/metrics/_scorer.py,sha256=h1VuZk-zzn4rIChHl9FvM7RxqVT3b-jR1CEB-cr9F2s,4306
|
20
20
|
skfolio/model_selection/__init__.py,sha256=8j9Z5tpbgBScjFbn8ZsCm_6rZO7RkPQ1QIF8BqYMVA8,507
|
21
|
-
skfolio/model_selection/_combinatorial.py,sha256=
|
21
|
+
skfolio/model_selection/_combinatorial.py,sha256=uf5DzklgyLhfMKm0kWHXl2QLlUOAoiaxNb7cafrHVIg,19062
|
22
22
|
skfolio/model_selection/_validation.py,sha256=3eFYzPejjDZljc33vRehDuBQTEKCkrj-mZihMVuGA4s,10034
|
23
23
|
skfolio/model_selection/_walk_forward.py,sha256=T57HhdFGjG31mAufujHQuRK1uKfAdkiBx9eucQZ-WG0,15043
|
24
24
|
skfolio/moments/__init__.py,sha256=zwxaRO4TLoPj8qrcYSofNyd3tYhbLLcZWQaErzfDdNg,794
|
@@ -49,18 +49,18 @@ skfolio/optimization/cluster/hierarchical/_base.py,sha256=ioOBsHA-kRFV_Bvl0-PcqL
|
|
49
49
|
skfolio/optimization/cluster/hierarchical/_herc.py,sha256=gFmliW8YJZbbIjHwZ5IqTmTBIt9voLUGCZKdy8RoTvw,17956
|
50
50
|
skfolio/optimization/cluster/hierarchical/_hrp.py,sha256=nB3W5Zm1TaKTLyRMqN6irAbXD-y-bL2b78d7VFYASa8,16511
|
51
51
|
skfolio/optimization/convex/__init__.py,sha256=F6BPFikTo0B-7JCKazqLGEwM3RkgTNbFm5GAGkaq9Uo,570
|
52
|
-
skfolio/optimization/convex/_base.py,sha256
|
53
|
-
skfolio/optimization/convex/_distributionally_robust.py,sha256=
|
54
|
-
skfolio/optimization/convex/_maximum_diversification.py,sha256=
|
55
|
-
skfolio/optimization/convex/_mean_risk.py,sha256=
|
56
|
-
skfolio/optimization/convex/_risk_budgeting.py,sha256=
|
52
|
+
skfolio/optimization/convex/_base.py,sha256=2at6Ll4qHkN_1wvYjl-yXWTbiRJj8fhNS-bfAT88YSw,76055
|
53
|
+
skfolio/optimization/convex/_distributionally_robust.py,sha256=tw_UNSDfAXP02khE10hpmcdlz3DQXQD7ttDqFDSHV1E,17811
|
54
|
+
skfolio/optimization/convex/_maximum_diversification.py,sha256=IVKVbK7bh4KPkhpNWLLerl-qx9Qcmf2cIIRotP8r8nI,19500
|
55
|
+
skfolio/optimization/convex/_mean_risk.py,sha256=H4Ik6vvIETdAZnNCA4Jhk_OTirHJg26KQZ5iLsXgaHo,44176
|
56
|
+
skfolio/optimization/convex/_risk_budgeting.py,sha256=ntPK57Ws-_U4QAiZjXFvKUYUELv9EBoJIWqofxx-0rY,23779
|
57
57
|
skfolio/optimization/ensemble/__init__.py,sha256=8TXxcxH2_gG3C1xtgQj9OHHr0Le8lhdejtlURL6T3ZY,158
|
58
58
|
skfolio/optimization/ensemble/_base.py,sha256=GaNDQu6ivosYuwMrb-b0PhToCsNrmhSYyXkxeM8W4rU,3399
|
59
59
|
skfolio/optimization/ensemble/_stacking.py,sha256=ZoICUnc_MwoXDQAR2kewCg-KIezSOIUdDV1fuf7vMyA,14168
|
60
60
|
skfolio/optimization/naive/__init__.py,sha256=Dkr55R48urC-jfYN007NTbei16N91Na_EDYLVqzhGgQ,147
|
61
61
|
skfolio/optimization/naive/_naive.py,sha256=AhEyYKEUAm-Fjn4p8SHwhp7yE9iF0tRyDZIjKYV4EeU,6390
|
62
62
|
skfolio/population/__init__.py,sha256=rsPPMUv95aTK7vmpPeQwF8NzFuBwk6RDo5g4HNaPzNM,80
|
63
|
-
skfolio/population/_population.py,sha256=
|
63
|
+
skfolio/population/_population.py,sha256=WYT6yTVmarzMH3nj1-rQCvD-X2nH6q9bo928-lenUXs,30426
|
64
64
|
skfolio/portfolio/__init__.py,sha256=YYtcAPmA2zeCxFGTXegg2FXcA7py6CxOX7IMTdYuXl0,586
|
65
65
|
skfolio/portfolio/_base.py,sha256=EFLsvHoxZmDvGPOKePr6hQGXU7y7TWsALvzYP9qt0fQ,39588
|
66
66
|
skfolio/portfolio/_multi_period_portfolio.py,sha256=K2JfEwlPD9iGO58lOdk7WUbWuXZDWw2prPT5T7pOdto,24387
|
@@ -71,7 +71,7 @@ skfolio/preprocessing/__init__.py,sha256=15A1bzfPsbfxxXgGP1gstf4R0E_347Wn18z5W5j
|
|
71
71
|
skfolio/preprocessing/_returns.py,sha256=oo1Mm-UCHwq4ECjfmsRxWzzK1EPsuv-EEtnimvv_nXo,4345
|
72
72
|
skfolio/prior/__init__.py,sha256=jql8NTiWlykPKJUXTOPdqm531mP8Pul1QAR6hXTXA6c,446
|
73
73
|
skfolio/prior/_base.py,sha256=u9GLCKJl-Txiem5rIO-qkH3VIyem3taD6T9kMzsYPRY,1941
|
74
|
-
skfolio/prior/_black_litterman.py,sha256=
|
74
|
+
skfolio/prior/_black_litterman.py,sha256=W3HbpvkViEiD7AOgpdVmNYTlWKSGDgo9Y3BfSrbMIQ4,10347
|
75
75
|
skfolio/prior/_empirical.py,sha256=K3htSj_MGX6wNL-XxkTqFxz8WeqNzek6X4YYwKUmMC4,7207
|
76
76
|
skfolio/prior/_factor_model.py,sha256=xMWyOaJNrCM6NyDQK_-G4wCfREaThI4QvhxxGhsodII,11311
|
77
77
|
skfolio/uncertainty_set/__init__.py,sha256=LlMHtYv9G9fgtM7m4sCSToS9et57Pm2Q2gGchTVrj6c,617
|
@@ -80,12 +80,12 @@ skfolio/uncertainty_set/_bootstrap.py,sha256=BRD8LhGKULkqqCBjLqU1EtCAMBkLJKEXJyg
|
|
80
80
|
skfolio/uncertainty_set/_empirical.py,sha256=ACqMVTBKibJm6E3IP4TOi3MYsxKMhiEoix5D_fp9X-w,9364
|
81
81
|
skfolio/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
82
82
|
skfolio/utils/bootstrap.py,sha256=3zY2kO_GQURKEcQMCasJOSByde9Mt2IAi3KJH0_a4mk,3550
|
83
|
-
skfolio/utils/equations.py,sha256=
|
83
|
+
skfolio/utils/equations.py,sha256=MQ1w3VSM2n_j9bTIKAQA716aWKYyUqtw5yM2bU-9t-M,13745
|
84
84
|
skfolio/utils/sorting.py,sha256=lSjMvH2L-sSj-06B3MlwBrH1rtjCeGEe4hG894W7TE0,3504
|
85
85
|
skfolio/utils/stats.py,sha256=wuOmSt5panMMTw_pFYizLbmrclsE_4PHQfamkzJ5J2s,13937
|
86
86
|
skfolio/utils/tools.py,sha256=4KrmBR9jOLiI6j0hb27gsPC--OHXo4Sp1xl-6i-k9Tg,20925
|
87
|
-
skfolio-0.4.
|
88
|
-
skfolio-0.4.
|
89
|
-
skfolio-0.4.
|
90
|
-
skfolio-0.4.
|
91
|
-
skfolio-0.4.
|
87
|
+
skfolio-0.4.2.dist-info/LICENSE,sha256=F6Gi-ZJX5BlVzYK8R9NcvAkAsKa7KO29xB1OScbrH6Q,1526
|
88
|
+
skfolio-0.4.2.dist-info/METADATA,sha256=g_b6XH3HiwSCz8DawgN2HWHKQIZQMBdhVhbY_TOERUo,19610
|
89
|
+
skfolio-0.4.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
90
|
+
skfolio-0.4.2.dist-info/top_level.txt,sha256=NXEaoS9Ms7t32gxkb867nV0OKlU0KmssL7IJBVo0fJs,8
|
91
|
+
skfolio-0.4.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|