skfolio 0.9.0__tar.gz → 0.10.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 (132) hide show
  1. {skfolio-0.9.0/src/skfolio.egg-info → skfolio-0.10.0}/PKG-INFO +94 -5
  2. {skfolio-0.9.0 → skfolio-0.10.0}/README.rst +93 -4
  3. {skfolio-0.9.0 → skfolio-0.10.0}/pyproject.toml +1 -1
  4. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/multivariate/_vine_copula.py +35 -34
  5. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_base.py +20 -15
  6. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/exceptions.py +5 -0
  7. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/measures/__init__.py +2 -0
  8. skfolio-0.10.0/src/skfolio/measures/_measures.py +868 -0
  9. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/_base.py +21 -4
  10. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/hierarchical/_base.py +16 -13
  11. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
  12. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
  13. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/_base.py +238 -144
  14. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/_distributionally_robust.py +32 -20
  15. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/_maximum_diversification.py +15 -18
  16. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/_mean_risk.py +35 -25
  17. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/_risk_budgeting.py +23 -21
  18. skfolio-0.10.0/src/skfolio/optimization/ensemble/__init__.py +6 -0
  19. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/ensemble/_stacking.py +1 -1
  20. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/naive/_naive.py +2 -2
  21. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/population/_population.py +30 -9
  22. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/portfolio/_base.py +68 -26
  23. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/portfolio/_multi_period_portfolio.py +5 -0
  24. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/portfolio/_portfolio.py +5 -0
  25. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_select_non_expiring.py +1 -1
  26. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/__init__.py +6 -2
  27. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/_base.py +7 -3
  28. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/_black_litterman.py +14 -12
  29. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/_empirical.py +8 -7
  30. skfolio-0.10.0/src/skfolio/prior/_entropy_pooling.py +1493 -0
  31. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/_factor_model.py +39 -22
  32. skfolio-0.10.0/src/skfolio/prior/_opinion_pooling.py +475 -0
  33. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/prior/_synthetic_data.py +10 -8
  34. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/uncertainty_set/_bootstrap.py +4 -4
  35. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/uncertainty_set/_empirical.py +6 -6
  36. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/equations.py +11 -5
  37. skfolio-0.10.0/src/skfolio/utils/figure.py +185 -0
  38. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/tools.py +4 -2
  39. {skfolio-0.9.0 → skfolio-0.10.0/src/skfolio.egg-info}/PKG-INFO +94 -5
  40. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio.egg-info/SOURCES.txt +4 -2
  41. skfolio-0.9.0/src/skfolio/measures/_measures.py +0 -633
  42. skfolio-0.9.0/src/skfolio/optimization/ensemble/__init__.py +0 -8
  43. skfolio-0.9.0/src/skfolio/synthetic_returns/__init__.py +0 -1
  44. {skfolio-0.9.0 → skfolio-0.10.0}/LICENSE +0 -0
  45. {skfolio-0.9.0 → skfolio-0.10.0}/MANIFEST.in +0 -0
  46. {skfolio-0.9.0 → skfolio-0.10.0}/setup.cfg +0 -0
  47. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/__init__.py +0 -0
  48. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/cluster/__init__.py +0 -0
  49. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/cluster/_hierarchical.py +0 -0
  50. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/__init__.py +0 -0
  51. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/_base.py +0 -0
  52. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/data/__init__.py +0 -0
  53. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  54. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  55. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/datasets/data/sp500_index.csv.gz +0 -0
  56. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distance/__init__.py +0 -0
  57. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distance/_base.py +0 -0
  58. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distance/_distance.py +0 -0
  59. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/__init__.py +0 -0
  60. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/_base.py +0 -0
  61. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/__init__.py +0 -0
  62. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_base.py +0 -0
  63. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_clayton.py +0 -0
  64. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_gaussian.py +0 -0
  65. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_gumbel.py +0 -0
  66. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_independent.py +0 -0
  67. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_joe.py +0 -0
  68. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_selection.py +0 -0
  69. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_student_t.py +0 -0
  70. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/copula/_utils.py +0 -0
  71. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/multivariate/__init__.py +0 -0
  72. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/multivariate/_base.py +0 -0
  73. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/multivariate/_utils.py +0 -0
  74. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/__init__.py +0 -0
  75. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_gaussian.py +0 -0
  76. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_johnson_su.py +0 -0
  77. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_normal_inverse_gaussian.py +0 -0
  78. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_selection.py +0 -0
  79. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/distribution/univariate/_student_t.py +0 -0
  80. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/measures/_enums.py +0 -0
  81. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/metrics/__init__.py +0 -0
  82. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/metrics/_scorer.py +0 -0
  83. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/model_selection/__init__.py +0 -0
  84. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/model_selection/_combinatorial.py +0 -0
  85. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/model_selection/_validation.py +0 -0
  86. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/model_selection/_walk_forward.py +0 -0
  87. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/__init__.py +0 -0
  88. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/__init__.py +0 -0
  89. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_base.py +0 -0
  90. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_denoise_covariance.py +0 -0
  91. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_detone_covariance.py +0 -0
  92. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_empirical_covariance.py +0 -0
  93. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_ew_covariance.py +0 -0
  94. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_gerber_covariance.py +0 -0
  95. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_graphical_lasso_cv.py +0 -0
  96. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_implied_covariance.py +0 -0
  97. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_ledoit_wolf.py +0 -0
  98. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_oas.py +0 -0
  99. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/covariance/_shrunk_covariance.py +0 -0
  100. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/__init__.py +0 -0
  101. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/_base.py +0 -0
  102. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/_empirical_mu.py +0 -0
  103. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/_equilibrium_mu.py +0 -0
  104. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/_ew_mu.py +0 -0
  105. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/moments/expected_returns/_shrunk_mu.py +0 -0
  106. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/__init__.py +0 -0
  107. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/__init__.py +0 -0
  108. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/_nco.py +0 -0
  109. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/cluster/hierarchical/__init__.py +0 -0
  110. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/convex/__init__.py +0 -0
  111. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/optimization/naive/__init__.py +0 -0
  112. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/population/__init__.py +0 -0
  113. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/portfolio/__init__.py +0 -0
  114. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/__init__.py +0 -0
  115. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_drop_correlated.py +0 -0
  116. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_drop_zero_variance.py +0 -0
  117. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_select_complete.py +0 -0
  118. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_select_k_extremes.py +0 -0
  119. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/pre_selection/_select_non_dominated.py +0 -0
  120. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/preprocessing/__init__.py +0 -0
  121. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/preprocessing/_returns.py +0 -0
  122. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/typing.py +0 -0
  123. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/uncertainty_set/__init__.py +0 -0
  124. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/uncertainty_set/_base.py +0 -0
  125. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/__init__.py +0 -0
  126. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/bootstrap.py +0 -0
  127. /skfolio-0.9.0/src/skfolio/optimization/ensemble/_base.py → /skfolio-0.10.0/src/skfolio/utils/composition.py +0 -0
  128. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/sorting.py +0 -0
  129. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio/utils/stats.py +0 -0
  130. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio.egg-info/dependency_links.txt +0 -0
  131. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio.egg-info/requires.txt +0 -0
  132. {skfolio-0.9.0 → skfolio-0.10.0}/src/skfolio.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skfolio
