skfolio 0.5.1__tar.gz → 0.6.0__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.
Files changed (101) hide show
  1. {skfolio-0.5.1/src/skfolio.egg-info → skfolio-0.6.0}/PKG-INFO +13 -5
  2. {skfolio-0.5.1 → skfolio-0.6.0}/README.rst +6 -3
  3. {skfolio-0.5.1 → skfolio-0.6.0}/pyproject.toml +8 -3
  4. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/_base.py +4 -1
  5. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/_base.py +342 -30
  6. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/_maximum_diversification.py +1 -1
  7. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/_mean_risk.py +120 -15
  8. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/equations.py +58 -1
  9. {skfolio-0.5.1 → skfolio-0.6.0/src/skfolio.egg-info}/PKG-INFO +13 -5
  10. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio.egg-info/requires.txt +6 -1
  11. {skfolio-0.5.1 → skfolio-0.6.0}/LICENSE +0 -0
  12. {skfolio-0.5.1 → skfolio-0.6.0}/MANIFEST.in +0 -0
  13. {skfolio-0.5.1 → skfolio-0.6.0}/setup.cfg +0 -0
  14. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/__init__.py +0 -0
  15. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/cluster/__init__.py +0 -0
  16. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/cluster/_hierarchical.py +0 -0
  17. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/__init__.py +0 -0
  18. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/data/__init__.py +0 -0
  19. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  20. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  21. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/datasets/data/sp500_index.csv.gz +0 -0
  22. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/distance/__init__.py +0 -0
  23. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/distance/_base.py +0 -0
  24. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/distance/_distance.py +0 -0
  25. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/exceptions.py +0 -0
  26. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/measures/__init__.py +0 -0
  27. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/measures/_enums.py +0 -0
  28. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/measures/_measures.py +0 -0
  29. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/metrics/__init__.py +0 -0
  30. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/metrics/_scorer.py +0 -0
  31. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/model_selection/__init__.py +0 -0
  32. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/model_selection/_combinatorial.py +0 -0
  33. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/model_selection/_validation.py +0 -0
  34. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/model_selection/_walk_forward.py +0 -0
  35. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/__init__.py +0 -0
  36. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/__init__.py +0 -0
  37. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_base.py +0 -0
  38. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_denoise_covariance.py +0 -0
  39. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_detone_covariance.py +0 -0
  40. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_empirical_covariance.py +0 -0
  41. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_ew_covariance.py +0 -0
  42. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_gerber_covariance.py +0 -0
  43. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_graphical_lasso_cv.py +0 -0
  44. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_implied_covariance.py +0 -0
  45. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_ledoit_wolf.py +0 -0
  46. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_oas.py +0 -0
  47. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/covariance/_shrunk_covariance.py +0 -0
  48. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/__init__.py +0 -0
  49. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/_base.py +0 -0
  50. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/_empirical_mu.py +0 -0
  51. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/_equilibrium_mu.py +0 -0
  52. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/_ew_mu.py +0 -0
  53. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/moments/expected_returns/_shrunk_mu.py +0 -0
  54. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/__init__.py +0 -0
  55. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/_base.py +0 -0
  56. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/__init__.py +0 -0
  57. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/_nco.py +0 -0
  58. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/hierarchical/__init__.py +0 -0
  59. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/hierarchical/_base.py +0 -0
  60. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/hierarchical/_herc.py +0 -0
  61. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/cluster/hierarchical/_hrp.py +0 -0
  62. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/__init__.py +0 -0
  63. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/_distributionally_robust.py +0 -0
  64. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/convex/_risk_budgeting.py +0 -0
  65. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/ensemble/__init__.py +0 -0
  66. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/ensemble/_base.py +0 -0
  67. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/ensemble/_stacking.py +0 -0
  68. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/naive/__init__.py +0 -0
  69. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/optimization/naive/_naive.py +0 -0
  70. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/population/__init__.py +0 -0
  71. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/population/_population.py +0 -0
  72. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/portfolio/__init__.py +0 -0
  73. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/portfolio/_base.py +0 -0
  74. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/portfolio/_multi_period_portfolio.py +0 -0
  75. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/portfolio/_portfolio.py +0 -0
  76. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/__init__.py +0 -0
  77. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/_drop_correlated.py +0 -0
  78. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/_select_complete.py +0 -0
  79. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/_select_k_extremes.py +0 -0
  80. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/_select_non_dominated.py +0 -0
  81. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/pre_selection/_select_non_expiring.py +0 -0
  82. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/preprocessing/__init__.py +0 -0
  83. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/preprocessing/_returns.py +0 -0
  84. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/prior/__init__.py +0 -0
  85. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/prior/_base.py +0 -0
  86. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/prior/_black_litterman.py +0 -0
  87. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/prior/_empirical.py +0 -0
  88. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/prior/_factor_model.py +0 -0
  89. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/typing.py +0 -0
  90. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/uncertainty_set/__init__.py +0 -0
  91. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/uncertainty_set/_base.py +0 -0
  92. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/uncertainty_set/_bootstrap.py +0 -0
  93. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/uncertainty_set/_empirical.py +0 -0
  94. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/__init__.py +0 -0
  95. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/bootstrap.py +0 -0
  96. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/sorting.py +0 -0
  97. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/stats.py +0 -0
  98. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio/utils/tools.py +0 -0
  99. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio.egg-info/SOURCES.txt +0 -0
  100. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio.egg-info/dependency_links.txt +0 -0
  101. {skfolio-0.5.1 → skfolio-0.6.0}/src/skfolio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skfolio
