skfolio 0.9.1__py3-none-any.whl → 0.10.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 (41) hide show
  1. skfolio/distribution/multivariate/_vine_copula.py +35 -34
  2. skfolio/distribution/univariate/_base.py +20 -15
  3. skfolio/exceptions.py +5 -0
  4. skfolio/measures/__init__.py +2 -0
  5. skfolio/measures/_measures.py +390 -155
  6. skfolio/optimization/_base.py +21 -4
  7. skfolio/optimization/cluster/hierarchical/_base.py +16 -13
  8. skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
  9. skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
  10. skfolio/optimization/convex/_base.py +238 -144
  11. skfolio/optimization/convex/_distributionally_robust.py +32 -20
  12. skfolio/optimization/convex/_maximum_diversification.py +15 -15
  13. skfolio/optimization/convex/_mean_risk.py +26 -24
  14. skfolio/optimization/convex/_risk_budgeting.py +23 -21
  15. skfolio/optimization/ensemble/__init__.py +2 -4
  16. skfolio/optimization/ensemble/_stacking.py +1 -1
  17. skfolio/optimization/naive/_naive.py +2 -2
  18. skfolio/population/_population.py +30 -9
  19. skfolio/portfolio/_base.py +68 -26
  20. skfolio/portfolio/_multi_period_portfolio.py +5 -0
  21. skfolio/portfolio/_portfolio.py +5 -0
  22. skfolio/prior/__init__.py +6 -2
  23. skfolio/prior/_base.py +7 -3
  24. skfolio/prior/_black_litterman.py +14 -12
  25. skfolio/prior/_empirical.py +8 -7
  26. skfolio/prior/_entropy_pooling.py +1493 -0
  27. skfolio/prior/_factor_model.py +39 -22
  28. skfolio/prior/_opinion_pooling.py +475 -0
  29. skfolio/prior/_synthetic_data.py +10 -8
  30. skfolio/uncertainty_set/_bootstrap.py +4 -4
  31. skfolio/uncertainty_set/_empirical.py +6 -6
  32. skfolio/utils/equations.py +10 -4
  33. skfolio/utils/figure.py +185 -0
  34. skfolio/utils/tools.py +4 -2
  35. {skfolio-0.9.1.dist-info → skfolio-0.10.0.dist-info}/METADATA +94 -5
  36. {skfolio-0.9.1.dist-info → skfolio-0.10.0.dist-info}/RECORD +40 -38
  37. {skfolio-0.9.1.dist-info → skfolio-0.10.0.dist-info}/WHEEL +1 -1
  38. skfolio/synthetic_returns/__init__.py +0 -1
  39. /skfolio/{optimization/ensemble/_base.py → utils/composition.py} +0 -0
  40. {skfolio-0.9.1.dist-info → skfolio-0.10.0.dist-info}/licenses/LICENSE +0 -0
  41. {skfolio-0.9.1.dist-info → skfolio-0.10.0.dist-info}/top_level.txt +0 -0
@@ -7,53 +7,74 @@
7
7
  # from Riskfolio-Lib, Copyright (c) 2020-2023, Dany Cajas, Licensed under BSD 3 clause.
8
8
 
9
9
  import numpy as np
10
+ import numpy.typing as npt
10
11
  import scipy.optimize as sco
11
12
 
12
13
 
13
- def mean(returns: np.ndarray) -> float:
14
+ def mean(
15
+ returns: npt.ArrayLike, sample_weight: np.ndarray | None = None
16
+ ) -> float | np.ndarray:
14
17
  """Compute the mean.
15
18
 
16
19
  Parameters
17
20
  ----------
18
- returns : ndarray of shape (n_observations,)
19
- Vector of returns.
21
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
22
+ Array of return values.
23
+
24
+ sample_weight : ndarray of shape (n_observations,), optional
25
+ Sample weights for each observation. If None, equal weights are assumed.
20
26
 
21
27
  Returns
22
28
  -------
23
- value : float
24
- Mean
29
+ value : float or ndarray of shape (n_assets,)
30
+ The computed mean.
31
+ If `returns` is a 1D-array, the result is a float.
32
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
25
33
  """
26
- return returns.mean()
34
+ if sample_weight is None:
35
+ return np.mean(returns, axis=0)
36
+ return sample_weight @ returns
27
37
 
28
38
 
29
39
  def mean_absolute_deviation(
30
- returns: np.ndarray, min_acceptable_return: float | None = None
31
- ) -> float:
40
+ returns: npt.ArrayLike,
41
+ min_acceptable_return: float | np.ndarray | None = None,
42
+ sample_weight: np.ndarray | None = None,
43
+ ) -> float | np.ndarray:
32
44
  """Compute the mean absolute deviation (MAD).
33
45
 
34
46
  Parameters
35
47
  ----------
36
- returns : ndarray of shape (n_observations,)
37
- Vector of returns.
48
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
49
+ Array of return values.
38
50
 
39
- min_acceptable_return : float, optional
51
+ min_acceptable_return : float or ndarray of shape (n_assets,) optional
40
52
  Minimum acceptable return. It is the return target to distinguish "downside" and
41
- "upside" returns.
42
- The default (`None`) is to use the returns' mean.
53
+ "upside" returns. The default (`None`) is to use the returns' mean.
54
+
55
+ sample_weight : ndarray of shape (n_observations,), optional
56
+ Sample weights for each observation. If None, equal weights are assumed.
43
57
 
44
58
  Returns
45
59
  -------
46
- value : float
60
+ value : float or ndarray of shape (n_assets,)
47
61
  Mean absolute deviation.
62
+ If `returns` is a 1D-array, the result is a float.
63
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
48
64
  """