3
- Version: 0.9.0
3
+ Version: 0.10.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>, Matteo Manzi <matteomanzi09@gmail.com>
@@ -92,7 +92,7 @@ Dynamic: license-file
92
92
 
93
93
  .. -*- mode: rst -*-
94
94
 
95
- |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite|
95
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite| |Discord|
96
96
 
97
97
  .. |Licence| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
98
98
  :target: https://github.com/skfolio/skfolio/blob/main/LICENSE
@@ -127,6 +127,9 @@ Dynamic: license-file
127
127
  .. |JupyterLite| image:: https://jupyterlite.rtfd.io/en/latest/_static/badge.svg
128
128
  :target: https://skfolio.org/lite
129
129
 
130
+ .. |Discord| image:: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
131
+ :target: https://discord.gg/Bu7EtNYugS
132
+
130
133
  .. |PythonMinVersion| replace:: 3.10
131
134
  .. |NumpyMinVersion| replace:: 1.23.4
132
135
  .. |ScipyMinVersion| replace:: 1.8.0
@@ -281,6 +284,8 @@ Available models
281
284
  * Black & Litterman
282
285
  * Factor Model
283
286
  * Synthetic Data (Stress Test, Factor Stress Test)
287
+ * Entropy Pooling
288
+ * Opinion Pooling
284
289
 
