skfolio 0.9.0__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.
- skfolio/distribution/multivariate/_vine_copula.py +35 -34
- skfolio/distribution/univariate/_base.py +20 -15
- skfolio/exceptions.py +5 -0
- skfolio/measures/__init__.py +2 -0
- skfolio/measures/_measures.py +390 -155
- skfolio/optimization/_base.py +21 -4
- skfolio/optimization/cluster/hierarchical/_base.py +16 -13
- skfolio/optimization/cluster/hierarchical/_herc.py +6 -6
- skfolio/optimization/cluster/hierarchical/_hrp.py +8 -6
- skfolio/optimization/convex/_base.py +238 -144
- skfolio/optimization/convex/_distributionally_robust.py +32 -20
- skfolio/optimization/convex/_maximum_diversification.py +15 -18
- skfolio/optimization/convex/_mean_risk.py +35 -25
- skfolio/optimization/convex/_risk_budgeting.py +23 -21
- skfolio/optimization/ensemble/__init__.py +2 -4
- skfolio/optimization/ensemble/_stacking.py +1 -1
- skfolio/optimization/naive/_naive.py +2 -2
- skfolio/population/_population.py +30 -9
- skfolio/portfolio/_base.py +68 -26
- skfolio/portfolio/_multi_period_portfolio.py +5 -0
- skfolio/portfolio/_portfolio.py +5 -0
- skfolio/pre_selection/_select_non_expiring.py +1 -1
- skfolio/prior/__init__.py +6 -2
- skfolio/prior/_base.py +7 -3
- skfolio/prior/_black_litterman.py +14 -12
- skfolio/prior/_empirical.py +8 -7
- skfolio/prior/_entropy_pooling.py +1493 -0
- skfolio/prior/_factor_model.py +39 -22
- skfolio/prior/_opinion_pooling.py +475 -0
- skfolio/prior/_synthetic_data.py +10 -8
- skfolio/uncertainty_set/_bootstrap.py +4 -4
- skfolio/uncertainty_set/_empirical.py +6 -6
- skfolio/utils/equations.py +11 -5
- skfolio/utils/figure.py +185 -0
- skfolio/utils/tools.py +4 -2
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/METADATA +94 -5
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/RECORD +41 -39
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/WHEEL +1 -1
- skfolio/synthetic_returns/__init__.py +0 -1
- /skfolio/{optimization/ensemble/_base.py → utils/composition.py} +0 -0
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {skfolio-0.9.0.dist-info → skfolio-0.10.0.dist-info}/top_level.txt +0 -0
skfolio/measures/_measures.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
31
|
-
|
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
|
-
|
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
|
-
|
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 =
|
51
|
-
|
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:
|
56
|
-
|
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
|
-
|
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
|
-
|
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 =
|
79
|
-
|
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(
|
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
|
-
|
88
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
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:
|
100
|
-
|
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
|
-
|
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
|
-
|
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 =
|
122
|
-
|
123
|
-
|
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(
|
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
|
-
|
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=
|
222
|
+
return np.sqrt(variance(returns, sample_weight=sample_weight, biased=biased))
|
141
223
|
|
142
224
|
|
143
225
|
def semi_deviation(
|
144
|
-
returns:
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
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-
|
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(
|
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(
|
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
|
-
|
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
|
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(
|
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
|
-
|
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
|
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(
|
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
|
-
|
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
|
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(
|
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
|
-
|
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
|
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:
|
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
|
-
|
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 =
|
267
|
-
return
|
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:
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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:
|
343
|
-
|
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
|
-
|
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(
|
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:
|
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(
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
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(
|
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:
|
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
|
-
|
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
|
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)
|