3
- Version: 0.5.1
3
+ Version: 0.6.0
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>
@@ -64,16 +64,18 @@ Requires-Dist: scikit-learn>=1.5.0
64
64
  Requires-Dist: joblib>=1.3.2
65
65
  Requires-Dist: plotly>=5.22.0
66
66
  Provides-Extra: tests
67
+ Requires-Dist: cvxpy[SCIP]; extra == "tests"
67
68
  Requires-Dist: pytest; extra == "tests"
68
69
  Requires-Dist: pytest-cov; extra == "tests"
69
70
  Requires-Dist: ruff; extra == "tests"
70
71
  Provides-Extra: docs
72
+ Requires-Dist: cvxpy[SCIP]; extra == "docs"
71
73
  Requires-Dist: Sphinx; extra == "docs"
72
74
  Requires-Dist: sphinx-gallery; extra == "docs"
73
75
  Requires-Dist: sphinx-design; extra == "docs"
74
76
  Requires-Dist: pydata-sphinx-theme==0.13.3; extra == "docs"
75
77
  Requires-Dist: matplotlib; extra == "docs"
76
- Requires-Dist: kaleido; extra == "docs"
78
+ Requires-Dist: kaleido==0.2.1; extra == "docs"
77
79
  Requires-Dist: sphinx-copybutton; extra == "docs"
78
80
  Requires-Dist: numpydoc; extra == "docs"
79
81
  Requires-Dist: sphinx-togglebutton; extra == "docs"
@@ -82,10 +84,13 @@ Requires-Dist: sphinx-prompt; extra == "docs"
82
84
  Requires-Dist: sphinxext.opengraph; extra == "docs"
83
85
  Requires-Dist: sphinx-sitemap; extra == "docs"
84
86
  Requires-Dist: sphinx-favicon; extra == "docs"
87
+ Requires-Dist: jupyterlite-sphinx; extra == "docs"
88
+ Requires-Dist: jupyterlite-pyodide-kernel; extra == "docs"
89
+ Requires-Dist: nbformat; extra == "docs"
85
90
 
86
91
  .. -*- mode: rst -*-
87
92
 
88
- |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website|
93
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite|
89
94
 
90
95
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
91
96
  :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
@@ -117,14 +122,17 @@ Requires-Dist: sphinx-favicon; extra == "docs"
117
122
  .. |Website| image:: https://img.shields.io/website.svg?down_color=red&down_message=down&up_color=53cc0d&up_message=up&url=https://skfolio.org
118
123
  :target: https://skfolio.org
119
124
 
125
+ .. |JupyterLite| image:: https://jupyterlite.rtfd.io/en/latest/_static/badge.svg
126
+ :target: https://skfolio.org/lite
127
+
120
128
  .. |PythonMinVersion| replace:: 3.10
121
129
  .. |NumpyMinVersion| replace:: 1.23.4
122
130
  .. |ScipyMinVersion| replace:: 1.8.0
123
131
  .. |PandasMinVersion| replace:: 1.4.1
124
132
  .. |CvxpyMinVersion| replace:: 1.4.1