285
290
  * Uncertainty Set Estimator:
286
291
  * On Expected Returns:
@@ -296,6 +301,7 @@ Available models
296
301
  * Drop Highly Correlated Assets
297
302
  * Select Non-Expiring Assets
298
303
  * Select Complete Assets (handle late inception, delisting, etc.)
304
+ * Drop Zero Variance
299
305
 
300
306
  * Cross-Validation and Model Selection:
301
307
  * Compatible with all `sklearn` methods (KFold, etc.)
@@ -385,7 +391,14 @@ Imports
385
391
  )
386
392
  from skfolio.pre_selection import SelectKExtremes
387
393
  from skfolio.preprocessing import prices_to_returns
388
- from skfolio.prior import BlackLitterman, EmpiricalPrior, FactorModel, SyntheticData
394
+ from skfolio.prior import (
395
+ BlackLitterman,
396
+ EmpiricalPrior,
397
+ EntropyPooling,
398
+ FactorModel,
399
+ OpinionPooling,
400
+ SyntheticData,
401
+ )
389
402
  from skfolio.uncertainty_set import BootstrapMuUncertaintySet
390
403
 
391
404
  Load Dataset
@@ -701,8 +714,84 @@ Factor Stress Test
701
714
  conditioning={"QUAL": -0.5}
702
715
  ))
703
716
  factor_model.fit(X,y)
704
- stressed_X = factor_model.prior_model_.returns
705
- stressed_ptf = model.predict(stressed_X)
717
+ stressed_dist = factor_model.return_distribution_
718
+ stressed_ptf = model.predict(stressed_dist)
719
+
720
+ Entropy Pooling
721
+ ---------------
722
+ .. code-block:: python
723
+
724
+ entropy_pooling = EntropyPooling(
725
+ mean_views=[
726
+ "JPM == -0.002",
727
+ "PG >= LLY",
728
+ "BAC >= prior(BAC) * 1.2",
729
+ ],
730
+ cvar_views=[
731
+ "GE == 0.08",
732
+ ],
733
+ )
734
+ entropy_pooling.fit(X)
735
+ print(entropy_pooling.relative_entropy_)
736
+ print(entropy_pooling.effective_number_of_scenarios_)
737
+ print(entropy_pooling.return_distribution_.sample_weight)
738
+
739
+ CVaR Hierarchical Risk Parity optimization on Entropy Pooling
740
+ -------------------------------------------------------------
741
+ .. code-block:: python
742
+
743
+ entropy_pooling = EntropyPooling(cvar_views=["GE == 0.08"])
744
+ model = HierarchicalRiskParity(
745
+ risk_measure=RiskMeasure.CVAR,
746
+ prior_estimator=entropy_pooling
747
+ )
748
+ model.fit(X)
749
+ print(model.weights_)
750
+
751
+ Stress Test with Entropy Pooling on Factor Synthetic Data
752
+ ---------------------------------------------------------
753
+ .. code-block:: python
754
+
755
+ # Regular Vine Copula and sampling of 100,000 synthetic factor returns
756
+ factor_synth = SyntheticData(
757
+ n_samples=100_000,
758
+ distribution_estimator=VineCopula(log_transform=True, n_jobs=-1, random_state=0)
759
+ )
760
+
761
+ # Entropy Pooling by imposing a CVaR-95% of 10% on the Quality factor
762
+ factor_entropy_pooling = EntropyPooling(
763
+ prior_estimator=factor_synth,
764
+ cvar_views=["QUAL == 0.10"],
765
+ )
766
+
767
+ factor_entropy_pooling.fit(X, factors)
768
+
769
+ # We retrieve the stressed distribution:
770
+ stressed_dist = factor_model.return_distribution_
771
+
772
+ # We stress-test our portfolio:
773
+ stressed_ptf = model.predict(stressed_dist)
774
+
775
+ Opinion Pooling
776
+ ---------------
777
+ .. code-block:: python
778
+
779
+ # We consider two expert opinions, each generated via Entropy Pooling with
780
+ # user-defined views.
781
+ # We assign probabilities of 40% to Expert 1, 50% to Expert 2, and by default
782
+ # the remaining 10% is allocated to the prior distribution:
783
+ opinion_1 = EntropyPooling(cvar_views=["AMD == 0.10"])
784
+ opinion_2 = EntropyPooling(
785
+ mean_views=["AMD >= BAC", "JPM <= prior(JPM) * 0.8"],
786
+ cvar_views=["GE == 0.12"],
787
+ )
788
+
789
+ opinion_pooling = OpinionPooling(
790
+ estimators=[("opinion_1", opinion_1), ("opinion_2", opinion_2)],
791
+ opinion_probabilities=[0.4, 0.5],
792
+ )
793
+
794
+ opinion_pooling.fit(X)
706
795
 
