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 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 <= ref2"
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 >= 0.7" --> the sum of all US weights must be greater than 70%
306
- * "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
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
- a, b = equations_to_matrix(
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 np.any(a != 0):
741
+ if len(a_eq) != 0:
742
742
  constraints.append(
743
- a @ w * self._scale_constraints
744
- - b * factor * self._scale_constraints
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 <= ref2"
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 >= 0.7" --> the sum of all US weights must be greater than 70%
141
- * "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
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 <= ref2"
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 >= 0.7" --> the sum of all US weights must be greater than 70%
217
- * "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
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 <= ref2"
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 >= 0.7" --> the sum of all US weights must be greater than 70%
350
- * "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
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
- Linear constraints.
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 <= ref2"
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 >= 0.7" --> the sum of all US weights must be greater than 70%
233
- * "Equity <= 3 * Bond" --> the sum of all Equity weights must be less or equal to 3 times the sum of all Bond weights.
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(
@@ -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 EquationToMatrixError, GroupNotFoundError
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 >= number * group_2"
61
+ * "group_1 == number * group_2"
48
62
  * "group_1 <= number"
49
- * "group_1 >= number"
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 <= 1",
61
- "3*SPX + 5*SX5E <= 2*TLT + 3",
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
- left: ndarray of shape (n_equations, n_assets)
80
- right: ndarray of shape (n_equations,)
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
- If none of the group inside the equations are part of the groups, `None` is
83
- returned.
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 = np.asarray(groups)
86
- equations = np.asarray(equations)
87
- if groups.ndim != 2:
88
- raise ValueError(
89
- f"`{names[0]}` must be a 2D array, got {groups.ndim}D array instead."
90
- )
91
- if equations.ndim != 1:
92
- raise ValueError(
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
- n_equations = len(equations)
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
- a[i] = left
108
- b[i] = right
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 a, b
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
- _operator_mapping = {">=": -1, "<=": 1, "==": 1, "=": 1}
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 _operator_mapping[operator]
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 _operator_sign(operator: str) -> int:
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 _operator_signs[operator]
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
- res = re.split(pattern, string)
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
- inequality_sign = None
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
- inequality_sign = _inequality_operator_sign(e)
369
+ comparison_sign = _comparison_operator_sign(e)
270
370
  e = next(iterator, None)
271
- if e in ["-", "+"]:
272
- sign *= _operator_sign(e)
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 *= _operator_sign(e)
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 operators:
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 *= inequality_sign
349
- right *= -inequality_sign
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.0
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 <2.0.0,>=1.23.4
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=-XniKql9QHgfitMgHsE9UXWVPdjWpNGO2dVk2SsdPWE,662
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=cS_W_4uA2aUC0eRbqbRnfezRRxUpm2_HGs1S5ea0q6E,19045
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=-lUwlV5wv4BayzOhB1g4smaJTm4VQ1ZTiaI0JAr_LWo,75806
53
- skfolio/optimization/convex/_distributionally_robust.py,sha256=INm3kyuKSwdSbgVlqJuKM4P1KQ0ImQulqoO4gfh-a4Q,17823
54
- skfolio/optimization/convex/_maximum_diversification.py,sha256=xucjuxiJf46vNwSZ7ICWmoscDvLn3Ts0xngszXfFXC0,19512
55
- skfolio/optimization/convex/_mean_risk.py,sha256=IBtZovEh0FBYBL5gh0KMsBmyOsSbKTAgMK0y3Yem9p4,44188
56
- skfolio/optimization/convex/_risk_budgeting.py,sha256=vOPxluh7yEEX1P4wLD6S2eT6HLyBWLeFmUqAaK_SLSU,23790
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=9NKnz_rQYLnauP1Me6tnDwD7lq3MeGnSyCq-sb0fTV0,30424
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=7ikhjV_pc61J_0zyDiW63MNFHIEdMP3rJhmsSq5mM_k,10220
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=w0HsYjA7cS0mHYsI9MpixHLkof3HN26nc14ZfqFrHlE,11047
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.0.dist-info/LICENSE,sha256=F6Gi-ZJX5BlVzYK8R9NcvAkAsKa7KO29xB1OScbrH6Q,1526
88
- skfolio-0.4.0.dist-info/METADATA,sha256=Vh_PqWOdbuuaxurc6k3BL0dZeG7BpKLlis___v8YMv4,19617
89
- skfolio-0.4.0.dist-info/WHEEL,sha256=5Mi1sN9lKoFv_gxcPtisEVrJZihrm_beibeg5R6xb4I,91
90
- skfolio-0.4.0.dist-info/top_level.txt,sha256=NXEaoS9Ms7t32gxkb867nV0OKlU0KmssL7IJBVo0fJs,8
91
- skfolio-0.4.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.0.0)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5