125
- .. |SklearnMinVersion| replace:: 1.3.2
133
+ .. |SklearnMinVersion| replace:: 1.5.0
126
134
  .. |JoblibMinVersion| replace:: 1.3.2
127
- .. |PlotlyMinVersion| replace:: 5.15.0
135
+ .. |PlotlyMinVersion| replace:: 5.22.0
128
136
 
129
137
 
130
138
  ===============
@@ -1,6 +1,6 @@
1
1
  .. -*- mode: rst -*-
2
2
 
3
- |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website|
3
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite|
4
4
 
5
5
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
6
6
  :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
@@ -32,14 +32,17 @@
32
32
  .. |Website| image:: https://img.shields.io/website.svg?down_color=red&down_message=down&up_color=53cc0d&up_message=up&url=https://skfolio.org
33
33
  :target: https://skfolio.org
34
34
 
35
+ .. |JupyterLite| image:: https://jupyterlite.rtfd.io/en/latest/_static/badge.svg
36
+ :target: https://skfolio.org/lite
37
+
35
38
  .. |PythonMinVersion| replace:: 3.10
36
39
  .. |NumpyMinVersion| replace:: 1.23.4
37
40
  .. |ScipyMinVersion| replace:: 1.8.0
38
41
  .. |PandasMinVersion| replace:: 1.4.1
39
42
  .. |CvxpyMinVersion| replace:: 1.4.1
40
- .. |SklearnMinVersion| replace:: 1.3.2
43
+ .. |SklearnMinVersion| replace:: 1.5.0
41
44
  .. |JoblibMinVersion| replace:: 1.3.2
42
- .. |PlotlyMinVersion| replace:: 5.15.0
45
+ .. |PlotlyMinVersion| replace:: 5.22.0
43
46
 
44
47
 
45
48
  ===============
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skfolio"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  maintainers = [
9
9
  { name = "Hugo Delatte", email = "delatte.hugo@gmail.com" },
10
10
  ]