707
796
 
708
797
  Recognition
@@ -1,6 +1,6 @@
1
1
  .. -*- mode: rst -*-
2
2
 
3
- |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite|
3
+ |Licence| |Codecov| |Black| |PythonVersion| |PyPi| |CI/CD| |Downloads| |Ruff| |Contribution| |Website| |JupyterLite| |Discord|
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
@@ -35,6 +35,9 @@
35
35
  .. |JupyterLite| image:: https://jupyterlite.rtfd.io/en/latest/_static/badge.svg
36
36
  :target: https://skfolio.org/lite
37
37
 
38
+ .. |Discord| image:: https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white
39
+ :target: https://discord.gg/Bu7EtNYugS
40
+
38
41
  .. |PythonMinVersion| replace:: 3.10
39
42
  .. |NumpyMinVersion| replace:: 1.23.4
40
43
  .. |ScipyMinVersion| replace:: 1.8.0
@@ -189,6 +192,8 @@ Available models
189
192
  * Black & Litterman
190
193
  * Factor Model
191
194
  * Synthetic Data (Stress Test, Factor Stress Test)
195
+ * Entropy Pooling
196
+ * Opinion Pooling
192
197
 
193
198
  * Uncertainty Set Estimator:
194
199
  * On Expected Returns:
@@ -204,6 +209,7 @@ Available models
204
209
  * Drop Highly Correlated Assets
205
210
  * Select Non-Expiring Assets
206
211
  * Select Complete Assets (handle late inception, delisting, etc.)
212
+ * Drop Zero Variance
207
213
 
208
214
  * Cross-Validation and Model Selection:
209
215
  * Compatible with all `sklearn` methods (KFold, etc.)
@@ -293,7 +299,14 @@ Imports
293
299
  )
294
300
  from skfolio.pre_selection import SelectKExtremes
295
301
  from skfolio.preprocessing import prices_to_returns
296
- from skfolio.prior import BlackLitterman, EmpiricalPrior, FactorModel, SyntheticData
302
+ from skfolio.prior import (
303
+ BlackLitterman,
304
+ EmpiricalPrior,
305
+ EntropyPooling,
306
+ FactorModel,
307
+ OpinionPooling,
308
+ SyntheticData,
309
+ )
297
310
  from skfolio.uncertainty_set import BootstrapMuUncertaintySet
298
311
 
299
312
  Load Dataset
@@ -609,8 +622,84 @@ Factor Stress Test
609
622
  conditioning={"QUAL": -0.5}
610
623
  ))
611
624
  factor_model.fit(X,y)