49
65
  if min_acceptable_return is None:
50
- min_acceptable_return = np.mean(returns, axis=0)
51
- return float(np.mean(np.abs(returns - min_acceptable_return)))
66
+ min_acceptable_return = mean(returns, sample_weight=sample_weight)
67
+
68
+ absolute_deviations = np.abs(returns - min_acceptable_return)
69
+
70
+ return mean(absolute_deviations, sample_weight=sample_weight)
52
71
 
53
72
 
54
73
  def first_lower_partial_moment(
55
- returns: np.ndarray, min_acceptable_return: float | None = None
56
- ) -> float:
74
+ returns: npt.ArrayLike,
75
+ min_acceptable_return: float | np.ndarray | None = None,
76
+ sample_weight: np.ndarray | None = None,
77
+ ) -> float | np.ndarray:
57
78
  """Compute the first lower partial moment.
58
79
 
59
80
  The first lower partial moment is the mean of the returns below a minimum
@@ -61,128 +82,216 @@ def first_lower_partial_moment(
61
82
 
62
83
  Parameters
63
84
  ----------
64
- returns : ndarray of shape (n_observations,)
65
- Vector of returns
85
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
86
+ Array of return values.
66
87
 
67
- min_acceptable_return : float, optional
88
+ min_acceptable_return : float or ndarray of shape (n_assets,) optional
68
89
  Minimum acceptable return. It is the return target to distinguish "downside" and
69
- "upside" returns.
70
- The default (`None`) is to use the mean.
90
+ "upside" returns. The default (`None`) is to use the returns' mean.
91
+
92
+ sample_weight : ndarray of shape (n_observations,), optional
93
+ Sample weights for each observation. If None, equal weights are assumed.
71
94
 
72
95
  Returns
73
96
  -------
74
- value : float
97
+ value : float or ndarray of shape (n_assets,)
75
98
  First lower partial moment.
99
+ If `returns` is a 1D-array, the result is a float.
100
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
76
101
  """
77
102
  if min_acceptable_return is None:
78
- min_acceptable_return = np.mean(returns, axis=0)
79
- return -np.sum(np.minimum(0, returns - min_acceptable_return)) / len(returns)
103
+ min_acceptable_return = mean(returns, sample_weight=sample_weight)
104
+
105
+ deviations = np.maximum(0, min_acceptable_return - returns)
106
+
107
+ return mean(deviations, sample_weight=sample_weight)
80
108
 
81
109
 
82
- def variance(returns: np.ndarray) -> float:
110
+ def variance(
111
+ returns: npt.ArrayLike,
112
+ biased: bool = False,
113
+ sample_weight: np.ndarray | None = None,
114
+ ) -> float | np.ndarray:
83
115
  """Compute the variance (second moment).
84
116
 
85
117
  Parameters
86
118
  ----------
87
- returns : ndarray of shape (n_observations,)
88
- Vector of returns.
119
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
120
+ Array of return values.
121
+
122
+ biased : bool, default=False
123
+ If False (default), computes the sample variance (unbiased); otherwise,
124
+ computes the population variance (biased).
125
+
126
+ sample_weight : ndarray of shape (n_observations,), optional
127
+ Sample weights for each observation. If None, equal weights are assumed.
89
128
 
90
129
  Returns
91
130
  -------
92
- value : float
93
- Variance.
131
+ value : float or ndarray of shape (n_assets,)
132
+ Variance.
133
+ If `returns` is a 1D-array, the result is a float.
134
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
94
135
  """
95
- return returns.var(ddof=1)
136
+ if sample_weight is None:
137
+ return np.var(returns, ddof=0 if biased else 1, axis=0)
138
+
139
+ biased_var = sample_weight @ (returns - mean(returns)) ** 2
140
+ if biased:
141
+ return biased_var
142
+ n_eff = 1 / np.sum(sample_weight**2)
143
+ return biased_var * n_eff / (n_eff - 1)
96
144
 
97
145
 