@@ -60,17 +60,19 @@ classifiers = [
60
60
 
61
61
  [project.optional-dependencies]
62
62
  tests = [
63
+ "cvxpy[SCIP]",
63
64
  "pytest",
64
65
  "pytest-cov",
65
66
  "ruff"
66
67
  ]
67
68
  docs = [
69
+ "cvxpy[SCIP]",
68
70
  "Sphinx",
69
71
  "sphinx-gallery",
70
72
  "sphinx-design",
71
73
  "pydata-sphinx-theme==0.13.3",
72
74
  "matplotlib",
73
- "kaleido",
75
+ "kaleido==0.2.1",
74
76
  "sphinx-copybutton",
75
77
  "numpydoc",
76
78
  "sphinx-togglebutton",
@@ -78,7 +80,10 @@ docs = [
78
80
  "sphinx-prompt",
79
81
  "sphinxext.opengraph",
80
82
  "sphinx-sitemap",
81
- "sphinx-favicon"
83
+ "sphinx-favicon",
84
+ "jupyterlite-sphinx",
85
+ "jupyterlite-pyodide-kernel",
86
+ "nbformat",
82
87
  ]
83
88
 
84
89
  [project.urls]
@@ -11,6 +11,7 @@
11
11
  import gzip
12
12
  import os
13
13
  import shutil
14
+ import sys
14
15
  import urllib.request as ur
15
16
  from importlib import resources
16
17
  from pathlib import Path
@@ -140,7 +141,9 @@ def download_dataset(
140
141
  DataFrame with each row representing one observation and each column
141
142
  representing the asset price of a given observation.
142
143
  """
143
- url = (
144
+ # Use a CORS proxy when triggering requests from the browser
145
+ url_prefix = "https://corsproxy.io/?" if sys.platform == "emscripten" else ""
146
+ url = url_prefix + (
144
147
  f"https://github.com/skfolio/skfolio-datasets/raw/main/"
145
148
  f"datasets/{data_filename}.csv.gz"
146
149
  )
@@ -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,8 +1679,8 @@ 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
 
@@ -1972,7 +2126,7 @@ class ConvexOptimization(BaseOptimization, ABC):
1972
2126
  ptf_returns * self._scale_constraints
1973
2127
  - ptf_transaction_cost * self._scale_constraints
1974
2128
  - ptf_management_fee * self._scale_constraints
1975
- == cp.reshape(z, (observation_nb,)) * self._scale_constraints,
2129
+ == cp.reshape(z, (observation_nb,), order="F") * self._scale_constraints,
1976
2130
  z @ gmd_w.T <= ones @ x.T + y @ ones.T,
1977
2131
  ]
1978
2132
  return risk, constraints
@@ -1988,3 +2142,161 @@ class ConvexOptimization(BaseOptimization, ABC):
1988
2142
  @abstractmethod
1989
2143
  def fit(self, X: npt.ArrayLike, y: npt.ArrayLike | None = None, **fit_params):
1990
2144
  pass
2145
+
2146
+
2147
+ def _mip_weight_constraints_no_short_threshold(
2148
+ n_assets: int,
2149
+ w: cp.Variable,
2150
+ factor: skt.Factor,
2151
+ scale_constraints: cp.Constant,
2152
+ cardinality: int | None,
2153
+ group_cardinalities: dict[str, int] | None,
2154
+ max_weights: np.ndarray | None,
2155
+ groups: np.ndarray | None,
2156
+ min_weights: np.ndarray | None,
2157
+ threshold_long: np.ndarray | None,
2158
+ ) -> list[cp.Expression]:
2159
+ """
2160
+ Create a list of MIP constraints for cardinality and threshold conditions
2161
+ when no short threshold is present. This only requires the creation of a single
2162
+ boolean variable array.
2163
+ """
2164
+ constraints = []
2165
+
2166
+ is_short = np.any(min_weights < 0)
2167
+
2168
+ is_invested_bool = cp.Variable(n_assets, boolean=True)
2169
+
2170
+ if cardinality is not None and cardinality < n_assets:
2171
+ constraints.append(cp.sum(is_invested_bool) <= cardinality)
2172
+
2173
+ if group_cardinalities is not None:
2174
+ a_card, b_card = group_cardinalities_to_matrix(
2175
+ groups=groups,
2176
+ group_cardinalities=group_cardinalities,
2177
+ raise_if_group_missing=False,
2178
+ )
2179
+ constraints.append(a_card @ is_invested_bool - b_card <= 0)
2180
+
2181
+ if isinstance(factor, cp.Variable):
2182
+ is_invested_factor = cp.Variable(n_assets, nonneg=True)
2183
+ # We want (w <= cp.multiply(is_invested_short_bool, max_weights) * factor
2184
+ # but this is not DCP. So we introduce another variable and set
2185
+ # constraint to ensure its value is equal to is_invested_short_bool * factor
2186
+
2187
+ M = 1e3
2188
+ # Big M method to activate or deactivate constraints
2189
+ # In the ratio homogenization procedure, the factor has been calibrated
2190
+ # to be around 0.1-10. By using M=1e3, we ensure that M is large enough while
2191
+ # not too large for improved MIP convergence.
2192
+
2193
+ constraints += [
2194
+ is_invested_factor <= factor,
2195
+ is_invested_factor <= M * is_invested_bool,
2196
+ is_invested_factor >= factor - M * (1 - is_invested_bool),
2197
+ ]
2198
+ is_invested = is_invested_factor
2199
+ else:
2200
+ is_invested = is_invested_bool
2201
+
2202
+ if threshold_long is not None:
2203
+ constraints.append(
2204
+ w * scale_constraints
2205
+ >= cp.multiply(is_invested, threshold_long) * scale_constraints
2206
+ )
2207
+
2208
+ constraints.append(
2209
+ w * scale_constraints
2210
+ <= cp.multiply(is_invested, max_weights) * scale_constraints
2211
+ )
2212
+
2213
+ if is_short:
2214
+ constraints.append(
2215
+ w * scale_constraints
2216
+ >= cp.multiply(is_invested, min_weights) * scale_constraints
2217
+ )
2218
+
2219
+ return constraints
2220
+
2221
+
2222
+ def _mip_weight_constraints_threshold_short(
2223
+ n_assets: int,
2224
+ w: cp.Variable,
2225
+ factor: skt.Factor,
2226
+ scale_constraints: cp.Constant,
2227
+ max_weights: np.ndarray,
2228
+ min_weights: np.ndarray,
2229
+ threshold_long: np.ndarray,
2230
+ threshold_short: np.ndarray,
2231
+ cardinality: int | None,
2232
+ group_cardinalities: dict[str, int] | None,
2233
+ groups: np.ndarray | None,
2234
+ ) -> list[cp.Expression]:
2235
+ """
2236
+ Create a list of MIP constraints for cardinality and threshold constraints
2237
+ when a short threshold is allowed. This requires the creation of two boolean
2238
+ variable arrays, one for long positions and one for short positions.
2239
+ """
2240
+ constraints = []
2241
+
2242
+ is_invested_short_bool = cp.Variable(n_assets, boolean=True)
2243
+ is_invested_long_bool = cp.Variable(n_assets, boolean=True)
2244
+ is_invested_bool = is_invested_short_bool + is_invested_long_bool
2245
+
2246
+ if cardinality is not None and cardinality < n_assets:
2247
+ constraints.append(cp.sum(is_invested_bool) <= cardinality)
2248
+
2249
+ if group_cardinalities is not None:
2250
+ a_card, b_card = group_cardinalities_to_matrix(
2251
+ groups=groups,
2252
+ group_cardinalities=group_cardinalities,
2253
+ raise_if_group_missing=False,
2254
+ )
2255
+ constraints.append(a_card @ is_invested_bool - b_card <= 0)
2256
+
2257
+ M = 1e3
2258
+ # Big M method to activate or deactivate constraints
2259
+ # In the ratio homogenization procedure, the factor has been calibrated
2260
+ # to be around 0.1-10. By using M=1e3, we ensure that M is large enough while
2261
+ # not too large for improved MIP convergence.
2262
+
2263
+ if isinstance(factor, cp.Variable):
2264
+ is_invested_short_factor = cp.Variable(n_assets, nonneg=True)
2265
+ is_invested_long_factor = cp.Variable(n_assets, nonneg=True)
2266
+ # We want (w <= cp.multiply(is_invested_short_bool, max_weights) * factor
2267
+ # but this is not DCP. So we introduce another variable and set
2268
+ # constraint to ensure its value is equal to is_invested_short_bool * factor
2269
+
2270
+ constraints += [
2271
+ is_invested_short_factor <= factor,
2272
+ is_invested_long_factor <= factor,
2273
+ is_invested_short_factor <= M * is_invested_short_bool,
2274
+ is_invested_long_factor <= M * is_invested_long_bool,
2275
+ is_invested_short_factor >= factor - M * (1 - is_invested_short_bool),
2276
+ is_invested_long_factor >= factor - M * (1 - is_invested_long_bool),
2277
+ ]
2278
+ is_invested_short = is_invested_short_factor
2279
+ is_invested_long = is_invested_long_factor
2280
+ else:
2281
+ is_invested_short = is_invested_short_bool
2282
+ is_invested_long = is_invested_long_bool
2283
+
2284
+ constraints += [
2285
+ is_invested_bool <= 1.0,
2286
+ w * scale_constraints
2287
+ <= cp.multiply(is_invested_long, max_weights) * scale_constraints,
2288
+ w * scale_constraints
2289
+ >= cp.multiply(is_invested_short, min_weights) * scale_constraints,
2290
+ # Apply threshold_long if is_invested_long == 1,
2291
+ # unrestricted if is_invested_long == 0
2292
+ w * scale_constraints
2293
+ >= cp.multiply(is_invested_long, threshold_long) * scale_constraints
2294
+ - M * (1 - is_invested_long_bool) * scale_constraints,
2295
+ # # Apply threshold_short if is_invested_short == 1,
2296
+ # # unrestricted if is_invested_short == 0
2297
+ w * scale_constraints
2298
+ <= cp.multiply(is_invested_short, threshold_short) * scale_constraints
2299
+ + M * (1 - is_invested_short_bool) * scale_constraints,
2300
+ ]
2301
+
2302
+ return constraints
@@ -364,7 +364,7 @@ class MaximumDiversification(MeanRisk):
364
364
  ):
365
365
  super().__init__(
366
366
  objective_function=ObjectiveFunction.MAXIMIZE_RATIO,
367
- risk_measure=RiskMeasure.VARIANCE,
367
+ risk_measure=RiskMeasure.STANDARD_DEVIATION,
368
368
  prior_estimator=prior_estimator,
369
369
  min_weights=min_weights,
370
370
  max_weights=max_weights,