612
- stressed_X = factor_model.prior_model_.returns
613
- stressed_ptf = model.predict(stressed_X)
625
+ stressed_dist = factor_model.return_distribution_
626
+ stressed_ptf = model.predict(stressed_dist)
627
+
628
+ Entropy Pooling
629
+ ---------------
630
+ .. code-block:: python
631
+
632
+ entropy_pooling = EntropyPooling(
633
+ mean_views=[
634
+ "JPM == -0.002",
635
+ "PG >= LLY",
636
+ "BAC >= prior(BAC) * 1.2",
637
+ ],
638
+ cvar_views=[
639
+ "GE == 0.08",
640
+ ],
641
+ )
642
+ entropy_pooling.fit(X)
643
+ print(entropy_pooling.relative_entropy_)
644
+ print(entropy_pooling.effective_number_of_scenarios_)
645
+ print(entropy_pooling.return_distribution_.sample_weight)
646
+
647
+ CVaR Hierarchical Risk Parity optimization on Entropy Pooling
648
+ -------------------------------------------------------------
649
+ .. code-block:: python
650
+
651
+ entropy_pooling = EntropyPooling(cvar_views=["GE == 0.08"])
652
+ model = HierarchicalRiskParity(
653
+ risk_measure=RiskMeasure.CVAR,
654
+ prior_estimator=entropy_pooling
655
+ )
656
+ model.fit(X)
657
+ print(model.weights_)
658
+
659
+ Stress Test with Entropy Pooling on Factor Synthetic Data
660
+ ---------------------------------------------------------
661
+ .. code-block:: python
662
+
663
+ # Regular Vine Copula and sampling of 100,000 synthetic factor returns
664
+ factor_synth = SyntheticData(
665
+ n_samples=100_000,
666
+ distribution_estimator=VineCopula(log_transform=True, n_jobs=-1, random_state=0)
667
+ )
668
+
669
+ # Entropy Pooling by imposing a CVaR-95% of 10% on the Quality factor
670
+ factor_entropy_pooling = EntropyPooling(
671
+ prior_estimator=factor_synth,
672
+ cvar_views=["QUAL == 0.10"],
673
+ )
674
+
675
+ factor_entropy_pooling.fit(X, factors)
676
+
677
+ # We retrieve the stressed distribution:
678
+ stressed_dist = factor_model.return_distribution_
679
+
680
+ # We stress-test our portfolio:
681
+ stressed_ptf = model.predict(stressed_dist)
682
+
683
+ Opinion Pooling
684
+ ---------------
685
+ .. code-block:: python
686
+
687
+ # We consider two expert opinions, each generated via Entropy Pooling with
688
+ # user-defined views.
689
+ # We assign probabilities of 40% to Expert 1, 50% to Expert 2, and by default
690
+ # the remaining 10% is allocated to the prior distribution:
691
+ opinion_1 = EntropyPooling(cvar_views=["AMD == 0.10"])
692
+ opinion_2 = EntropyPooling(
693
+ mean_views=["AMD >= BAC", "JPM <= prior(JPM) * 0.8"],
694
+ cvar_views=["GE == 0.12"],
695
+ )
696
+
697
+ opinion_pooling = OpinionPooling(
698
+ estimators=[("opinion_1", opinion_1), ("opinion_2", opinion_2)],
699
+ opinion_probabilities=[0.4, 0.5],
700
+ )
701
+
702
+ opinion_pooling.fit(X)
614
703
 
615
704
 
616
705
  Recognition
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skfolio"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  maintainers = [
9
9
  { name = "Hugo Delatte", email = "delatte.hugo@gmail.com" },
10
10
  { name = "Matteo Manzi", email = "matteomanzi09@gmail.com" }
@@ -34,7 +34,6 @@ import numpy as np
34
34
  import numpy.typing as npt
35
35
  import plotly.express as px
36
36
  import plotly.graph_objects as go
37
- import scipy.stats as st
38
37
  import sklearn.utils as sku
39
38
  import sklearn.utils.parallel as skp
40
39
  import sklearn.utils.validation as skv
@@ -65,6 +64,7 @@ from skfolio.distribution.univariate import (
65
64
  StudentT,
66
65
  select_univariate_dist,
67
66
  )
67
+ from skfolio.utils.figure import kde_trace
68
68
  from skfolio.utils.tools import input_to_array, validate_input_list
69
69
 
70
70
  _UNIFORM_SAMPLE_EPSILON = 1e-14
@@ -996,6 +996,7 @@ class VineCopula(BaseMultivariateDist):
996
996
  | None = None,
997
997
  subset: list[int | str] | None = None,
998
998
  n_samples: int = 500,
