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.
- skfolio/__init__.py +29 -0
- skfolio/cluster/__init__.py +8 -0
- skfolio/cluster/_hierarchical.py +387 -0
- skfolio/datasets/__init__.py +20 -0
- skfolio/datasets/_base.py +389 -0
- skfolio/datasets/data/__init__.py +0 -0
- skfolio/datasets/data/factors_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
- skfolio/datasets/data/sp500_index.csv.gz +0 -0
- skfolio/distance/__init__.py +26 -0
- skfolio/distance/_base.py +55 -0
- skfolio/distance/_distance.py +574 -0
- skfolio/exceptions.py +30 -0
- skfolio/measures/__init__.py +76 -0
- skfolio/measures/_enums.py +355 -0
- skfolio/measures/_measures.py +607 -0
- skfolio/metrics/__init__.py +3 -0
- skfolio/metrics/_scorer.py +121 -0
- skfolio/model_selection/__init__.py +18 -0
- skfolio/model_selection/_combinatorial.py +407 -0
- skfolio/model_selection/_validation.py +194 -0
- skfolio/model_selection/_walk_forward.py +221 -0
- skfolio/moments/__init__.py +41 -0
- skfolio/moments/covariance/__init__.py +29 -0
- skfolio/moments/covariance/_base.py +101 -0
- skfolio/moments/covariance/_covariance.py +1108 -0
- skfolio/moments/expected_returns/__init__.py +21 -0
- skfolio/moments/expected_returns/_base.py +31 -0
- skfolio/moments/expected_returns/_expected_returns.py +415 -0
- skfolio/optimization/__init__.py +36 -0
- skfolio/optimization/_base.py +147 -0
- skfolio/optimization/cluster/__init__.py +13 -0
- skfolio/optimization/cluster/_nco.py +348 -0
- skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
- skfolio/optimization/cluster/hierarchical/_base.py +440 -0
- skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
- skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
- skfolio/optimization/convex/__init__.py +16 -0
- skfolio/optimization/convex/_base.py +1944 -0
- skfolio/optimization/convex/_distributionally_robust.py +392 -0
- skfolio/optimization/convex/_maximum_diversification.py +417 -0
- skfolio/optimization/convex/_mean_risk.py +974 -0
- skfolio/optimization/convex/_risk_budgeting.py +560 -0
- skfolio/optimization/ensemble/__init__.py +6 -0
- skfolio/optimization/ensemble/_base.py +87 -0
- skfolio/optimization/ensemble/_stacking.py +326 -0
- skfolio/optimization/naive/__init__.py +3 -0
- skfolio/optimization/naive/_naive.py +173 -0
- skfolio/population/__init__.py +3 -0
- skfolio/population/_population.py +883 -0
- skfolio/portfolio/__init__.py +13 -0
- skfolio/portfolio/_base.py +1096 -0
- skfolio/portfolio/_multi_period_portfolio.py +610 -0
- skfolio/portfolio/_portfolio.py +842 -0
- skfolio/pre_selection/__init__.py +7 -0
- skfolio/pre_selection/_pre_selection.py +342 -0
- skfolio/preprocessing/__init__.py +3 -0
- skfolio/preprocessing/_returns.py +114 -0
- skfolio/prior/__init__.py +18 -0
- skfolio/prior/_base.py +63 -0
- skfolio/prior/_black_litterman.py +238 -0
- skfolio/prior/_empirical.py +163 -0
- skfolio/prior/_factor_model.py +268 -0
- skfolio/typing.py +50 -0
- skfolio/uncertainty_set/__init__.py +23 -0
- skfolio/uncertainty_set/_base.py +108 -0
- skfolio/uncertainty_set/_bootstrap.py +281 -0
- skfolio/uncertainty_set/_empirical.py +237 -0
- skfolio/utils/__init__.py +0 -0
- skfolio/utils/bootstrap.py +115 -0
- skfolio/utils/equations.py +350 -0
- skfolio/utils/sorting.py +117 -0
- skfolio/utils/stats.py +466 -0
- skfolio/utils/tools.py +567 -0
- skfolio-0.0.1.dist-info/LICENSE +29 -0
- skfolio-0.0.1.dist-info/METADATA +568 -0
- skfolio-0.0.1.dist-info/RECORD +79 -0
- skfolio-0.0.1.dist-info/WHEEL +5 -0
- skfolio-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
|
|
1
|
+
"""Module that includes all Measures functions used across `skfolio`."""
|
2
|
+
|
3
|
+
# Author: Hugo Delatte <delatte.hugo@gmail.com>
|
4
|
+
# License: BSD 3 clause
|
5
|
+
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import scipy.optimize as sco
|
9
|
+
|
10
|
+
|
11
|
+
def mean(returns: np.ndarray) -> float:
|
12
|
+
"""Compute the mean.
|
13
|
+
|
14
|
+
Parameters
|
15
|
+
----------
|
16
|
+
returns : ndarray of shape (n_observations,)
|
17
|
+
Vector of returns.
|
18
|
+
|
19
|
+
Returns
|
20
|
+
-------
|
21
|
+
value : float
|
22
|
+
Mean
|
23
|
+
"""
|
24
|
+
return returns.mean()
|
25
|
+
|
26
|
+
|
27
|
+
def mean_absolute_deviation(
|
28
|
+
returns: np.ndarray, min_acceptable_return: float | None = None
|
29
|
+
) -> float:
|
30
|
+
"""Compute the mean absolute deviation (MAD).
|
31
|
+
|
32
|
+
Parameters
|
33
|
+
----------
|
34
|
+
returns : ndarray of shape (n_observations,)
|
35
|
+
Vector of returns.
|
36
|
+
|
37
|
+
min_acceptable_return : float, optional
|
38
|
+
Minimum acceptable return. It is the return target to distinguish "downside" and
|
39
|
+
"upside" returns.
|
40
|
+
The default (`None`) is to use the returns' mean.
|
41
|
+
|
42
|
+
Returns
|
43
|
+
-------
|
44
|
+
value : float
|
45
|
+
Mean absolute deviation.
|
46
|
+
"""
|
47
|
+
if min_acceptable_return is None:
|
48
|
+
min_acceptable_return = np.mean(returns, axis=0)
|
49
|
+
return float(np.mean(np.abs(returns - min_acceptable_return)))
|
50
|
+
|
51
|
+
|
52
|
+
def first_lower_partial_moment(
|
53
|
+
returns: np.ndarray, min_acceptable_return: float | None = None
|
54
|
+
) -> float:
|
55
|
+
"""Compute the first lower partial moment.
|
56
|
+
|
57
|
+
The first lower partial moment is the mean of the returns below a minimum
|
58
|
+
acceptable return.
|
59
|
+
|
60
|
+
Parameters
|
61
|
+
----------
|
62
|
+
returns : ndarray of shape (n_observations,)
|
63
|
+
Vector of returns
|
64
|
+
|
65
|
+
min_acceptable_return : float, optional
|
66
|
+
Minimum acceptable return. It is the return target to distinguish "downside" and
|
67
|
+
"upside" returns.
|
68
|
+
The default (`None`) is to use the mean.
|
69
|
+
|
70
|
+
Returns
|
71
|
+
-------
|
72
|
+
value : float
|
73
|
+
First lower partial moment.
|
74
|
+
"""
|
75
|
+
if min_acceptable_return is None:
|
76
|
+
min_acceptable_return = np.mean(returns, axis=0)
|
77
|
+
return -np.sum(np.minimum(0, returns - min_acceptable_return)) / len(returns)
|
78
|
+
|
79
|
+
|
80
|
+
def variance(returns: np.ndarray) -> float:
|
81
|
+
"""Compute the variance (second moment).
|
82
|
+
|
83
|
+
Parameters
|
84
|
+
----------
|
85
|
+
returns : ndarray of shape (n_observations,)
|
86
|
+
Vector of returns.
|
87
|
+
|
88
|
+
Returns
|
89
|
+
-------
|
90
|
+
value : float
|
91
|
+
Variance.
|
92
|
+
"""
|
93
|
+
return returns.var(ddof=1)
|
94
|
+
|
95
|
+
|
96
|
+
def semi_variance(
|
97
|
+
returns: np.ndarray, min_acceptable_return: float | None = None
|
98
|
+
) -> float:
|
99
|
+
"""Compute the semi-variance (second lower partial moment).
|
100
|
+
|
101
|
+
The semi-variance is the variance of the returns below a minimum acceptable return.
|
102
|
+
|
103
|
+
Parameters
|
104
|
+
----------
|
105
|
+
returns : ndarray of shape (n_observations,)
|
106
|
+
Vector of returns
|
107
|
+
|
108
|
+
min_acceptable_return : float, optional
|
109
|
+
Minimum acceptable return. It is the return target to distinguish "downside" and
|
110
|
+
"upside" returns.
|
111
|
+
The default (`None`) is to use the mean.
|
112
|
+
|
113
|
+
Returns
|
114
|
+
-------
|
115
|
+
value : float
|
116
|
+
Semi-variance.
|
117
|
+
"""
|
118
|
+
if min_acceptable_return is None:
|
119
|
+
min_acceptable_return = np.mean(returns, axis=0)
|
120
|
+
return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 2)) / (
|
121
|
+
len(returns) - 1
|
122
|
+
)
|
123
|
+
|
124
|
+
|
125
|
+
def standard_deviation(returns: np.ndarray) -> float:
|
126
|
+
"""Compute the standard-deviation (square root of the second moment).
|
127
|
+
|
128
|
+
Parameters
|
129
|
+
----------
|
130
|
+
returns : ndarray of shape (n_observations,)
|
131
|
+
Vector of returns.
|
132
|
+
|
133
|
+
Returns
|
134
|
+
-------
|
135
|
+
value : float
|
136
|
+
Standard-deviation.
|
137
|
+
"""
|
138
|
+
return np.sqrt(variance(returns=returns))
|
139
|
+
|
140
|
+
|
141
|
+
def semi_deviation(
|
142
|
+
returns: np.ndarray, min_acceptable_return: float | None = None
|
143
|
+
) -> float:
|
144
|
+
"""Compute the semi standard-deviation (semi-deviation) (square root of the second lower
|
145
|
+
partial moment).
|
146
|
+
|
147
|
+
Parameters
|
148
|
+
----------
|
149
|
+
returns : ndarray of shape (n_observations,)
|
150
|
+
Vector of returns.
|
151
|
+
|
152
|
+
min_acceptable_return : float, optional
|
153
|
+
Minimum acceptable return. It is the return target to distinguish "downside" and
|
154
|
+
"upside" returns.
|
155
|
+
The default (`None`) is to use the returns mean.
|
156
|
+
|
157
|
+
Returns
|
158
|
+
-------
|
159
|
+
value : float
|
160
|
+
Semi-standard-deviation.
|
161
|
+
"""
|
162
|
+
return np.sqrt(
|
163
|
+
semi_variance(returns=returns, min_acceptable_return=min_acceptable_return)
|
164
|
+
)
|
165
|
+
|
166
|
+
|
167
|
+
def third_central_moment(returns: np.ndarray) -> float:
|
168
|
+
"""Compute the third central moment.
|
169
|
+
|
170
|
+
Parameters
|
171
|
+
----------
|
172
|
+
returns : ndarray of shape (n_observations,)
|
173
|
+
Vector of returns.
|
174
|
+
|
175
|
+
Returns
|
176
|
+
-------
|
177
|
+
value : float
|
178
|
+
Third central moment.
|
179
|
+
"""
|
180
|
+
|
181
|
+
return np.sum(np.power(returns - np.mean(returns, axis=0), 3)) / len(returns)
|
182
|
+
|
183
|
+
|
184
|
+
def skew(returns: np.ndarray) -> float:
|
185
|
+
"""Compute the Skew.
|
186
|
+
|
187
|
+
The Skew is a measure of the lopsidedness of the distribution.
|
188
|
+
A symmetric distribution have a Skew of zero.
|
189
|
+
Higher Skew corresponds to longer right tail.
|
190
|
+
|
191
|
+
Parameters
|
192
|
+
----------
|
193
|
+
returns : ndarray of shape (n_observations,)
|
194
|
+
Vector of returns.
|
195
|
+
|
196
|
+
Returns
|
197
|
+
-------
|
198
|
+
value : float
|
199
|
+
Skew.
|
200
|
+
"""
|
201
|
+
|
202
|
+
return third_central_moment(returns) / standard_deviation(returns) ** 3
|
203
|
+
|
204
|
+
|
205
|
+
def fourth_central_moment(returns: np.ndarray) -> float:
|
206
|
+
"""Compute the Fourth central moment.
|
207
|
+
|
208
|
+
Parameters
|
209
|
+
----------
|
210
|
+
returns : ndarray of shape (n_observations,)
|
211
|
+
Vector of returns.
|
212
|
+
|
213
|
+
Returns
|
214
|
+
-------
|
215
|
+
value : float
|
216
|
+
Fourth central moment.
|
217
|
+
"""
|
218
|
+
return np.sum(np.power(returns - np.mean(returns, axis=0), 4)) / len(returns)
|
219
|
+
|
220
|
+
|
221
|
+
def kurtosis(returns: np.ndarray) -> float:
|
222
|
+
"""Compute the Kurtosis.
|
223
|
+
|
224
|
+
The Kurtosis is a measure of the heaviness of the tail of the distribution.
|
225
|
+
Higher Kurtosis corresponds to greater extremity of deviations (fat tails).
|
226
|
+
|
227
|
+
Parameters
|
228
|
+
----------
|
229
|
+
returns : ndarray of shape (n_observations,)
|
230
|
+
Vector of returns.
|
231
|
+
|
232
|
+
Returns
|
233
|
+
-------
|
234
|
+
value : float
|
235
|
+
Kurtosis.
|
236
|
+
"""
|
237
|
+
|
238
|
+
return fourth_central_moment(returns) / standard_deviation(returns) ** 4
|
239
|
+
|
240
|
+
|
241
|
+
def fourth_lower_partial_moment(
|
242
|
+
returns: np.ndarray, min_acceptable_return: float | None = None
|
243
|
+
) -> float:
|
244
|
+
"""Compute the fourth lower partial moment.
|
245
|
+
|
246
|
+
The Fourth Lower Partial Moment is a measure of the heaviness of the downside tail
|
247
|
+
of the returns below a minimum acceptable return.
|
248
|
+
Higher Fourth Lower Partial Moment corresponds to greater extremity of downside
|
249
|
+
deviations (downside fat tail).
|
250
|
+
|
251
|
+
Parameters
|
252
|
+
----------
|
253
|
+
returns : ndarray of shape (n_observations,)
|
254
|
+
Vector of returns
|
255
|
+
|
256
|
+
min_acceptable_return : float, optional
|
257
|
+
Minimum acceptable return. It is the return target to distinguish "downside" and
|
258
|
+
"upside" returns.
|
259
|
+
The default (`None`) is to use the returns mean.
|
260
|
+
|
261
|
+
Returns
|
262
|
+
-------
|
263
|
+
value : float
|
264
|
+
Fourth lower partial moment.
|
265
|
+
"""
|
266
|
+
if min_acceptable_return is None:
|
267
|
+
min_acceptable_return = np.mean(returns, axis=0)
|
268
|
+
return np.sum(np.power(np.minimum(0, returns - min_acceptable_return), 4)) / len(
|
269
|
+
returns
|
270
|
+
)
|
271
|
+
|
272
|
+
|
273
|
+
def worst_realization(returns: np.ndarray) -> float:
|
274
|
+
"""Compute the worst realization (worst return).
|
275
|
+
|
276
|
+
Parameters
|
277
|
+
----------
|
278
|
+
returns : ndarray of shape (n_observations,)
|
279
|
+
Vector of returns.
|
280
|
+
|
281
|
+
Returns
|
282
|
+
-------
|
283
|
+
value : float
|
284
|
+
Worst realization.
|
285
|
+
"""
|
286
|
+
return -min(returns)
|
287
|
+
|
288
|
+
|
289
|
+
def value_at_risk(returns: np.ndarray, beta: float = 0.95) -> float:
|
290
|
+
"""Compute the historical value at risk (VaR).
|
291
|
+
|
292
|
+
The VaR is the maximum loss at a given confidence level (beta).
|
293
|
+
|
294
|
+
Parameters
|
295
|
+
----------
|
296
|
+
returns : ndarray of shape (n_observations,)
|
297
|
+
Vector of returns.
|
298
|
+
|
299
|
+
beta : float, default=0.95
|
300
|
+
The VaR confidence level (return on the worst (1-beta)% observation).
|
301
|
+
|
302
|
+
Returns
|
303
|
+
-------
|
304
|
+
value : float
|
305
|
+
Value at Risk.
|
306
|
+
"""
|
307
|
+
k = (1 - beta) * len(returns)
|
308
|
+
ik = max(0, int(np.ceil(k) - 1))
|
309
|
+
# We only need the first k elements so using `partition` O(n log(n)) is faster
|
310
|
+
# than `sort` O(n).
|
311
|
+
ret = np.partition(returns, ik)
|
312
|
+
return -ret[ik]
|
313
|
+
|
314
|
+
|
315
|
+
def cvar(returns: np.ndarray, beta: float = 0.95) -> float:
|
316
|
+
"""Compute the historical CVaR (conditional value at risk).
|
317
|
+
|
318
|
+
The CVaR (or Tail VaR) represents the mean shortfall at a specified confidence
|
319
|
+
level (beta).
|
320
|
+
|
321
|
+
Parameters
|
322
|
+
----------
|
323
|
+
returns : ndarray of shape (n_observations,)
|
324
|
+
Vector of returns.
|
325
|
+
|
326
|
+
beta : float, default=0.95
|
327
|
+
The CVaR confidence level (expected VaR on the worst (1-beta)% observations).
|
328
|
+
|
329
|
+
Returns
|
330
|
+
-------
|
331
|
+
value : float
|
332
|
+
CVaR.
|
333
|
+
"""
|
334
|
+
k = (1 - beta) * len(returns)
|
335
|
+
ik = max(0, int(np.ceil(k) - 1))
|
336
|
+
# We only need the first k elements so using `partition` O(n log(n)) is faster
|
337
|
+
# than `sort` O(n).
|
338
|
+
ret = np.partition(returns, ik)
|
339
|
+
return -np.sum(ret[:ik]) / k + ret[ik] * (ik / k - 1)
|
340
|
+
|
341
|
+
|
342
|
+
def entropic_risk_measure(
|
343
|
+
returns: np.ndarray, theta: float = 1, beta: float = 0.95
|
344
|
+
) -> float:
|
345
|
+
"""Compute the entropic risk measure.
|
346
|
+
|
347
|
+
The entropic risk measure is a risk measure which depends on the risk aversion
|
348
|
+
defined by the investor (theat) through the exponential utility function at a given
|
349
|
+
confidence level (beta).
|
350
|
+
|
351
|
+
Parameters
|
352
|
+
----------
|
353
|
+
returns : ndarray of shape (n_observations,)
|
354
|
+
Vector of returns.
|
355
|
+
|
356
|
+
theta : float, default=1.0
|
357
|
+
Risk aversion.
|
358
|
+
|
359
|
+
beta : float, default=0.95
|
360
|
+
Confidence level.
|
361
|
+
|
362
|
+
Returns
|
363
|
+
-------
|
364
|
+
value : float
|
365
|
+
Entropic risk measure.
|
366
|
+
"""
|
367
|
+
return theta * np.log(np.mean(np.exp(-returns / theta)) / (1 - beta))
|
368
|
+
|
369
|
+
|
370
|
+
def evar(returns: np.ndarray, beta: float = 0.95) -> float:
|
371
|
+
"""Compute the EVaR (entropic value at risk) and its associated risk aversion.
|
372
|
+
|
373
|
+
The EVaR is a coherent risk measure which is an upper bound for the VaR and the
|
374
|
+
CVaR, obtained from the Chernoff inequality. The EVaR can be represented by using
|
375
|
+
the concept of relative entropy.
|
376
|
+
|
377
|
+
Parameters
|
378
|
+
----------
|
379
|
+
returns : ndarray of shape (n_observations,)
|
380
|
+
Vector of returns.
|
381
|
+
|
382
|
+
beta : float, default=0.95
|
383
|
+
The EVaR confidence level.
|
384
|
+
|
385
|
+
Returns
|
386
|
+
-------
|
387
|
+
value : float
|
388
|
+
EVaR.
|
389
|
+
"""
|
390
|
+
|
391
|
+
def func(x: float) -> float:
|
392
|
+
return entropic_risk_measure(returns=returns, theta=x, beta=beta)
|
393
|
+
|
394
|
+
# The lower bound is chosen to avoid exp overflow
|
395
|
+
lower_bound = np.max(-returns) / 100
|
396
|
+
result = sco.minimize(
|
397
|
+
func,
|
398
|
+
x0=np.array([lower_bound * 2]),
|
399
|
+
method="SLSQP",
|
400
|
+
bounds=[(lower_bound, np.inf)],
|
401
|
+
tol=1e-10,
|
402
|
+
)
|
403
|
+
return result.fun
|
404
|
+
|
405
|
+
|
406
|
+
def get_cumulative_returns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
|
407
|
+
"""Compute the cumulative returns from the returns.
|
408
|
+
Non-compounded cumulative returns start at 0.
|
409
|
+
Compounded cumulative returns are rescaled to start at 1000.
|
410
|
+
|
411
|
+
Parameters
|
412
|
+
----------
|
413
|
+
returns : ndarray of shape (n_observations,)
|
414
|
+
Vector of returns.
|
415
|
+
|
416
|
+
compounded : bool, default=False
|
417
|
+
If this is set to True, the cumulative returns are compounded otherwise they
|
418
|
+
are uncompounded.
|
419
|
+
|
420
|
+
Returns
|
421
|
+
-------
|
422
|
+
values: ndarray of shape (n_observations,)
|
423
|
+
Cumulative returns.
|
424
|
+
"""
|
425
|
+
if compounded:
|
426
|
+
cumulative_returns = 1000 * np.cumprod(1 + returns) # Rescaled to start at 1000
|
427
|
+
else:
|
428
|
+
cumulative_returns = np.cumsum(returns)
|
429
|
+
return cumulative_returns
|
430
|
+
|
431
|
+
|
432
|
+
def get_drawdowns(returns: np.ndarray, compounded: bool = False) -> np.ndarray:
|
433
|
+
"""Compute the drawdowns' series from the returns.
|
434
|
+
|
435
|
+
Parameters
|
436
|
+
----------
|
437
|
+
returns : ndarray of shape (n_observations,)
|
438
|
+
Vector of returns.
|
439
|
+
|
440
|
+
compounded : bool, default=False
|
441
|
+
If this is set to True, the cumulative returns are compounded otherwise they
|
442
|
+
are uncompounded.
|
443
|
+
|
444
|
+
Returns
|
445
|
+
-------
|
446
|
+
values: ndarray of shape (n_observations,)
|
447
|
+
Drawdowns.
|
448
|
+
"""
|
449
|
+
cumulative_returns = get_cumulative_returns(returns=returns, compounded=compounded)
|
450
|
+
if compounded:
|
451
|
+
drawdowns = cumulative_returns / np.maximum.accumulate(cumulative_returns) - 1
|
452
|
+
else:
|
453
|
+
drawdowns = cumulative_returns - np.maximum.accumulate(cumulative_returns)
|
454
|
+
return drawdowns
|
455
|
+
|
456
|
+
|
457
|
+
def drawdown_at_risk(drawdowns: np.ndarray, beta: float = 0.95) -> float:
|
458
|
+
"""Compute the Drawdown at risk.
|
459
|
+
|
460
|
+
The Drawdown at risk is the maximum drawdown at a given confidence level (beta).
|
461
|
+
|
462
|
+
Parameters
|
463
|
+
----------
|
464
|
+
drawdowns : ndarray of shape (n_observations,)
|
465
|
+
Vector of drawdowns.
|
466
|
+
|
467
|
+
beta : float, default = 0.95
|
468
|
+
The DaR confidence level (drawdown on the worst (1-beta)% observations).
|
469
|
+
|
470
|
+
Returns
|
471
|
+
-------
|
472
|
+
value : float
|
473
|
+
Drawdown at risk.
|
474
|
+
"""
|
475
|
+
return value_at_risk(returns=drawdowns, beta=beta)
|
476
|
+
|
477
|
+
|
478
|
+
def max_drawdown(drawdowns: np.ndarray) -> float:
|
479
|
+
"""Compute the maximum drawdown.
|
480
|
+
|
481
|
+
Parameters
|
482
|
+
----------
|
483
|
+
drawdowns : ndarray of shape (n_observations,)
|
484
|
+
Vector of drawdowns.
|
485
|
+
|
486
|
+
Returns
|
487
|
+
-------
|
488
|
+
value : float
|
489
|
+
Maximum drawdown.
|
490
|
+
"""
|
491
|
+
return drawdown_at_risk(drawdowns=drawdowns, beta=1)
|
492
|
+
|
493
|
+
|
494
|
+
def average_drawdown(drawdowns: np.ndarray) -> float:
|
495
|
+
"""Compute the average drawdown.
|
496
|
+
|
497
|
+
Parameters
|
498
|
+
----------
|
499
|
+
drawdowns : ndarray of shape (n_observations,)
|
500
|
+
Vector of drawdowns.
|
501
|
+
|
502
|
+
Returns
|
503
|
+
-------
|
504
|
+
value : float
|
505
|
+
Average drawdown.
|
506
|
+
"""
|
507
|
+
return cdar(drawdowns=drawdowns, beta=0)
|
508
|
+
|
509
|
+
|
510
|
+
def cdar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
|
511
|
+
"""Compute the historical CDaR (conditional drawdown at risk).
|
512
|
+
|
513
|
+
Parameters
|
514
|
+
----------
|
515
|
+
drawdowns : ndarray of shape (n_observations,)
|
516
|
+
Vector of drawdowns.
|
517
|
+
|
518
|
+
beta : float, default = 0.95
|
519
|
+
The CDaR confidence level (expected drawdown on the worst
|
520
|
+
(1-beta)% observations).
|
521
|
+
|
522
|
+
Returns
|
523
|
+
-------
|
524
|
+
value : float
|
525
|
+
CDaR.
|
526
|
+
"""
|
527
|
+
return cvar(returns=drawdowns, beta=beta)
|
528
|
+
|
529
|
+
|
530
|
+
def edar(drawdowns: np.ndarray, beta: float = 0.95) -> float:
|
531
|
+
"""Compute the EDaR (entropic drawdown at risk).
|
532
|
+
|
533
|
+
The EDaR is a coherent risk measure which is an upper bound for the DaR and the
|
534
|
+
CDaR, obtained from the Chernoff inequality. The EDaR can be represented by using
|
535
|
+
the concept of relative entropy.
|
536
|
+
|
537
|
+
Parameters
|
538
|
+
----------
|
539
|
+
drawdowns : ndarray of shape (n_observations,)
|
540
|
+
Vector of drawdowns.
|
541
|
+
|
542
|
+
beta : float, default=0.95
|
543
|
+
The EDaR confidence level.
|
544
|
+
|
545
|
+
Returns
|
546
|
+
-------
|
547
|
+
value : float
|
548
|
+
EDaR.
|
549
|
+
"""
|
550
|
+
return evar(returns=drawdowns, beta=beta)
|
551
|
+
|
552
|
+
|
553
|
+
def ulcer_index(drawdowns: np.ndarray) -> float:
|
554
|
+
"""Compute the Ulcer index.
|
555
|
+
|
556
|
+
Parameters
|
557
|
+
----------
|
558
|
+
drawdowns : ndarray of shape (n_observations,)
|
559
|
+
Vector of drawdowns.
|
560
|
+
|
561
|
+
Returns
|
562
|
+
-------
|
563
|
+
value : float
|
564
|
+
Ulcer index.
|
565
|
+
"""
|
566
|
+
return np.sqrt(np.sum(np.power(drawdowns, 2)) / len(drawdowns))
|
567
|
+
|
568
|
+
|
569
|
+
def owa_gmd_weights(n_observations: int) -> np.ndarray:
|
570
|
+
"""Compute the OWA weights used for the Gini mean difference (GMD) computation.
|
571
|
+
|
572
|
+
Parameters
|
573
|
+
----------
|
574
|
+
n_observations : int
|
575
|
+
Number of observations.
|
576
|
+
|
577
|
+
Returns
|
578
|
+
-------
|
579
|
+
value : float
|
580
|
+
OWA GMD weights.
|
581
|
+
"""
|
582
|
+
return (4 * np.arange(1, n_observations + 1) - 2 * (n_observations + 1)) / (
|
583
|
+
n_observations * (n_observations - 1)
|
584
|
+
)
|
585
|
+
|
586
|
+
|
587
|
+
def gini_mean_difference(returns: np.ndarray) -> float:
|
588
|
+
"""Compute the Gini mean difference (GMD).
|
589
|
+
|
590
|
+
The GMD is the expected absolute difference between two realisations.
|
591
|
+
The GMD is a superior measure of variability for non-normal distribution than the
|
592
|
+
variance.
|
593
|
+
It can be used to form necessary conditions for second-degree stochastic dominance,
|
594
|
+
while the variance cannot.
|
595
|
+
|
596
|
+
Parameters
|
597
|
+
----------
|
598
|
+
returns : ndarray of shape (n_observations,)
|
599
|
+
Vector of returns.
|
600
|
+
|
601
|
+
Returns
|
602
|
+
-------
|
603
|
+
value : float
|
604
|
+
Gini mean difference.
|
605
|
+
"""
|
606
|
+
w = owa_gmd_weights(len(returns))
|
607
|
+
return float(w @ np.sort(returns, axis=0))
|