98
146
  def semi_variance(
99
- returns: np.ndarray, min_acceptable_return: float | None = None
100
- ) -> float:
147
+ returns: npt.ArrayLike,
148
+ min_acceptable_return: float | np.ndarray | None = None,
149
+ sample_weight: np.ndarray | None = None,
150
+ biased: bool = False,
151
+ ) -> float | np.ndarray:
101
152
  """Compute the semi-variance (second lower partial moment).
102
153
 
103
154
  The semi-variance is the variance of the returns below a minimum acceptable return.
104
155
 
105
156
  Parameters
106
157
  ----------
107
- returns : ndarray of shape (n_observations,)
108
- Vector of returns
158
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
159
+ Array of return values.
109
160
 
110
- min_acceptable_return : float, optional
161
+ min_acceptable_return : float or ndarray of shape (n_assets,) optional
111
162
  Minimum acceptable return. It is the return target to distinguish "downside" and
112
- "upside" returns.
113
- The default (`None`) is to use the mean.
163
+ "upside" returns. The default (`None`) is to use the returns' mean.
164
+
165
+ biased : bool, default=False
166
+ If False (default), computes the sample semi-variance (unbiased); otherwise,
167
+ computes the population semi-variance (biased).
168
+
169
+ sample_weight : ndarray of shape (n_observations,), optional
170
+ Sample weights for each observation. If None, equal weights are assumed.
114
171
 
115
172
  Returns
116
173
  -------
117
- value : float
174
+ value : float or ndarray of shape (n_assets,)
118
175
  Semi-variance.
176
+ If `returns` is a 1D-array, the result is a float.
177
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
119
178
  """
120
179
  if min_acceptable_return is None:
121
- min_acceptable_return = np.mean(returns, axis=0)
122
- return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 2)) / (
123
- len(returns) - 1
180
+ min_acceptable_return = mean(returns)
181
+
182
+ biased_semi_var = mean(
183
+ np.maximum(0, min_acceptable_return - returns) ** 2, sample_weight=sample_weight
124
184
  )
185
+ if biased:
186
+ return biased_semi_var
187
+
188
+ n_observations = len(returns)
189
+ if sample_weight is None:
190
+ correction = n_observations / (n_observations - 1)
191
+ else:
192
+ correction = 1.0 / (1.0 - np.sum(sample_weight**2))
193
+ return biased_semi_var * correction
125
194
 
126
195
 
127
- def standard_deviation(returns: np.ndarray) -> float:
196
+ def standard_deviation(
197
+ returns: npt.ArrayLike,
198
+ sample_weight: np.ndarray | None = None,
199
+ biased: bool = False,
200
+ ) -> float | np.ndarray:
128
201
  """Compute the standard-deviation (square root of the second moment).
129
202
 
130
203
  Parameters
131
204
  ----------
132
- returns : ndarray of shape (n_observations,)
133
- Vector of returns.
205
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
206
+ Array of return values.
207
+
208
+ biased : bool, default=False
209
+ If False (default), computes the sample standard-deviation (unbiased);
210
+ otherwise, computes the population standard-deviation (biased).
211
+
212
+ sample_weight : ndarray of shape (n_observations,), optional
213
+ Sample weights for each observation. If None, equal weights are assumed.
134
214
 
135
215
  Returns
136
216
  -------
137
- value : float
217
+ value : float or ndarray of shape (n_assets,)
138
218
  Standard-deviation.
219
+ If `returns` is a 1D-array, the result is a float.
220
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
139
221
  """
140
- return np.sqrt(variance(returns=returns))
222
+ return np.sqrt(variance(returns, sample_weight=sample_weight, biased=biased))
141
223
 
142
224
 
143
225
  def semi_deviation(
144
- returns: np.ndarray, min_acceptable_return: float | None = None
145
- ) -> float:
146
- """Compute the semi standard-deviation (semi-deviation) (square root of the second lower
147
- partial moment).
226
+ returns: npt.ArrayLike,
227
+ min_acceptable_return: float | np.ndarray | None = None,
228
+ sample_weight: np.ndarray | None = None,
229
+ biased: bool = False,
230
+ ) -> float | np.ndarray:
231
+ """Compute the semi deviation (square root of the second lower partial moment).
148
232
 
149
233
  Parameters
150
234
  ----------
151
- returns : ndarray of shape (n_observations,)
152
- Vector of returns.
235
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
236
+ Array of return values.
153
237
 
154
- min_acceptable_return : float, optional
238
+ min_acceptable_return : float or ndarray of shape (n_assets,) optional
155
239
  Minimum acceptable return. It is the return target to distinguish "downside" and
156
- "upside" returns.
157
- The default (`None`) is to use the returns mean.
240
+ "upside" returns. The default (`None`) is to use the returns' mean.
241
+
242
+ biased : bool, default=False
243
+ If False (default), computes the sample semi-deviation (unbiased); otherwise,
244
+ computes the population semi-seviation (biased).
245
+
246
+ sample_weight : ndarray of shape (n_observations,), optional
247
+ Sample weights for each observation. If None, equal weights are assumed.
158
248
 
159
249
  Returns
160
250
  -------
161
- value : float
162
- Semi-standard-deviation.
251
+ value : float or ndarray of shape (n_assets,)
252
+ Semi-deviation.
253
+ If `returns` is a 1D-array, the result is a float.
254
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
163
255
  """
164
256
  return np.sqrt(
165
- semi_variance(returns=returns, min_acceptable_return=min_acceptable_return)
257
+ semi_variance(
258
+ returns,
259
+ min_acceptable_return=min_acceptable_return,
260
+ biased=biased,
261
+ sample_weight=sample_weight,
262
+ )
166
263
  )
167
264
 
168
265
 
169
- def third_central_moment(returns: np.ndarray) -> float:
266
+ def third_central_moment(
267
+ returns: npt.ArrayLike, sample_weight: np.ndarray | None = None
268
+ ) -> float | np.ndarray:
170
269
  """Compute the third central moment.
171
270
 
172
271
  Parameters
173
272
  ----------
174
- returns : ndarray of shape (n_observations,)
175
- Vector of returns.
273
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
274
+ Array of return values.
275
+
276
+ sample_weight : ndarray of shape (n_observations,), optional
277
+ Sample weights for each observation. If None, equal weights are assumed.
176
278
 
177
279
  Returns
178
280
  -------
179
- value : float
281
+ value : float or ndarray of shape (n_assets,)
180
282
  Third central moment.
283
+ If `returns` is a 1D-array, the result is a float.
284
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
181
285
  """
182
- return np.sum(np.power(returns - np.mean(returns, axis=0), 3)) / len(returns)
286
+ return mean(
287
+ (returns - mean(returns, sample_weight=sample_weight)) ** 3,
288
+ sample_weight=sample_weight,
289
+ )
183
290
 
184
291
 
185
- def skew(returns: np.ndarray) -> float:
292
+ def skew(
293
+ returns: npt.ArrayLike, sample_weight: np.ndarray | None = None
294
+ ) -> float | np.ndarray:
186
295
  """Compute the Skew.
187
296
 
188
297
  The Skew is a measure of the lopsidedness of the distribution.
@@ -191,34 +300,54 @@ def skew(returns: np.ndarray) -> float:
191
300
 
192
301
  Parameters
193
302
  ----------
194
- returns : ndarray of shape (n_observations,)
195
- Vector of returns.
303
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
304
+ Array of return values.
305
+
306
+ sample_weight : ndarray of shape (n_observations,), optional
307
+ Sample weights for each observation. If None, equal weights are assumed.
196
308
 
197
309
  Returns
198
310
  -------
199
- value : float
311
+ value : float or ndarray of shape (n_assets,)
200
312
  Skew.
313
+ If `returns` is a 1D-array, the result is a float.
314
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
201
315
  """
202
- return third_central_moment(returns) / standard_deviation(returns) ** 3
316
+ return (
317
+ third_central_moment(returns, sample_weight)
318
+ / variance(returns, sample_weight=sample_weight, biased=True) ** 1.5
319
+ )
203
320
 
204
321
 
205
- def fourth_central_moment(returns: np.ndarray) -> float:
322
+ def fourth_central_moment(
323
+ returns: npt.ArrayLike, sample_weight: np.ndarray | None = None
324
+ ) -> float | np.ndarray:
206
325
  """Compute the Fourth central moment.
207
326
 
208
327
  Parameters
209
328
  ----------
210
- returns : ndarray of shape (n_observations,)
211
- Vector of returns.
329
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
330
+ Array of return values.
331
+
332
+ sample_weight : ndarray of shape (n_observations,), optional
333
+ Sample weights for each observation. If None, equal weights are assumed.
212
334
 
213
335
  Returns
214
336
  -------
215
- value : float
337
+ value : float or ndarray of shape (n_assets,)
216
338
  Fourth central moment.
339
+ If `returns` is a 1D-array, the result is a float.
340
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
217
341
  """
218
- return np.sum(np.power(returns - np.mean(returns, axis=0), 4)) / len(returns)
342
+ return mean(
343
+ (returns - mean(returns, sample_weight=sample_weight)) ** 4,
344
+ sample_weight=sample_weight,
345
+ )
219
346
 
220
347
 
221
- def kurtosis(returns: np.ndarray) -> float:
348
+ def kurtosis(
349
+ returns: npt.ArrayLike, sample_weight: np.ndarray | None = None
350
+ ) -> float | np.ndarray:
222
351
  """Compute the Kurtosis.
223
352
 
224
353
  The Kurtosis is a measure of the heaviness of the tail of the distribution.
@@ -226,20 +355,28 @@ def kurtosis(returns: np.ndarray) -> float:
226
355
 
227
356
  Parameters
228
357
  ----------
229
- returns : ndarray of shape (n_observations,)
230
- Vector of returns.
358
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
359
+ Array of return values.
360
+
361
+ sample_weight : ndarray of shape (n_observations,), optional
362
+ Sample weights for each observation. If None, equal weights are assumed.
231
363
 
232
364
  Returns
233
365
  -------
234
- value : float
366
+ value : float or ndarray of shape (n_assets,)
235
367
  Kurtosis.
368
+ If `returns` is a 1D-array, the result is a float.
369
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
236
370
  """
237
- return fourth_central_moment(returns) / standard_deviation(returns) ** 4
371
+ return (
372
+ fourth_central_moment(returns, sample_weight=sample_weight)
373
+ / variance(returns, sample_weight=sample_weight, biased=True) ** 2
374
+ )
238
375
 
239
376
 
240
377
  def fourth_lower_partial_moment(
241
- returns: np.ndarray, min_acceptable_return: float | None = None
242
- ) -> float:
378
+ returns: npt.ArrayLike, min_acceptable_return: float | None = None
379
+ ) -> float | np.ndarray:
243
380
  """Compute the fourth lower partial moment.
244
381
 
245
382
  The Fourth Lower Partial Moment is a measure of the heaviness of the downside tail
@@ -249,8 +386,8 @@ def fourth_lower_partial_moment(
249
386
 
250
387
  Parameters
251
388
  ----------
252
- returns : ndarray of shape (n_observations,)
253
- Vector of returns
389
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
390
+ Array of return values.
254
391
 
255
392
  min_acceptable_return : float, optional
256
393
  Minimum acceptable return. It is the return target to distinguish "downside" and
@@ -259,59 +396,79 @@ def fourth_lower_partial_moment(
259
396
 
260
397
  Returns
261
398
  -------
262
- value : float
399
+ value : float or ndarray of shape (n_assets,)
263
400
  Fourth lower partial moment.
401
+ If `returns` is a 1D-array, the result is a float.
402
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
264
403
  """
265
404
  if min_acceptable_return is None:
266
- min_acceptable_return = np.mean(returns, axis=0)
267
- return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 4)) / len(
268
- returns
269
- )
405
+ min_acceptable_return = mean(returns)
406
+ return mean(np.maximum(0, min_acceptable_return - returns) ** 4)
270
407
 
271
408
 
272
- def worst_realization(returns: np.ndarray) -> float:
409
+ def worst_realization(returns: npt.ArrayLike) -> float | np.ndarray:
273
410
  """Compute the worst realization (worst return).
274
411
 
275
412
  Parameters
276
413
  ----------
277
- returns : ndarray of shape (n_observations,)
278
- Vector of returns.
414
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
415
+ Array of return values.
279
416
 
280
417
  Returns
281
418
  -------
282
- value : float
419
+ value : float or ndarray of shape (n_assets,)
283
420
  Worst realization.
421
+ If `returns` is a 1D-array, the result is a float.
422
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
284
423
  """
285
- return -min(returns)
424
+ return -np.min(returns, axis=0)
286
425
 
287
426
 
288
- def value_at_risk(returns: np.ndarray, beta: float = 0.95) -> float:
427
+ def value_at_risk(
428
+ returns: npt.ArrayLike, beta: float = 0.95, sample_weight: np.ndarray | None = None
429
+ ) -> float | np.ndarray:
289
430
  """Compute the historical value at risk (VaR).
290
-
291
431
  The VaR is the maximum loss at a given confidence level (beta).
292
432
 
293
433
  Parameters
294
434
  ----------
295
- returns : ndarray of shape (n_observations,)
296
- Vector of returns.
435
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
436
+ Array of return values.
297
437
 
298
438
  beta : float, default=0.95
299
439
  The VaR confidence level (return on the worst (1-beta)% observation).
300
440
 
441
+ sample_weight : ndarray of shape (n_observations,), optional
442
+ Sample weights for each observation. If None, equal weights are assumed.
443
+
301
444
  Returns
302
445
  -------
303
- value : float
446
+ value : float or ndarray of shape (n_assets,)
304
447
  Value at Risk.
448
+ If `returns` is a 1D-array, the result is a float.
449
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
305
450
  """
306
- k = (1 - beta) * len(returns)
307
- ik = max(0, int(np.ceil(k) - 1))
308
- # We only need the first k elements so using `partition` O(n log(n)) is faster
309
- # than `sort` O(n).
310
- ret = np.partition(returns, ik)
311
- return -ret[ik]
451
+ returns = np.asarray(returns)
452
+ if sample_weight is None:
453
+ k = (1 - beta) * len(returns)
454
+ ik = max(0, int(np.ceil(k) - 1))
455
+ # We only need the first k elements so using `partition` O(n log(n)) is faster
456
+ # than `sort` O(n).
457
+ return -np.partition(returns, ik, axis=0)[ik]
458
+
459
+ sorted_idx = np.argsort(returns, axis=0)
460
+ cum_weights = np.cumsum(sample_weight[sorted_idx], axis=0)
461
+ i = np.apply_along_axis(
462
+ np.searchsorted, axis=0, arr=cum_weights, v=1 - beta, side="left"
463
+ )
464
+ if returns.ndim == 1:
465
+ return -returns[sorted_idx][i]
466
+ return -np.diag(np.take_along_axis(returns, sorted_idx, axis=0)[i])
312
467
 
313
468
 
314
- def cvar(returns: np.ndarray, beta: float = 0.95) -> float:
469
+ def cvar(
470
+ returns: npt.ArrayLike, beta: float = 0.95, sample_weight: np.ndarray | None = None
471
+ ) -> float | np.ndarray:
315
472
  """Compute the historical CVaR (conditional value at risk).
316
473
 
317
474
  The CVaR (or Tail VaR) represents the mean shortfall at a specified confidence
@@ -319,28 +476,63 @@ def cvar(returns: np.ndarray, beta: float = 0.95) -> float:
319
476
 
320
477
  Parameters
321
478
  ----------
322
- returns : ndarray of shape (n_observations,)
323
- Vector of returns.
479
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
480
+ Array of return values.
324
481
 
325
482
  beta : float, default=0.95
326
483
  The CVaR confidence level (expected VaR on the worst (1-beta)% observations).
327
484
 
485
+ sample_weight : ndarray of shape (n_observations,), optional
486
+ Sample weights for each observation. If None, equal weights are assumed.
487
+
328
488
  Returns
329
489
  -------
330
- value : float
490
+ value : float or ndarray of shape (n_assets,)
331
491
  CVaR.
492
+ If `returns` is a 1D-array, the result is a float.
493
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
332
494
  """
333
- k = (1 - beta) * len(returns)
334
- ik = max(0, int(np.ceil(k) - 1))
335
- # We only need the first k elements so using `partition` O(n log(n)) is faster
336
- # than `sort` O(n).
337
- ret = np.partition(returns, ik)
338
- return -np.sum(ret[:ik]) / k + ret[ik] * (ik / k - 1)
495
+ returns = np.asarray(returns)
496
+ if sample_weight is None:
497
+ k = (1 - beta) * len(returns)
498
+ ik = max(0, int(np.ceil(k) - 1))
499
+ # We only need the first k elements so using `partition` O(n log(n)) is faster
500
+ # than `sort` O(n).
501
+ ret = np.partition(returns, ik, axis=0)
502
+ return -np.sum(ret[:ik], axis=0) / k + ret[ik] * (ik / k - 1)
503
+
504
+ order = np.argsort(returns, axis=0)
505
+ sorted_returns = np.take_along_axis(returns, order, axis=0)
506
+ sorted_w = sample_weight[order]
507
+ cum_w = np.cumsum(sorted_w, axis=0)
508
+ idx = np.apply_along_axis(
509
+ np.searchsorted, axis=0, arr=cum_w, v=1 - beta, side="left"
510
+ )
511
+
512
+ def _func(_idx, _sorted_returns, _sorted_w, _cum_w) -> float:
513
+ if _idx == 0:
514
+ return _sorted_returns[0]
515
+ return (
516
+ _sorted_returns[:_idx] @ _sorted_w[:_idx]
517
+ + _sorted_returns[_idx] * (1 - beta - _cum_w[_idx - 1])
518
+ ) / (1 - beta)
519
+
520
+ if returns.ndim == 1:
521
+ return -_func(idx, sorted_returns, sorted_w, cum_w)
522
+ return -np.array(
523
+ [
524
+ _func(idx[i], sorted_returns[:, i], sorted_w[:, i], cum_w[:, i])
525
+ for i in range(returns.shape[1])
526
+ ]
527
+ )
339
528
 
340
529
 
341
530
  def entropic_risk_measure(
342
- returns: np.ndarray, theta: float = 1, beta: float = 0.95
343
- ) -> float:
531
+ returns: npt.ArrayLike,
532
+ theta: float = 1,
533
+ beta: float = 0.95,
534
+ sample_weight: np.ndarray | None = None,
535
+ ) -> float | np.ndarray:
344
536
  """Compute the entropic risk measure.
345
537
 
346
538
  The entropic risk measure is a risk measure which depends on the risk aversion
@@ -349,8 +541,8 @@ def entropic_risk_measure(
349
541
 
350
542
  Parameters
351
543
  ----------
352
- returns : ndarray of shape (n_observations,)
353
- Vector of returns.
544
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
545
+ Array of return values.
354
546
 
355
547
  theta : float, default=1.0
356
548
  Risk aversion.
@@ -358,15 +550,22 @@ def entropic_risk_measure(
358
550
  beta : float, default=0.95
359
551
  Confidence level.
360
552
 
553
+ sample_weight : ndarray of shape (n_observations,), optional
554
+ Sample weights for each observation. If None, equal weights are assumed.
555
+
361
556
  Returns
362
557
  -------
363
- value : float
558
+ value : float or ndarray of shape (n_assets,)
364
559
  Entropic risk measure.
560
+ If `returns` is a 1D-array, the result is a float.
561
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
365
562
  """
366
- return theta * np.log(np.mean(np.exp(-returns / theta)) / (1 - beta))
563
+ return theta * np.log(
564
+ mean(np.exp(-returns / theta), sample_weight=sample_weight) / (1 - beta)
565
+ )
367
566
 
368
567
 
369
- def evar(returns: np.ndarray, beta: float = 0.95) -> float:
568
+ def evar(returns: npt.ArrayLike, beta: float = 0.95) -> float:
370
569
  """Compute the EVaR (entropic value at risk) and its associated risk aversion.
371
570
 
372
571
  The EVaR is a coherent risk measure which is an upper bound for the VaR and the
@@ -402,15 +601,17 @@ def evar(returns: np.ndarray, beta: float = 0.95) -> float:
402
601
  return result.fun
403
602
 
404
603
 
405
- def get_cumulative_returns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
604
+ def get_cumulative_returns(
605
+ returns: npt.ArrayLike, compounded: bool = False
606
+ ) -> np.ndarray:
406
607
  """Compute the cumulative returns from the returns.
407
608
  Non-compounded cumulative returns start at 0.
408
609
  Compounded cumulative returns are rescaled to start at 1000.
409
610
 
410
611
  Parameters
411
612
  ----------
412
- returns : ndarray of shape (n_observations,)
413
- Vector of returns.
613
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
614
+ Array of return values.
414
615
 
415
616
  compounded : bool, default=False
416
617
  If this is set to True, the cumulative returns are compounded otherwise they
@@ -418,23 +619,24 @@ def get_cumulative_returns(returns: np.ndarray, compounded: bool = False) -> np.
418
619
 
419
620
  Returns
420
621
  -------
421
- values: ndarray of shape (n_observations,)
622
+ values: ndarray of shape (n_observations,) or (n_observations, n_assets)
422
623
  Cumulative returns.
423
624
  """
424
625
  if compounded:
425
- cumulative_returns = 1000 * np.cumprod(1 + returns) # Rescaled to start at 1000
626
+ # Rescaled to start at 1000
627
+ cumulative_returns = 1000 * np.cumprod(1 + returns, axis=0)
426
628
  else:
427
- cumulative_returns = np.cumsum(returns)
629
+ cumulative_returns = np.cumsum(returns, axis=0)
428
630
  return cumulative_returns
429
631
 
430
632
 
431
- def get_drawdowns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
633
+ def get_drawdowns(returns: npt.ArrayLike, compounded: bool = False) -> np.ndarray:
432
634
  """Compute the drawdowns' series from the returns.
433
635
 
434
636
  Parameters
435
637
  ----------
436
- returns : ndarray of shape (n_observations,)
437
- Vector of returns.
638
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
639
+ Array of return values.
438
640
 
439
641
  compounded : bool, default=False
440
642
  If this is set to True, the cumulative returns are compounded otherwise they
@@ -442,7 +644,7 @@ def get_drawdowns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
442
644
 
443
645
  Returns
444
646
  -------
445
- values: ndarray of shape (n_observations,)
647
+ values: ndarray of shape (n_observations,) or (n_observations, n_assets)
446
648
  Drawdowns.
447
649
  """
448
650
  cumulative_returns = get_cumulative_returns(returns=returns, compounded=compounded)
@@ -453,14 +655,14 @@ def get_drawdowns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
453
655
  return drawdowns
454
656
 
455
657
 
456
- def drawdown_at_risk(drawdowns: np.ndarray, beta: float = 0.95) -> float:
658
+ def drawdown_at_risk(drawdowns: np.ndarray, beta: float = 0.95) -> float | np.ndarray:
457
659
  """Compute the Drawdown at risk.
458
660
 
459
661
  The Drawdown at risk is the maximum drawdown at a given confidence level (beta).
460
662
 
461
663
  Parameters
462
664
  ----------
463
- drawdowns : ndarray of shape (n_observations,)
665
+ drawdowns : ndarray of shape (n_observations,) or (n_observations, n_assets)
464
666
  Vector of drawdowns.
465
667
 
466
668
  beta : float, default = 0.95
@@ -468,50 +670,56 @@ def drawdown_at_risk(drawdowns: np.ndarray, beta: float = 0.95) -> float:
468
670
 
469
671
  Returns
470
672
  -------
471
- value : float
472
- Drawdown at risk.
673
+ value : float or ndarray of shape (n_assets,)
674
+ Drawdown at risk.
675
+ If `returns` is a 1D-array, the result is a float.
676
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
473
677
  """
474
678
  return value_at_risk(returns=drawdowns, beta=beta)
475
679
 
476
680
 
477
- def max_drawdown(drawdowns: np.ndarray) -> float:
681
+ def max_drawdown(drawdowns: np.ndarray) -> float | np.ndarray:
478
682
  """Compute the maximum drawdown.
479
683
 
480
684
  Parameters
481
685
  ----------
482
- drawdowns : ndarray of shape (n_observations,)
686
+ drawdowns : ndarray of shape (n_observations,) or (n_observations, n_assets)
483
687
  Vector of drawdowns.
484
688
 
485
689
  Returns
486
690
  -------
487
- value : float
691
+ value : float or ndarray of shape (n_assets,)
488
692
  Maximum drawdown.
693
+ If `returns` is a 1D-array, the result is a float.
694
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
489
695
  """
490
696
  return drawdown_at_risk(drawdowns=drawdowns, beta=1)
491
697
 
492
698
 
493
- def average_drawdown(drawdowns: np.ndarray) -> float:
699
+ def average_drawdown(drawdowns: np.ndarray) -> float | np.ndarray:
494
700
  """Compute the average drawdown.
495
701
 
496
702
  Parameters
497
703
  ----------
498
- drawdowns : ndarray of shape (n_observations,)
704
+ drawdowns : ndarray of shape (n_observations,) or (n_observations, n_assets)
499
705
  Vector of drawdowns.
500
706
 
501
707
  Returns
502
708
  -------
503
- value : float
709
+ value : float or ndarray of shape (n_assets,)
504
710
  Average drawdown.
711
+ If `returns` is a 1D-array, the result is a float.
712
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
505
713
  """
506
714
  return cdar(drawdowns=drawdowns, beta=0)
507
715
 
508
716
 
509
- def cdar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
717
+ def cdar(drawdowns: np.ndarray, beta: float = 0.95) -> float | np.ndarray:
510
718
  """Compute the historical CDaR (conditional drawdown at risk).
511
719
 
512
720
  Parameters
513
721
  ----------
514
- drawdowns : ndarray of shape (n_observations,)
722
+ drawdowns : ndarray of shape (n_observations,) or (n_observations, n_assets)
515
723
  Vector of drawdowns.
516
724
 
517
725
  beta : float, default = 0.95
@@ -520,8 +728,10 @@ def cdar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
520
728
 
521
729
  Returns
522
730
  -------
523
- value : float
731
+ value : float or ndarray of shape (n_assets,)
524
732
  CDaR.
733
+ If `returns` is a 1D-array, the result is a float.
734
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
525
735
  """
526
736
  return cvar(returns=drawdowns, beta=beta)
527
737
 
@@ -549,20 +759,22 @@ def edar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
549
759
  return evar(returns=drawdowns, beta=beta)
550
760
 
551
761
 
552
- def ulcer_index(drawdowns: np.ndarray) -> float:
762
+ def ulcer_index(drawdowns: np.ndarray) -> float | np.ndarray:
553
763
  """Compute the Ulcer index.
554
764
 
555
765
  Parameters
556
766
  ----------
557
- drawdowns : ndarray of shape (n_observations,)
767
+ drawdowns : ndarray of shape (n_observations,) or (n_observations, n_assets)
558
768
  Vector of drawdowns.
559
769
 
560
770
  Returns
561
771
  -------
562
- value : float
563
- Ulcer index.
772
+ value : float or ndarray of shape (n_assets,)
773
+ Ulcer Index.
774
+ If `returns` is a 1D-array, the result is a float.
775
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
564
776
  """
565
- return np.sqrt(np.sum(np.power(drawdowns, 2)) / len(drawdowns))
777
+ return np.sqrt(mean(np.power(drawdowns, 2)))
566
778
 
567
779
 
568
780
  def owa_gmd_weights(n_observations: int) -> np.ndarray:
@@ -583,7 +795,7 @@ def owa_gmd_weights(n_observations: int) -> np.ndarray:
583
795
  )
584
796
 
585
797
 
586
- def gini_mean_difference(returns: np.ndarray) -> float:
798
+ def gini_mean_difference(returns: npt.ArrayLike) -> float | np.ndarray:
587
799
  """Compute the Gini mean difference (GMD).
588
800
 
589
801
  The GMD is the expected absolute difference between two realisations.
@@ -594,16 +806,18 @@ def gini_mean_difference(returns: np.ndarray) -> float:
594
806
 
595
807
  Parameters
596
808
  ----------
597
- returns : ndarray of shape (n_observations,)
598
- Vector of returns.
809
+ returns : ndarray of shape (n_observations,) or (n_observations, n_assets)
810
+ Array of return values.
599
811
 
600
812
  Returns
601
813
  -------
602
- value : float
814
+ value : float or ndarray of shape (n_assets,)
603
815
  Gini mean difference.
816
+ If `returns` is a 1D-array, the result is a float.
817
+ If `returns` is a 2D-array, the result is a ndarray of shape (n_assets,).
604
818
  """
605
819
  w = owa_gmd_weights(len(returns))
606
- return float(w @ np.sort(returns, axis=0))
820
+ return w @ np.sort(returns, axis=0)
607
821
 
608
822
 
609
823
  def effective_number_assets(weights: np.ndarray) -> float:
@@ -631,3 +845,24 @@ def effective_number_assets(weights: np.ndarray) -> float:
631
845
  Lovett, William Anthony (1988)
632
846
  """
633
847
  return 1.0 / (np.power(weights, 2).sum())
848
+
849
+
850
+ def correlation(X: np.ndarray, sample_weight: np.ndarray | None = None) -> np.ndarray:
851
+ """Compute the correlation matrix.
852
+
853
+ Parameters
854
+ ----------
855
+ X : ndarray of shape (n_observations, n_assets)
856
+ Array of values.
857
+
858
+ sample_weight : ndarray of shape (n_observations,), optional
859
+ Sample weights for each observation. If None, equal weights are assumed.
860
+
861
+ Returns
862
+ -------
863
+ corr : ndarray of shape (n_assets,)
864
+ The correlation matrix.
865
+ """
866
+ cov = np.cov(X, rowvar=False, aweights=sample_weight)
867
+ std = np.sqrt(np.diag(cov))
868
+ return cov / np.outer(std, std)