999
+ percentile_cutoff: float | None = None,
999
1000
  title: str = "Vine Copula Marginal Distributions",
1000
1001
  ) -> go.Figure:
1001
1002
  """
@@ -1025,7 +1026,7 @@ class VineCopula(BaseMultivariateDist):
1025
1026
  If an array-like of length `n_samples` is provided, each sample is
1026
1027
  conditioned on the corresponding value in the array for that asset.
1027
1028
 
1028
- **Important:** When using conditional sampling, it is recommended that the
1029
+ When using conditional sampling, it is recommended that the
1029
1030
  assets you condition on are set as central during the vine copula
1030
1031
  construction. This can be specified via the `central_assets` parameter in
1031
1032
  the vine copula instantiation.
@@ -1041,6 +1042,12 @@ class VineCopula(BaseMultivariateDist):
1041
1042
  rows than `n_samples`, the value is adjusted to match the number of rows in
1042
1043
  `X` to ensure balanced visualization.
1043
1044
 
1045
+ percentile_cutoff : float, default=None
1046
+ Percentile cutoff for tail truncation (percentile), in percent.
1047
+ If a float p is provided, the distribution support is truncated at
1048
+ the p-th and (100 - p)-th percentiles.
1049
+ If None, no truncation is applied (uses full min/max of returns).
1050
+
1044
1051
  title : str, default="Vine Copula Marginal Distributions"
1045
1052
  The title for the plot.
1046
1053
 
@@ -1051,7 +1058,6 @@ class VineCopula(BaseMultivariateDist):
1051
1058
  """
1052
1059
  n_assets = self.n_features_in_
1053
1060
  subset = subset or list(range(n_assets))
1054
- colors = px.colors.qualitative.Plotly
1055
1061
  if X is not None:
1056
1062
  X = np.asarray(X)
1057
1063
  if X.ndim != 2:
@@ -1070,30 +1076,43 @@ class VineCopula(BaseMultivariateDist):
1070
1076
  n_samples = X.shape[0]
1071
1077
 
1072
1078
  samples = self.sample(n_samples=n_samples, conditioning=conditioning)
1079
+ colors = px.colors.qualitative.Plotly
1073
1080
 
1074
- traces = []
1081
+ traces: list[go.Scatter] = []
1075
1082
  for i, s in enumerate(subset):
