skfolio 0.5.2__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. skfolio/__init__.py +5 -5
  2. skfolio/cluster/__init__.py +1 -1
  3. skfolio/cluster/_hierarchical.py +1 -1
  4. skfolio/datasets/__init__.py +2 -2
  5. skfolio/distance/__init__.py +3 -3
  6. skfolio/distance/_distance.py +7 -6
  7. skfolio/exceptions.py +2 -2
  8. skfolio/measures/__init__.py +23 -23
  9. skfolio/model_selection/__init__.py +2 -2
  10. skfolio/moments/__init__.py +11 -11
  11. skfolio/moments/covariance/__init__.py +6 -6
  12. skfolio/moments/covariance/_denoise_covariance.py +2 -1
  13. skfolio/moments/covariance/_detone_covariance.py +2 -1
  14. skfolio/moments/covariance/_empirical_covariance.py +2 -1
  15. skfolio/moments/covariance/_ew_covariance.py +2 -1
  16. skfolio/moments/covariance/_gerber_covariance.py +2 -1
  17. skfolio/moments/covariance/_implied_covariance.py +1 -1
  18. skfolio/moments/expected_returns/__init__.py +2 -2
  19. skfolio/moments/expected_returns/_empirical_mu.py +2 -1
  20. skfolio/moments/expected_returns/_equilibrium_mu.py +2 -1
  21. skfolio/moments/expected_returns/_ew_mu.py +2 -1
  22. skfolio/moments/expected_returns/_shrunk_mu.py +2 -1
  23. skfolio/optimization/__init__.py +10 -10
  24. skfolio/optimization/cluster/__init__.py +1 -1
  25. skfolio/optimization/cluster/_nco.py +3 -2
  26. skfolio/optimization/cluster/hierarchical/__init__.py +1 -1
  27. skfolio/optimization/cluster/hierarchical/_herc.py +2 -1
  28. skfolio/optimization/cluster/hierarchical/_hrp.py +2 -1
  29. skfolio/optimization/convex/__init__.py +3 -3
  30. skfolio/optimization/convex/_base.py +344 -31
  31. skfolio/optimization/convex/_distributionally_robust.py +4 -1
  32. skfolio/optimization/convex/_maximum_diversification.py +4 -2
  33. skfolio/optimization/convex/_mean_risk.py +125 -17
  34. skfolio/optimization/convex/_risk_budgeting.py +3 -1
  35. skfolio/optimization/ensemble/_stacking.py +2 -2
  36. skfolio/optimization/naive/__init__.py +1 -1
  37. skfolio/optimization/naive/_naive.py +3 -2
  38. skfolio/portfolio/__init__.py +1 -1
  39. skfolio/portfolio/_base.py +1 -0
  40. skfolio/portfolio/_portfolio.py +1 -0
  41. skfolio/pre_selection/__init__.py +1 -1
  42. skfolio/pre_selection/_drop_correlated.py +1 -1
  43. skfolio/pre_selection/_select_complete.py +6 -4
  44. skfolio/pre_selection/_select_k_extremes.py +1 -1
  45. skfolio/pre_selection/_select_non_dominated.py +1 -1
  46. skfolio/pre_selection/_select_non_expiring.py +6 -4
  47. skfolio/prior/__init__.py +3 -3
  48. skfolio/prior/_black_litterman.py +2 -1
  49. skfolio/prior/_empirical.py +2 -1
  50. skfolio/prior/_factor_model.py +2 -1
  51. skfolio/typing.py +6 -6
  52. skfolio/uncertainty_set/__init__.py +5 -5
  53. skfolio/uncertainty_set/_base.py +3 -2
  54. skfolio/utils/equations.py +58 -1
  55. skfolio/utils/stats.py +8 -8
  56. skfolio/utils/tools.py +10 -10
  57. {skfolio-0.5.2.dist-info → skfolio-0.7.0.dist-info}/METADATA +32 -29
  58. skfolio-0.7.0.dist-info/RECORD +95 -0
  59. {skfolio-0.5.2.dist-info → skfolio-0.7.0.dist-info}/WHEEL +1 -1
  60. skfolio-0.5.2.dist-info/RECORD +0 -95
  61. {skfolio-0.5.2.dist-info → skfolio-0.7.0.dist-info}/LICENSE +0 -0
  62. {skfolio-0.5.2.dist-info → skfolio-0.7.0.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ import numpy.typing as npt
18
18
  import scipy as sc
19
19
  import scipy.sparse.linalg as scl
20
20
  import sklearn.utils.metadata_routing as skm
21
+ from cvxpy.reductions.solvers.defines import MI_SOLVERS
21
22
 
22
23
  import skfolio.typing as skt
23
24
  from skfolio.measures import RiskMeasure, owa_gmd_weights
@@ -28,7 +29,7 @@ from skfolio.uncertainty_set import (
28
29
  BaseMuUncertaintySet,
29
30
  UncertaintySet,
30
31
  )
31
- from skfolio.utils.equations import equations_to_matrix
32
+ from skfolio.utils.equations import equations_to_matrix, group_cardinalities_to_matrix
32
33
  from skfolio.utils.tools import AutoEnum, cache_method, input_to_array
33
34
 
34
35
  INSTALLED_SOLVERS = cp.installed_solvers()
@@ -169,6 +170,36 @@ class ConvexOptimization(BaseOptimization, ABC):
169
170
  weights.
170
171
  The default (`None`) means no maximum long position.
171
172
 
173
+ cardinality : int, optional
174
+ Specifies the cardinality constraint to limit the number of invested assets
175
+ (non-zero weights). This feature requires a mixed-integer solver. For an
176
+ open-source option, we recommend using SCIP by setting `solver="SCIP"`.
177
+ To install it, use: `pip install cvxpy[SCIP]`. For commercial solvers,
178
+ supported options include MOSEK, GUROBI, or CPLEX.
179
+
180
+ group_cardinalities : dict[str, int], optional
181
+ A dictionary specifying cardinality constraints for specific groups of assets.
182
+ The keys represent group names (strings), and the values specify the maximum
183
+ number of assets allowed in each group. You must provide the groups using the
184
+ `groups` parameter. This requires a mixed-integer solver (see `cardinality`
185
+ for more details).
186
+
187
+ threshold_long : float | dict[str, float] | array-like of shape (n_assets, ), optional
188
+ Specifies the minimum weight threshold for assets in the portfolio to be
189
+ considered as a long position. Assets with weights below this threshold
190
+ will not be included as part of the portfolio's long positions. This
191
+ constraint can help eliminate insignificant allocations.
192
+ This requires a mixed-integer solver (see `cardinality` for more details).
193
+ It follows the same format as `min_weights` and `max_weights`.
194
+
195
+ threshold_short : float | dict[str, float] | array-like of shape (n_assets, ), optional
196
+ Specifies the maximum weight threshold for assets in the portfolio to be
197
+ considered as a short position. Assets with weights above this threshold
198
+ will not be included as part of the portfolio's short positions. This
199
+ constraint can help control the magnitude of short positions.
200
+ This requires a mixed-integer solver (see `cardinality` for more details).
201
+ It follows the same format as `min_weights` and `max_weights`.
202
+
172
203
  transaction_costs : float | dict[str, float] | array-like of shape (n_assets, ), default=0.0
173
204
  Transaction costs of the assets. It is used to add linear transaction costs to
174
205
  the optimization problem:
@@ -382,7 +413,7 @@ class ConvexOptimization(BaseOptimization, ABC):
382
413
  The default (`None`) is use `{"tol_gap_abs": 1e-9, "tol_gap_rel": 1e-9}`
383
414
  for the solver "CLARABEL" and the CVXPY default otherwise.
384
415
  For more details about solver arguments, check the CVXPY documentation:
385
- https://www.cvxpy.org/tutorial/advanced/index.html#setting-solver-options
416
+ https://www.cvxpy.org/tutorial/solvers
386
417
 
387
418
  scale_objective : float, optional
388
419
  Scale each objective element by this value.
@@ -453,6 +484,10 @@ class ConvexOptimization(BaseOptimization, ABC):
453
484
  max_budget: float | None = None,
454
485
  max_short: float | None = None,
455
486
  max_long: float | None = None,
487
+ cardinality: int | None = None,
488
+ group_cardinalities: dict[str, int] | None = None,
489
+ threshold_long: skt.MultiInput | None = None,
490
+ threshold_short: skt.MultiInput | None = None,
456
491
  transaction_costs: skt.MultiInput = 0.0,
457
492
  management_fees: skt.MultiInput = 0.0,
458
493
  previous_weights: skt.MultiInput | None = None,
@@ -502,6 +537,10 @@ class ConvexOptimization(BaseOptimization, ABC):
502
537
  self.max_budget = max_budget
503
538
  self.max_short = max_short
504
539
  self.max_long = max_long
540
+ self.cardinality = cardinality
541
+ self.group_cardinalities = group_cardinalities
542
+ self.threshold_long = threshold_long
543
+ self.threshold_short = threshold_short
505
544
  self.min_acceptable_return = min_acceptable_return
506
545
  self.transaction_costs = transaction_costs
507
546
  self.management_fees = management_fees
@@ -648,32 +687,92 @@ class ConvexOptimization(BaseOptimization, ABC):
648
687
  """
649
688
  constraints = []
650
689
 
651
- if self.min_weights is not None:
690
+ # Clean and convert to array
691
+ min_weights = self.min_weights
692
+ max_weights = self.max_weights
693
+ threshold_long = self.threshold_long
694
+ threshold_short = self.threshold_short
695
+ groups = self.groups
696
+
697
+ if min_weights is not None:
652
698
  min_weights = self._clean_input(
653
- self.min_weights,
699
+ min_weights,
654
700
  n_assets=n_assets,
655
701
  fill_value=0,
656
702
  name="min_weights",
657
703
  )
658
704
 
705
+ if max_weights is not None:
706
+ max_weights = self._clean_input(
707
+ max_weights,
708
+ n_assets=n_assets,
709
+ fill_value=1,
710
+ name="max_weights",
711
+ )
712
+
713
+ if threshold_long is not None:
714
+ threshold_long = self._clean_input(
715
+ threshold_long,
716
+ n_assets=n_assets,
717
+ fill_value=0,
718
+ name="threshold_long",
719
+ )
720
+ if np.all(threshold_long == 0):
721
+ threshold_long = None
722
+
723
+ if threshold_short is not None:
724
+ threshold_short = self._clean_input(
725
+ threshold_short,
726
+ n_assets=n_assets,
727
+ fill_value=0,
728
+ name="threshold_short",
729
+ )
730
+ if np.all(threshold_short == 0):
731
+ threshold_short = None
732
+
733
+ if groups is not None:
734
+ groups = input_to_array(
735
+ items=groups,
736
+ n_assets=n_assets,
737
+ fill_value="",
738
+ dim=2,
739
+ assets_names=(
740
+ self.feature_names_in_
741
+ if hasattr(self, "feature_names_in_")
742
+ else None
743
+ ),
744
+ name="groups",
745
+ )
746
+
747
+ is_mip = (
748
+ (self.cardinality is not None and self.cardinality < n_assets)
749
+ or (self.group_cardinalities is not None)
750
+ or self.threshold_long is not None
751
+ or self.threshold_short is not None
752
+ )
753
+
754
+ if is_mip and self.solver not in MI_SOLVERS:
755
+ raise ValueError(
756
+ "You are using constraints that require a mixed-integer solver and "
757
+ f"{self.solver} doesn't support MIP problems. For an open-source "
758
+ "option, we recommend using SCIP by setting `solver='SCIP'`. "
759
+ "To install it, use: `pip install cvxpy[SCIP]`. For commercial "
760
+ "solvers, supported options include MOSEK, GUROBI, or CPLEX."
761
+ )
762
+
763
+ # Constraints
764
+ if min_weights is not None:
659
765
  if not allow_negative_weights and np.any(min_weights < 0):
660
766
  raise ValueError(
661
767
  f"{self.__class__.__name__} must have non negative `min_weights` "
662
768
  f"constraint otherwise the problem becomes non-convex."
663
769
  )
664
-
665
770
  constraints.append(
666
771
  w * self._scale_constraints
667
772
  >= min_weights * factor * self._scale_constraints
668
773
  )
669
774
 
670
- if self.max_weights is not None:
671
- max_weights = self._clean_input(
672
- self.max_weights,
673
- n_assets=n_assets,
674
- fill_value=1,
675
- name="max_weights",
676
- )
775
+ if max_weights is not None:
677
776
  constraints.append(
678
777
  w * self._scale_constraints
679
778
  <= max_weights * factor * self._scale_constraints
@@ -723,27 +822,80 @@ class ConvexOptimization(BaseOptimization, ABC):
723
822
  == float(self.budget) * factor * self._scale_constraints
724
823
  )
725
824
 
825
+ if is_mip:
826
+ is_short = np.any(min_weights < 0)
827
+
828
+ if max_weights is None or min_weights is None:
829
+ raise ValueError(
830
+ "'max_weights' and 'min_weights' must be provided with cardinality "
831
+ "constraint"
832
+ )
833
+ if np.all(min_weights > 0):
834
+ raise ValueError(
835
+ "Cardinality and Threshold constraint can only be applied "
836
+ "if 'min_weights' are not all strictly positive (you allow some "
837
+ "weights to be 0)"
838
+ )
839
+
840
+ if self.group_cardinalities is not None and groups is None:
841
+ raise ValueError(
842
+ "When 'group_cardinalities' is provided, you must also "
843
+ "also provide 'groups'"
844
+ )
845
+
846
+ if (
847
+ self.threshold_long is not None
848
+ and self.threshold_short is None
849
+ and is_short
850
+ ):
851
+ raise ValueError(
852
+ "When 'threshold_long' is provided and 'min_weights' can be negative "
853
+ "(short position are allowed), then 'threshold_short' must also be "
854
+ "provided"
855
+ )
856
+
857
+ if threshold_short is not None and threshold_long is None:
858
+ raise ValueError(
859
+ "When 'threshold_short' is provided, 'threshold_long' must also be "
860
+ "provided"
861
+ )
862
+
863
+ if self.threshold_short is not None and is_short:
864
+ constraints += _mip_weight_constraints_threshold_short(
865
+ n_assets=n_assets,
866
+ w=w,
867
+ factor=factor,
868
+ scale_constraints=self._scale_constraints,
869
+ cardinality=self.cardinality,
870
+ group_cardinalities=self.group_cardinalities,
871
+ max_weights=max_weights,
872
+ groups=groups,
873
+ min_weights=min_weights,
874
+ threshold_long=threshold_long,
875
+ threshold_short=threshold_short,
876
+ )
877
+ else:
878
+ constraints += _mip_weight_constraints_no_short_threshold(
879
+ n_assets=n_assets,
880
+ w=w,
881
+ factor=factor,
882
+ scale_constraints=self._scale_constraints,
883
+ cardinality=self.cardinality,
884
+ group_cardinalities=self.group_cardinalities,
885
+ max_weights=max_weights,
886
+ groups=groups,
887
+ min_weights=min_weights,
888
+ threshold_long=threshold_long,
889
+ )
890
+
726
891
  if self.linear_constraints is not None:
727
- if self.groups is None:
892
+ if groups is None:
728
893
  if not hasattr(self, "feature_names_in_"):
729
894
  raise ValueError(
730
895
  "If `linear_constraints` is provided you must provide either"
731
896
  " `groups` or `X` as a DataFrame with asset names in columns"
732
897
  )
733
898
  groups = np.asarray([self.feature_names_in_])
734
- else:
735
- groups = input_to_array(
736
- items=self.groups,
737
- n_assets=n_assets,
738
- fill_value="",
739
- dim=2,
740
- assets_names=(
741
- self.feature_names_in_
742
- if hasattr(self, "feature_names_in_")
743
- else None
744
- ),
745
- name="groups",
746
- )
747
899
  a_eq, b_eq, a_ineq, b_ineq = equations_to_matrix(
748
900
  groups=groups,
749
901
  equations=self.linear_constraints,
@@ -975,6 +1127,8 @@ class ConvexOptimization(BaseOptimization, ABC):
975
1127
  weights = w.value / factor.value
976
1128
  problem_values = {
977
1129
  name: expression.value / factor.value
1130
+ if name != "factor"
1131
+ else expression.value
978
1132
  for name, expression in expressions.items()
979
1133
  }
980
1134
  problem_values["objective"] = (
@@ -1000,7 +1154,7 @@ class ConvexOptimization(BaseOptimization, ABC):
1000
1154
  if len(params_string) != 0:
1001
1155
  params_string = f" with parameters {params_string}"
1002
1156
  msg = (
1003
- f"Solver '{self.solver}' failed for {params_string}. Try another"
1157
+ f"Solver '{self.solver}' failed{params_string}. Try another"
1004
1158
  " solver, or solve with solver_params=dict(verbose=True) for more"
1005
1159
  " information"
1006
1160
  )
@@ -1525,13 +1679,14 @@ class ConvexOptimization(BaseOptimization, ABC):
1525
1679
  n_assets = prior_model.returns.shape[1]
1526
1680
  x = cp.Variable((n_assets, n_assets), symmetric=True)
1527
1681
  y = cp.Variable((n_assets, n_assets), symmetric=True)
1528
- w_reshaped = cp.reshape(w, (n_assets, 1))
1529
- factor_reshaped = cp.reshape(factor, (1, 1))
1682
+ w_reshaped = cp.reshape(w, (n_assets, 1), order="F")
1683
+ factor_reshaped = cp.reshape(factor, (1, 1), order="F")
1530
1684
  z1 = cp.vstack([x, w_reshaped.T])
1531
1685
  z2 = cp.vstack([w_reshaped, factor_reshaped])
1532
1686
 
1533
1687
  risk = covariance_uncertainty_set.k * cp.pnorm(
1534
- sc.linalg.sqrtm(covariance_uncertainty_set.sigma) @ (cp.vec(x) + cp.vec(y)),
1688
+ sc.linalg.sqrtm(covariance_uncertainty_set.sigma)
1689
+ @ (cp.vec(x, order="F") + cp.vec(y, order="F")),
1535
1690
  2,
1536
1691
  ) + cp.trace(prior_model.covariance @ (x + y))
1537
1692
  # semi-definite positive constraints
@@ -1972,7 +2127,7 @@ class ConvexOptimization(BaseOptimization, ABC):
1972
2127
  ptf_returns * self._scale_constraints
1973
2128
  - ptf_transaction_cost * self._scale_constraints
1974
2129
  - ptf_management_fee * self._scale_constraints
1975
- == cp.reshape(z, (observation_nb,)) * self._scale_constraints,
2130
+ == cp.reshape(z, (observation_nb,), order="F") * self._scale_constraints,
1976
2131
  z @ gmd_w.T <= ones @ x.T + y @ ones.T,
1977
2132
  ]
1978
2133
  return risk, constraints
@@ -1988,3 +2143,161 @@ class ConvexOptimization(BaseOptimization, ABC):
1988
2143
  @abstractmethod
1989
2144
  def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params):
1990
2145
  pass
2146
+
2147
+
2148
+ def _mip_weight_constraints_no_short_threshold(
2149
+ n_assets: int,
2150
+ w: cp.Variable,
2151
+ factor: skt.Factor,
2152
+ scale_constraints: cp.Constant,
2153
+ cardinality: int | None,
2154
+ group_cardinalities: dict[str, int] | None,
2155
+ max_weights: np.ndarray | None,
2156
+ groups: np.ndarray | None,
2157
+ min_weights: np.ndarray | None,
2158
+ threshold_long: np.ndarray | None,
2159
+ ) -> list[cp.Expression]:
2160
+ """
2161
+ Create a list of MIP constraints for cardinality and threshold conditions
2162
+ when no short threshold is present. This only requires the creation of a single
2163
+ boolean variable array.
2164
+ """
2165
+ constraints = []
2166
+
2167
+ is_short = np.any(min_weights < 0)
2168
+
2169
+ is_invested_bool = cp.Variable(n_assets, boolean=True)
2170
+
2171
+ if cardinality is not None and cardinality < n_assets:
2172
+ constraints.append(cp.sum(is_invested_bool) <= cardinality)
2173
+
2174
+ if group_cardinalities is not None:
2175
+ a_card, b_card = group_cardinalities_to_matrix(
2176
+ groups=groups,
2177
+ group_cardinalities=group_cardinalities,
2178
+ raise_if_group_missing=False,
2179
+ )
2180
+ constraints.append(a_card @ is_invested_bool - b_card <= 0)
2181
+
2182
+ if isinstance(factor, cp.Variable):
2183
+ is_invested_factor = cp.Variable(n_assets, nonneg=True)
2184
+ # We want (w <= cp.multiply(is_invested_short_bool, max_weights) * factor
2185
+ # but this is not DCP. So we introduce another variable and set
2186
+ # constraint to ensure its value is equal to is_invested_short_bool * factor
2187
+
2188
+ M = 1e3
2189
+ # Big M method to activate or deactivate constraints
2190
+ # In the ratio homogenization procedure, the factor has been calibrated
2191
+ # to be around 0.1-10. By using M=1e3, we ensure that M is large enough while
2192
+ # not too large for improved MIP convergence.
2193
+
2194
+ constraints += [
2195
+ is_invested_factor <= factor,
2196
+ is_invested_factor <= M * is_invested_bool,
2197
+ is_invested_factor >= factor - M * (1 - is_invested_bool),
2198
+ ]
2199
+ is_invested = is_invested_factor
2200
+ else:
2201
+ is_invested = is_invested_bool
2202
+
2203
+ if threshold_long is not None:
2204
+ constraints.append(
2205
+ w * scale_constraints
2206
+ >= cp.multiply(is_invested, threshold_long) * scale_constraints
2207
+ )
2208
+
2209
+ constraints.append(
2210
+ w * scale_constraints
2211
+ <= cp.multiply(is_invested, max_weights) * scale_constraints
2212
+ )
2213
+
2214
+ if is_short:
2215
+ constraints.append(
2216
+ w * scale_constraints
2217
+ >= cp.multiply(is_invested, min_weights) * scale_constraints
2218
+ )
2219
+
2220
+ return constraints
2221
+
2222
+
2223
+ def _mip_weight_constraints_threshold_short(
2224
+ n_assets: int,
2225
+ w: cp.Variable,
2226
+ factor: skt.Factor,
2227
+ scale_constraints: cp.Constant,
2228
+ max_weights: np.ndarray,
2229
+ min_weights: np.ndarray,
2230
+ threshold_long: np.ndarray,
2231
+ threshold_short: np.ndarray,
2232
+ cardinality: int | None,
2233
+ group_cardinalities: dict[str, int] | None,
2234
+ groups: np.ndarray | None,
2235
+ ) -> list[cp.Expression]:
2236
+ """
2237
+ Create a list of MIP constraints for cardinality and threshold constraints
2238
+ when a short threshold is allowed. This requires the creation of two boolean
2239
+ variable arrays, one for long positions and one for short positions.
2240
+ """
2241
+ constraints = []
2242
+
2243
+ is_invested_short_bool = cp.Variable(n_assets, boolean=True)
2244
+ is_invested_long_bool = cp.Variable(n_assets, boolean=True)
2245
+ is_invested_bool = is_invested_short_bool + is_invested_long_bool
2246
+
2247
+ if cardinality is not None and cardinality < n_assets:
2248
+ constraints.append(cp.sum(is_invested_bool) <= cardinality)
2249
+
2250
+ if group_cardinalities is not None:
2251
+ a_card, b_card = group_cardinalities_to_matrix(
2252
+ groups=groups,
2253
+ group_cardinalities=group_cardinalities,
2254
+ raise_if_group_missing=False,
2255
+ )
2256
+ constraints.append(a_card @ is_invested_bool - b_card <= 0)
2257
+
2258
+ M = 1e3
2259
+ # Big M method to activate or deactivate constraints
2260
+ # In the ratio homogenization procedure, the factor has been calibrated
2261
+ # to be around 0.1-10. By using M=1e3, we ensure that M is large enough while
2262
+ # not too large for improved MIP convergence.
2263
+
2264
+ if isinstance(factor, cp.Variable):
2265
+ is_invested_short_factor = cp.Variable(n_assets, nonneg=True)
2266
+ is_invested_long_factor = cp.Variable(n_assets, nonneg=True)
2267
+ # We want (w <= cp.multiply(is_invested_short_bool, max_weights) * factor
2268
+ # but this is not DCP. So we introduce another variable and set
2269
+ # constraint to ensure its value is equal to is_invested_short_bool * factor
2270
+
2271
+ constraints += [
2272
+ is_invested_short_factor <= factor,
2273
+ is_invested_long_factor <= factor,
2274
+ is_invested_short_factor <= M * is_invested_short_bool,
2275
+ is_invested_long_factor <= M * is_invested_long_bool,
2276
+ is_invested_short_factor >= factor - M * (1 - is_invested_short_bool),
2277
+ is_invested_long_factor >= factor - M * (1 - is_invested_long_bool),
2278
+ ]
2279
+ is_invested_short = is_invested_short_factor
2280
+ is_invested_long = is_invested_long_factor
2281
+ else:
2282
+ is_invested_short = is_invested_short_bool
2283
+ is_invested_long = is_invested_long_bool
2284
+
2285
+ constraints += [
2286
+ is_invested_bool <= 1.0,
2287
+ w * scale_constraints
2288
+ <= cp.multiply(is_invested_long, max_weights) * scale_constraints,
2289
+ w * scale_constraints
2290
+ >= cp.multiply(is_invested_short, min_weights) * scale_constraints,
2291
+ # Apply threshold_long if is_invested_long == 1,
2292
+ # unrestricted if is_invested_long == 0
2293
+ w * scale_constraints
2294
+ >= cp.multiply(is_invested_long, threshold_long) * scale_constraints
2295
+ - M * (1 - is_invested_long_bool) * scale_constraints,
2296
+ # # Apply threshold_short if is_invested_short == 1,
2297
+ # # unrestricted if is_invested_short == 0
2298
+ w * scale_constraints
2299
+ <= cp.multiply(is_invested_short, threshold_short) * scale_constraints
2300
+ + M * (1 - is_invested_short_bool) * scale_constraints,
2301
+ ]
2302
+
2303
+ return constraints
@@ -8,6 +8,7 @@ import cvxpy as cp
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
10
  import sklearn.utils.metadata_routing as skm
11
+ import sklearn.utils.validation as skv
11
12
 
12
13
  import skfolio.typing as skt
13
14
  from skfolio.measures import RiskMeasure
@@ -331,7 +332,9 @@ class DistributionallyRobustCVaR(ConvexOptimization):
331
332
  """
332
333
  routed_params = skm.process_routing(self, "fit", **fit_params)
333
334
 
334
- self._check_feature_names(X, reset=True)
335
+ # `X` is unchanged and only `feature_names_in_` is performed
336
+ _ = skv.validate_data(self, X, skip_check_array=True)
337
+
335
338
  # Used to avoid adding multiple times similar constrains linked to identical
336
339
  # risk models
337
340
  self.prior_estimator_ = check_estimator(
@@ -6,6 +6,7 @@
6
6
 
7
7
  import numpy as np
8
8
  import numpy.typing as npt
9
+ import sklearn.utils.validation as skv
9
10
 
10
11
  import skfolio.typing as skt
11
12
  from skfolio.measures import RiskMeasure
@@ -364,7 +365,7 @@ class MaximumDiversification(MeanRisk):
364
365
  ):
365
366
  super().__init__(
366
367
  objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
367
- risk_measure=RiskMeasure.VARIANCE,
368
+ risk_measure=RiskMeasure.STANDARD_DEVIATION,
368
369
  prior_estimator=prior_estimator,
369
370
  min_weights=min_weights,
370
371
  max_weights=max_weights,
@@ -423,7 +424,8 @@ class MaximumDiversification(MeanRisk):
423
424
  self : MaximumDiversification
424
425
  Fitted estimator.
425
426
  """
426
- self._check_feature_names(X, reset=True)
427
+ # `X` is unchanged and only `feature_names_in_` is performed
428
+ _ = skv.validate_data(self, X, skip_check_array=True)
427
429
 
428
430
  def func(w, obj):
429
431
  """weighted volatilities"""