skfolio 0.0.1__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 (79) hide show
  1. skfolio/__init__.py +29 -0
  2. skfolio/cluster/__init__.py +8 -0
  3. skfolio/cluster/_hierarchical.py +387 -0
  4. skfolio/datasets/__init__.py +20 -0
  5. skfolio/datasets/_base.py +389 -0
  6. skfolio/datasets/data/__init__.py +0 -0
  7. skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  8. skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  9. skfolio/datasets/data/sp500_index.csv.gz +0 -0
  10. skfolio/distance/__init__.py +26 -0
  11. skfolio/distance/_base.py +55 -0
  12. skfolio/distance/_distance.py +574 -0
  13. skfolio/exceptions.py +30 -0
  14. skfolio/measures/__init__.py +76 -0
  15. skfolio/measures/_enums.py +355 -0
  16. skfolio/measures/_measures.py +607 -0
  17. skfolio/metrics/__init__.py +3 -0
  18. skfolio/metrics/_scorer.py +121 -0
  19. skfolio/model_selection/__init__.py +18 -0
  20. skfolio/model_selection/_combinatorial.py +407 -0
  21. skfolio/model_selection/_validation.py +194 -0
  22. skfolio/model_selection/_walk_forward.py +221 -0
  23. skfolio/moments/__init__.py +41 -0
  24. skfolio/moments/covariance/__init__.py +29 -0
  25. skfolio/moments/covariance/_base.py +101 -0
  26. skfolio/moments/covariance/_covariance.py +1108 -0
  27. skfolio/moments/expected_returns/__init__.py +21 -0
  28. skfolio/moments/expected_returns/_base.py +31 -0
  29. skfolio/moments/expected_returns/_expected_returns.py +415 -0
  30. skfolio/optimization/__init__.py +36 -0
  31. skfolio/optimization/_base.py +147 -0
  32. skfolio/optimization/cluster/__init__.py +13 -0
  33. skfolio/optimization/cluster/_nco.py +348 -0
  34. skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
  35. skfolio/optimization/cluster/hierarchical/_base.py +440 -0
  36. skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
  37. skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
  38. skfolio/optimization/convex/__init__.py +16 -0
  39. skfolio/optimization/convex/_base.py +1944 -0
  40. skfolio/optimization/convex/_distributionally_robust.py +392 -0
  41. skfolio/optimization/convex/_maximum_diversification.py +417 -0
  42. skfolio/optimization/convex/_mean_risk.py +974 -0
  43. skfolio/optimization/convex/_risk_budgeting.py +560 -0
  44. skfolio/optimization/ensemble/__init__.py +6 -0
  45. skfolio/optimization/ensemble/_base.py +87 -0
  46. skfolio/optimization/ensemble/_stacking.py +326 -0
  47. skfolio/optimization/naive/__init__.py +3 -0
  48. skfolio/optimization/naive/_naive.py +173 -0
  49. skfolio/population/__init__.py +3 -0
  50. skfolio/population/_population.py +883 -0
  51. skfolio/portfolio/__init__.py +13 -0
  52. skfolio/portfolio/_base.py +1096 -0
  53. skfolio/portfolio/_multi_period_portfolio.py +610 -0
  54. skfolio/portfolio/_portfolio.py +842 -0
  55. skfolio/pre_selection/__init__.py +7 -0
  56. skfolio/pre_selection/_pre_selection.py +342 -0
  57. skfolio/preprocessing/__init__.py +3 -0
  58. skfolio/preprocessing/_returns.py +114 -0
  59. skfolio/prior/__init__.py +18 -0
  60. skfolio/prior/_base.py +63 -0
  61. skfolio/prior/_black_litterman.py +238 -0
  62. skfolio/prior/_empirical.py +163 -0
  63. skfolio/prior/_factor_model.py +268 -0
  64. skfolio/typing.py +50 -0
  65. skfolio/uncertainty_set/__init__.py +23 -0
  66. skfolio/uncertainty_set/_base.py +108 -0
  67. skfolio/uncertainty_set/_bootstrap.py +281 -0
  68. skfolio/uncertainty_set/_empirical.py +237 -0
  69. skfolio/utils/__init__.py +0 -0
  70. skfolio/utils/bootstrap.py +115 -0
  71. skfolio/utils/equations.py +350 -0
  72. skfolio/utils/sorting.py +117 -0
  73. skfolio/utils/stats.py +466 -0
  74. skfolio/utils/tools.py +567 -0
  75. skfolio-0.0.1.dist-info/LICENSE +29 -0
  76. skfolio-0.0.1.dist-info/METADATA +568 -0
  77. skfolio-0.0.1.dist-info/RECORD +79 -0
  78. skfolio-0.0.1.dist-info/WHEEL +5 -0
  79. skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ """Multi Period Portfolio module.
2
+ `MultiPeriodPortfolio` is returned by the `predict` method of Optimization estimators.
3
+ `MultiPeriodPortfolio` is a list of `Portfolio`.
4
+ """
5
+
6
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
7
+ # License: BSD 3 clause
8
+
9
+ import numbers
10
+ from collections.abc import Iterator
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ import skfolio.typing as skt
16
+ from skfolio.portfolio._base import BasePortfolio
17
+ from skfolio.portfolio._portfolio import Portfolio
18
+ from skfolio.utils.tools import deduplicate_names
19
+
20
+ pd.options.plotting.backend = "plotly"
21
+
22
+
23
+ class MultiPeriodPortfolio(BasePortfolio):
24
+ r"""Multi-Period Portfolio class.
25
+
26
+ A Multi-Period Portfolio is composed of a list of :class:`Portfolio`.
27
+
28
+ Parameters
29
+ ----------
30
+ portfolios : list[Portfolio], optional
31
+ A list of :class:`Portfolio`. The default (`None`) is to initialize with an
32
+ empty list.
33
+
34
+ name : str, optional
35
+ Name of the multi-period portfolio.
36
+ The default (`None`) is to use the object id.
37
+
38
+ tag : str, optional
39
+ Tag given to the multi-period portfolio.
40
+ Tags are used to manipulate groups of portfolios from a `Population`.
41
+
42
+ fitness_measures : list[measures], optional
43
+ List of fitness measures.
44
+ Fitness measures are used to compute the portfolio fitness which is used to
45
+ compute domination.
46
+ The default (`None`) is to use the list [PerfMeasure.MEAN, RiskMeasure.VARIANCE]
47
+
48
+ annualized_factor : float, default=255.0
49
+ Factor used to annualize the below measures using the square-root rule:
50
+
51
+ * Annualized Mean = Mean * factor
52
+ * Annualized Variance = Variance * factor
53
+ * Annualized Semi-Variance = Semi-Variance * factor
54
+ * Annualized Standard-Deviation = Standard-Deviation * sqrt(factor)
55
+ * Annualized Semi-Deviation = Semi-Deviation * sqrt(factor)
56
+ * Annualized Sharpe Ratio = Sharpe Ratio * sqrt(factor)
57
+ * Annualized Sortino Ratio = Sortino Ratio * sqrt(factor)
58
+
59
+
60
+ risk_free_rate : float, default=0.0
61
+ Risk-free rate. The default value is `0.0`.
62
+
63
+ compounded : bool, default=False
64
+ If this is set to True, cumulative returns are compounded.
65
+ The default is `False`.
66
+
67
+ min_acceptable_return : float, optional
68
+ The minimum acceptable return used to distinguish "downside" and "upside"
69
+ returns for the computation of lower partial moments:
70
+
71
+ * First Lower Partial Moment
72
+ * Semi-Variance
73
+ * Semi-Deviation
74
+
75
+ The default (`None`) is to use the mean.
76
+
77
+ value_at_risk_beta : float, default=0.95
78
+ The confidence level of the portfolio VaR (Value At Risk) which represents
79
+ the return on the worst (1-beta)% observations.
80
+ The default value is `0.95`.
81
+
82
+ entropic_risk_measure_theta : float, default=1.0
83
+ The risk aversion level of the portfolio Entropic Risk Measure.
84
+ The default value is `1.0`.
85
+
86
+ entropic_risk_measure_beta : float, default=0.95
87
+ The confidence level of the portfolio Entropic Risk Measure.
88
+ The default value is `0.95`.
89
+
90
+ cvar_beta : float, default=0.95
91
+ The confidence level of the portfolio CVaR (Conditional Value at Risk) which
92
+ represents the expected VaR on the worst (1-beta)% observations.
93
+ The default value is `0.95`.
94
+
95
+ evar_beta : float, default=0.95
96
+ The confidence level of the portfolio EVaR (Entropic Value at Risk).
97
+ The default value is `0.95`.
98
+
99
+ drawdown_at_risk_beta : float, default=0.95
100
+ The confidence level of the portfolio Drawdown at Risk (DaR) which represents
101
+ the drawdown on the worst (1-beta)% observations.
102
+ The default value is `0.95`.
103
+
104
+ cdar_beta : float, default=0.95
105
+ The confidence level of the portfolio CDaR (Conditional Drawdown at Risk) which
106
+ represents the expected drawdown on the worst (1-beta)% observations.
107
+ The default value is `0.95`.
108
+
109
+ edar_beta : float, default=0.95
110
+ The confidence level of the portfolio EDaR (Entropic Drawdown at Risk).
111
+ The default value is `0.95`.
112
+
113
+ check_observations_order : bool, default=False
114
+ If this is set to True, and if the list of portfolios is not chronologically
115
+ sorted, an error is raised. The chronological order is determined by comparing
116
+ the first and last observations of each portfolio.
117
+ The default is `False`.
118
+
119
+ Attributes
120
+ ----------
121
+ n_observations : float
122
+ Number of observations.
123
+
124
+ mean : float
125
+ Mean of the portfolio returns.
126
+
127
+ annualized_mean : float
128
+ Mean annualized by :math:`mean \times annualization\_factor`
129
+
130
+ mean_absolute_deviation : float
131
+ Mean Absolute Deviation. The deviation is the difference between the
132
+ return and a minimum acceptable return (`min_acceptable_return`).
133
+
134
+ first_lower_partial_moment : float
135
+ First Lower Partial Moment. The First Lower Partial Moment is the mean of the
136
+ returns below a minimum acceptable return (`min_acceptable_return`).
137
+
138
+ variance : float
139
+ Variance (Second Moment)
140
+
141
+ annualized_variance : float
142
+ Variance annualized by :math:`variance \times annualization\_factor`
143
+
144
+ semi_variance : float
145
+ Semi-variance (Second Lower Partial Moment).
146
+ The semi-variance is the variance of the returns below a minimum acceptable
147
+ return (`min_acceptable_return`).
148
+
149
+ annualized_semi_variance : float
150
+ Semi-variance annualized by
151
+ :math:`semi\_variance \times annualization\_factor`
152
+
153
+ standard_deviation : float
154
+ Standard Deviation (Square Root of the Second Moment).
155
+
156
+ annualized_standard_deviation : float
157
+ Standard Deviation annualized by
158
+ :math:`standard\_deviation \times \sqrt{annualization\_factor}`
159
+
160
+ semi_deviation : float
161
+ Semi-deviation (Square Root of the Second Lower Partial Moment).
162
+ The Semi Standard Deviation is the Standard Deviation of the returns below a
163
+ minimum acceptable return (`min_acceptable_return`).
164
+
165
+ annualized_semi_deviation : float
166
+ Semi-deviation annualized by
167
+ :math:`semi\_deviation \times \sqrt{annualization\_factor}`
168
+
169
+ skew : float
170
+ Skew. The Skew is a measure of the lopsidedness of the distribution.
171
+ A symmetric distribution have a Skew of zero.
172
+ Higher Skew corresponds to longer right tail.
173
+
174
+ kurtosis : float
175
+ Kurtosis. It is a measure of the heaviness of the tail of the distribution.
176
+ Higher Kurtosis corresponds to greater extremity of deviations (fat tails).
177
+
178
+ fourth_central_moment : float
179
+ Fourth Central Moment.
180
+
181
+ fourth_lower_partial_moment : float
182
+ Fourth Lower Partial Moment. It is a measure of the heaviness of the downside
183
+ tail of the returns below a minimum acceptable return (`min_acceptable_return`).
184
+ Higher Fourth Lower Partial Moment corresponds to greater extremity of downside
185
+ deviations (downside fat tail).
186
+
187
+ worst_realization : float
188
+ Worst Realization which is the worst return.
189
+
190
+ value_at_risk : float
191
+ Historical VaR (Value at Risk).
192
+ The VaR is the maximum loss at a given confidence level (`value_at_risk_beta`).
193
+
194
+ cvar : float
195
+ Historical CVaR (Conditional Value at Risk). The CVaR (or Tail VaR) represents
196
+ the mean shortfall at a specified confidence level (`cvar_beta`).
197
+
198
+ entropic_risk_measure : float
199
+ Historical Entropic Risk Measure. It is a risk measure which depends on the
200
+ risk aversion defined by the investor (`entropic_risk_measure_theta`) through
201
+ the exponential utility function at a given confidence level
202
+ (`entropic_risk_measure_beta`).
203
+
204
+ evar : float
205
+ Historical EVaR (Entropic Value at Risk). It is a coherent risk measure which
206
+ is an upper bound for the VaR and the CVaR, obtained from the Chernoff
207
+ inequality at a given confidence level (`evar_beta`). The EVaR can be
208
+ represented by using the concept of relative entropy.
209
+
210
+ drawdown_at_risk : float
211
+ Historical Drawdown at Risk. It is the maximum drawdown at a given
212
+ confidence level (`drawdown_at_risk_beta`).
213
+
214
+ cdar : float
215
+ Historical CDaR (Conditional Drawdown at Risk) at a given confidence level
216
+ (`cdar_beta`).
217
+
218
+ max_drawdown : float
219
+ Maximum Drawdown.
220
+
221
+ average_drawdown : float
222
+ Average Drawdown.
223
+
224
+ edar : float
225
+ EDaR (Entropic Drawdown at Risk). It is a coherent risk measure which is an
226
+ upper bound for the Drawdown at Risk and the CDaR, obtained from the Chernoff
227
+ inequality at a given confidence level (`edar_beta`). The EDaR can be
228
+ represented by using the concept of relative entropy.
229
+
230
+ ulcer_index : float
231
+ Ulcer Index
232
+
233
+ gini_mean_difference : float
234
+ Gini Mean Difference (GMD). It is the expected absolute difference between two
235
+ realizations. The GMD is a superior measure of variability for non-normal
236
+ distribution than the variance. It can be used to form necessary conditions
237
+ for second-degree stochastic dominance, while the variance cannot.
238
+
239
+ mean_absolute_deviation_ratio : float
240
+ Mean Absolute Deviation ratio.
241
+ It is the excess mean (mean - risk_free_rate) divided by the MaD.
242
+
243
+ first_lower_partial_moment_ratio : float
244
+ First Lower Partial Moment ratio.
245
+ It is the excess mean (mean - risk_free_rate) divided by the First Lower
246
+ Partial Moment.
247
+
248
+ sharpe_ratio : float
249
+ Sharpe ratio.
250
+ It is the excess mean (mean - risk_free_rate) divided by the standard-deviation.
251
+
252
+ annualized_sharpe_ratio : float
253
+ Sharpe ratio annualized by
254
+ :math:`sharpe\_ratio \times \sqrt{annualization\_factor}`.
255
+
256
+ sortino_ratio : float
257
+ Sortino ratio.
258
+ It is the excess mean (mean - risk_free_rate) divided by the semi
259
+ standard-deviation.
260
+
261
+ annualized_sortino_ratio : float
262
+ Sortino ratio annualized by
263
+ :math:`sortino\_ratio \times \sqrt{annualization\_factor}`.
264
+
265
+ value_at_risk_ratio : float
266
+ VaR ratio.
267
+ It is the excess mean (mean - risk_free_rate) divided by the Value at Risk
268
+ (VaR).
269
+
270
+ cvar_ratio : float
271
+ CVaR ratio.
272
+ It is the excess mean (mean - risk_free_rate) divided by the Conditional Value
273
+ at Risk (CVaR).
274
+
275
+ entropic_risk_measure_ratio : float
276
+ Entropic risk measure ratio.
277
+ It is the excess mean (mean - risk_free_rate) divided by the Entropic risk
278
+ measure.
279
+
280
+ evar_ratio : float
281
+ EVaR ratio.
282
+ It is the excess mean (mean - risk_free_rate) divided by the EVaR (Entropic
283
+ Value at Risk).
284
+
285
+ worst_realization_ratio : float
286
+ Worst Realization ratio.
287
+ It is the excess mean (mean - risk_free_rate) divided by the Worst Realization
288
+ (worst return).
289
+
290
+ drawdown_at_risk_ratio : float
291
+ Drawdown at Risk ratio.
292
+ It is the excess mean (mean - risk_free_rate) divided by the drawdown at
293
+ risk.
294
+
295
+ cdar_ratio : float
296
+ CDaR ratio.
297
+ It is the excess mean (mean - risk_free_rate) divided by the CDaR (conditional
298
+ drawdown at risk).
299
+
300
+ calmar_ratio : float
301
+ Calmar ratio.
302
+ It is the excess mean (mean - risk_free_rate) divided by the Maximum Drawdown.
303
+
304
+ average_drawdown_ratio : float
305
+ Average Drawdown ratio.
306
+ It is the excess mean (mean - risk_free_rate) divided by the Average Drawdown.
307
+
308
+ edar_ratio : float
309
+ EDaR ratio.
310
+ It is the excess mean (mean - risk_free_rate) divided by the EDaR (Entropic
311
+ Drawdown at Risk).
312
+
313
+ ulcer_index_ratio : float
314
+ Ulcer Index ratio.
315
+ It is the excess mean (mean - risk_free_rate) divided by the Ulcer Index.
316
+
317
+ gini_mean_difference_ratio : float
318
+ Gini Mean Difference ratio.
319
+ It is the excess mean (mean - risk_free_rate) divided by the Gini Mean
320
+ Difference.
321
+ """
322
+
323
+ __slots__ = {
324
+ # read-only
325
+ "_portfolios",
326
+ "check_observations_order",
327
+ }
328
+
329
+ def __init__(
330
+ self,
331
+ portfolios: list[Portfolio] | None = None,
332
+ name: str | None = None,
333
+ tag: str | None = None,
334
+ risk_free_rate: float = 0,
335
+ annualized_factor: float = 255.0,
336
+ fitness_measures: list[skt.Measure] | None = None,
337
+ compounded: bool = False,
338
+ min_acceptable_return: float | None = None,
339
+ value_at_risk_beta: float = 0.95,
340
+ entropic_risk_measure_theta: float = 1,
341
+ entropic_risk_measure_beta: float = 0.95,
342
+ cvar_beta: float = 0.95,
343
+ evar_beta: float = 0.95,
344
+ drawdown_at_risk_beta: float = 0.95,
345
+ cdar_beta: float = 0.95,
346
+ edar_beta: float = 0.95,
347
+ check_observations_order: bool = False,
348
+ ):
349
+ super().__init__(
350
+ returns=np.array([]),
351
+ observations=np.array([]),
352
+ name=name,
353
+ tag=tag,
354
+ risk_free_rate=risk_free_rate,
355
+ annualized_factor=annualized_factor,
356
+ fitness_measures=fitness_measures,
357
+ compounded=compounded,
358
+ min_acceptable_return=min_acceptable_return,
359
+ value_at_risk_beta=value_at_risk_beta,
360
+ cvar_beta=cvar_beta,
361
+ entropic_risk_measure_theta=entropic_risk_measure_theta,
362
+ entropic_risk_measure_beta=entropic_risk_measure_beta,
363
+ evar_beta=evar_beta,
364
+ drawdown_at_risk_beta=drawdown_at_risk_beta,
365
+ cdar_beta=cdar_beta,
366
+ edar_beta=edar_beta,
367
+ )
368
+ self.check_observations_order = check_observations_order
369
+ self._set_portfolios(portfolios=portfolios)
370
+
371
+ def __getitem__(self, key: int | slice) -> Portfolio | list[Portfolio]:
372
+ return self._portfolios[key]
373
+
374
+ def __setitem__(self, key: int, value: Portfolio) -> None:
375
+ if not isinstance(value, Portfolio):
376
+ raise TypeError(f"Cannot set a value with type {type(value)}")
377
+ new_portfolios = self._portfolios.copy()
378
+ new_portfolios[key] = value
379
+ self._set_portfolios(portfolios=new_portfolios)
380
+ self.clear()
381
+
382
+ def __delitem__(self, key: int) -> None:
383
+ new_portfolios = self._portfolios.copy()
384
+ del new_portfolios[key]
385
+ self._set_portfolios(portfolios=new_portfolios)
386
+ self.clear()
387
+
388
+ def __iter__(self) -> Iterator[Portfolio]:
389
+ return iter(self._portfolios)
390
+
391
+ def __contains__(self, value: Portfolio) -> bool:
392
+ if not isinstance(value, Portfolio):
393
+ return False
394
+ return value in self._portfolios
395
+
396
+ def __neg__(self):
397
+ return self.__class__(
398
+ portfolios=[-p for p in self],
399
+ tag=self.tag,
400
+ fitness_measures=self.fitness_measures,
401
+ )
402
+
403
+ def __abs__(self):
404
+ return self.__class__(
405
+ portfolios=[abs(p) for p in self],
406
+ tag=self.tag,
407
+ fitness_measures=self.fitness_measures,
408
+ )
409
+
410
+ def __round__(self, n: int):
411
+ return self.__class__(
412
+ portfolios=[p.__round__(n) for p in self],
413
+ tag=self.tag,
414
+ fitness_measures=self.fitness_measures,
415
+ )
416
+
417
+ def __floor__(self):
418
+ return self.__class__(
419
+ portfolios=[np.floor(p) for p in self],
420
+ tag=self.tag,
421
+ fitness_measures=self.fitness_measures,
422
+ )
423
+
424
+ def __trunc__(self):
425
+ return self.__class__(
426
+ portfolios=[np.trunc(p) for p in self],
427
+ tag=self.tag,
428
+ fitness_measures=self.fitness_measures,
429
+ )
430
+
431
+ def __add__(self, other):
432
+ if not isinstance(other, self.__class__):
433
+ raise TypeError(
434
+ "Cannot add a MultiPeriodPortfolio with an object of type"
435
+ f" {type(other)}"
436
+ )
437
+ if len(self) != len(other):
438
+ raise TypeError("Cannot add two MultiPeriodPortfolio of different sizes")
439
+ return self.__class__(
440
+ portfolios=[p1 + p2 for p1, p2 in zip(self, other, strict=True)],
441
+ tag=self.tag,
442
+ fitness_measures=self.fitness_measures,
443
+ )
444
+
445
+ def __sub__(self, other):
446
+ if not isinstance(other, self.__class__):
447
+ raise TypeError(
448
+ "Cannot subtract a MultiPeriodPortfolio with an object of type"
449
+ f" {type(other)}"
450
+ )
451
+ if len(self) != len(other):
452
+ raise TypeError(
453
+ "Cannot subtract two MultiPeriodPortfolio of different sizes"
454
+ )
455
+ return self.__class__(
456
+ portfolios=[p1 - p2 for p1, p2 in zip(self, other, strict=True)],
457
+ tag=self.tag,
458
+ fitness_measures=self.fitness_measures,
459
+ )
460
+
461
+ def __mul__(self, other: numbers.Number | list[numbers.Number] | np.ndarray):
462
+ if np.isscalar(other):
463
+ portfolios = [p * other for p in self]
464
+ else:
465
+ portfolios = [p * a for p, a in zip(self, other, strict=True)]
466
+ return self.__class__(
467
+ portfolios=portfolios, tag=self.tag, fitness_measures=self.fitness_measures
468
+ )
469
+
470
+ __rmul__ = __mul__
471
+
472
+ def __floordiv__(self, other: numbers.Number | list[numbers.Number] | np.ndarray):
473
+ if np.isscalar(other):
474
+ portfolios = [p // other for p in self]
475
+ else:
476
+ portfolios = [p // a for p, a in zip(self, other, strict=True)]
477
+ return self.__class__(
478
+ portfolios=portfolios, tag=self.tag, fitness_measures=self.fitness_measures
479
+ )
480
+
481
+ def __truediv__(self, other: numbers.Number | list[numbers.Number] | np.ndarray):
482
+ if np.isscalar(other):
483
+ portfolios = [p / other for p in self]
484
+ else:
485
+ portfolios = [p / a for p, a in zip(self, other, strict=True)]
486
+ return self.__class__(
487
+ portfolios=portfolios, tag=self.tag, fitness_measures=self.fitness_measures
488
+ )
489
+
490
+ # Private method
491
+ def _set_portfolios(self, portfolios: list[Portfolio] | None = None) -> None:
492
+ """Set the returns, observations and portfolios list.
493
+
494
+ Parameters
495
+ ----------
496
+ portfolios : list[Portfolio], optional
497
+ The list of Portfolios. The default (`None`) is to use an empty list.
498
+ """
499
+ returns = []
500
+ observations = []
501
+ if portfolios is None:
502
+ portfolios = []
503
+ if len(portfolios) != 0:
504
+ for item in portfolios:
505
+ if not isinstance(item, BasePortfolio | Portfolio):
506
+ raise TypeError(
507
+ "`portfolios` items must be of type `Portfolio`, got"
508
+ f" {type(item).__name__}"
509
+ )
510
+ returns.append(item.returns)
511
+ observations.append(item.observations)
512
+ returns = np.concatenate(returns)
513
+ observations = np.concatenate(observations)
514
+ if self.check_observations_order:
515
+ iteration = iter(portfolios)
516
+ prev_p = next(iteration)
517
+ while (p := next(iteration, None)) is not None:
518
+ if p.observations[0] <= prev_p.observations[-1]:
519
+ raise ValueError(
520
+ "Portfolios observations should not overlap:"
521
+ f" {p} overlapping {prev_p}"
522
+ )
523
+ prev_p = p
524
+ self._loaded = False
525
+ self._portfolios = portfolios
526
+ self.returns = np.asarray(returns)
527
+ self.observations = np.asarray(observations)
528
+ self._loaded = True
529
+
530
+ # Custom attribute setter and getter
531
+ @property
532
+ def portfolios(self) -> list[Portfolio]:
533
+ """List of portfolios composing the mutli-period portfolio."""
534
+ return self._portfolios
535
+
536
+ @portfolios.setter
537
+ def portfolios(self, value: list[Portfolio] | None = None):
538
+ """Set the list of Portfolios and clear the attributes cache linked to the
539
+ list of portfolios."""
540
+ self._set_portfolios(portfolios=value)
541
+ self.clear()
542
+
543
+ # Classic property
544
+ @property
545
+ def assets(self) -> list:
546
+ """List of assets names in each Portfolio."""
547
+ return [p.assets for p in self]
548
+
549
+ @property
550
+ def composition(self) -> pd.DataFrame:
551
+ """DataFrame of the Portfolio composition."""
552
+ df = pd.concat([p.composition for p in self], axis=1)
553
+ df.fillna(0, inplace=True)
554
+ df.columns = deduplicate_names(df.columns)
555
+ return df
556
+
557
+ def summary(self, formatted: bool = True) -> pd.Series:
558
+ """Portfolio summary of all its measures.
559
+
560
+ Parameters
561
+ ----------
562
+ formatted : bool, default=True
563
+ If this is set to True, the measures are formatted into rounded string with
564
+ units.
565
+
566
+ Returns
567
+ -------
568
+ summary : series
569
+ Portfolio summary of all its measures.
570
+ """
571
+ df = super().summary(formatted=formatted)
572
+ portfolios_number = len(self)
573
+ avg_assets_per_portfolio = np.mean([len(p) for p in self])
574
+ if formatted:
575
+ portfolios_number = str(int(portfolios_number))
576
+ avg_assets_per_portfolio = f"{avg_assets_per_portfolio:0.1f}"
577
+ df["Portfolios number"] = portfolios_number
578
+ df["Avg nb of assets per portfolio"] = avg_assets_per_portfolio
579
+ return df
580
+
581
+ # Public methods
582
+ def append(self, portfolio: Portfolio) -> None:
583
+ """Append a Portfolio to the Portfolio list.
584
+
585
+ Parameters
586
+ ----------
587
+ portfolio : Portfolio
588
+ The Portfolio to append.
589
+ """
590
+ if self.check_observations_order and len(self) != 0:
591
+ start_date = portfolio.observations[0]
592
+ prev_last_date = self[-1].observations[-1]
593
+ if start_date < prev_last_date:
594
+ raise ValueError(
595
+ f"Portfolios observations should not overlap: {prev_last_date} ->"
596
+ f" {start_date} "
597
+ )
598
+ self._loaded = False
599
+ self._portfolios.append(portfolio)
600
+ if len(self.observations) == 0:
601
+ # We don"t concatenate an empty array as we cannot know the dtype before.
602
+ self.observations = portfolio.observations
603
+ self.returns = portfolio.returns
604
+ else:
605
+ self.observations = np.concatenate(
606
+ [self.observations, portfolio.observations], axis=0
607
+ )
608
+ self.returns = np.concatenate([self.returns, portfolio.returns], axis=0)
609
+ self._loaded = True
610
+ self.clear()