1083
+ visible = True if i == 0 else "legendonly"
1084
+ color = colors[i % len(colors)]
1085
+ asset = self.feature_names_in_[s]
1086
+
1076
1087
  traces.append(
1077
- _kde_trace(
1088
+ kde_trace(
1078
1089
  x=samples[:, s],
1079
- opacity=1.0,
1080
- color=colors[i % len(colors)],
1081
- name=f"{self.feature_names_in_[s]} Generated",
1082
- visible=True if i == 0 else "legendonly",
1090
+ sample_weight=None,
1091
+ percentile_cutoff=percentile_cutoff,
1092
+ name=f"{asset} Generated",
1093
+ line_color=color,
1094
+ fill_opacity=0.17,
1095
+ line_dash="solid",
1096
+ line_width=1,
1097
+ visible=visible,
1083
1098
  )
1084
1099
  )
1085
1100
 
1086
- if X is not None:
1087
- for i, s in enumerate(subset):
1101
+ if X is not None:
1088
1102
  traces.append(
1089
- _kde_trace(
1103
+ kde_trace(
1090
1104
  x=X[:, s],
1091
- opacity=0.6,
1092
- color=colors[i % len(colors)],
1093
- name=f"{self.feature_names_in_[s]} Empirical",
1094
- visible=True if i == 0 else "legendonly",
1105
+ sample_weight=None,
1106
+ percentile_cutoff=percentile_cutoff,
1107
+ name=f"{asset} Empirical",
1108
+ line_color=color,
1109
+ fill_opacity=0.17,
1110
+ line_dash="dash",
1111
+ line_width=1.5,
1112
+ visible=visible,
1095
1113
  )
1096
1114
  )
1115
+
1097
1116
  fig = go.Figure(data=traces)
1098
1117
  fig.update_layout(
1099
1118
  title=title,
@@ -1234,21 +1253,3 @@ def _inverse_partial_derivative(
1234
1253
  if is_count:
1235
1254
  return np.array([np.nan])
1236
1255
  return edge.copula.inverse_partial_derivative(X)
1237
-
1238
-
1239
- def _kde_trace(
1240
- x: np.ndarray, opacity: float, color: str, name: str, visible: bool
1241
- ) -> go.Scatter:
1242
- """Gaussian KDE line plot."""
1243
- kde = st.gaussian_kde(x)
1244
- x = np.linspace(min(x), max(x), 500)
1245
- return go.Scatter(
1246
- x=x,
1247
- y=kde(x),
1248
- mode="lines",
1249
- name=name,
1250
- line=dict(color=color),
1251
- fill="tozeroy",
1252
- opacity=opacity,
1253
- visible=visible,
1254
- )
@@ -202,6 +202,23 @@ class BaseUnivariateDist(BaseDistribution, ABC):
202
202
  x = np.linspace(lower_bound, upper_bound, 1000)
203
203
 
204
204
  traces = []
205
+
206
+ with warnings.catch_warnings():
207
+ warnings.filterwarnings("ignore", category=UserWarning)
208
+ pdfs = np.exp(self.score_samples(x.reshape(-1, 1)))
209
+ traces.append(
210
+ go.Scatter(
211
+ x=x,
212
+ y=pdfs.flatten(),
213
+ mode="lines",
214
+ name=self.__class__.__name__,
215
+ line=dict(color="rgb(31, 119, 180)", dash="solid", width=1),
216
+ fill="tozeroy",
217
+ fillcolor="rgba(31, 119, 180, 0.17)",
218
+ opacity=1.0,
219
+ )
220
+ )
221
+
205
222
  if X is not None:
206
223
  with warnings.catch_warnings():
207
224
  warnings.filterwarnings(
@@ -216,25 +233,13 @@ class BaseUnivariateDist(BaseDistribution, ABC):
216
233
  y=y_kde,
217
234
  mode="lines",
218
235
  name="Empirical KDE",
219
- line=dict(color="rgb(85,168,104)"),
236
+ line=dict(color="rgb(85, 168, 104)", dash="dash", width=2),
220
237
  fill="tozeroy",
238
+ fillcolor="rgba(85, 168, 104, 0.17)",
239
+ opacity=1.0,
221
240
  )
222
241
  )
223
242
 
224
- with warnings.catch_warnings():
225
- warnings.filterwarnings("ignore", category=UserWarning)
226
- pdfs = np.exp(self.score_samples(x.reshape(-1, 1)))
227
- traces.append(
228
- go.Scatter(
229
- x=x,
230
- y=pdfs.flatten(),
231
- mode="lines",
232
- name=self.__class__.__name__,
233
- line=dict(color="rgb(31, 119, 180)"),
234
- fill="tozeroy",
235
- )
236
- )
237
-
238
243
  fig = go.Figure(data=traces)
239
244
  fig.update_layout(
240
245
  title=title,
@@ -13,6 +13,7 @@ __all__ = [
13
13
  "GroupNotFoundError",
14
14
  "NonPositiveVarianceError",
15
15
  "OptimizationError",
16
+ "SolverError",
16
17
  ]
17
18
 
18
19
 
@@ -20,6 +21,10 @@ class OptimizationError(Exception):
20
21
  """Optimization Did not converge."""
21
22
 
22
23
 
24
+ class SolverError(Exception):
25
+ """Solver error."""
26
+
27
+
23
28
  class EquationToMatrixError(Exception):
24
29
  """Error while processing equations."""
25
30
 
@@ -13,6 +13,7 @@ from skfolio.measures._enums import (
13
13
  from skfolio.measures._measures import (
14
14
  average_drawdown,
15
15
  cdar,
16
+ correlation,
16
17
  cvar,
17
18
  drawdown_at_risk,
18
19
  edar,
@@ -49,6 +50,7 @@ __all__ = [
49
50
  "RiskMeasure",
50
51
  "average_drawdown",
51
52
  "cdar",
53
+ "correlation",
52
54
  "cvar",
53
55
  "drawdown_at_risk",
54
56
  "edar",