riskfolio-lib 7.2.0__cp313-cp313-macosx_10_13_x86_64.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.
- riskfolio/__init__.py +14 -0
- riskfolio/external/__init__.py +10 -0
- riskfolio/external/cppfunctions.py +376 -0
- riskfolio/external/functions.cpython-313-darwin.so +0 -0
- riskfolio/src/AuxFunctions.py +1488 -0
- riskfolio/src/ConstraintsFunctions.py +2210 -0
- riskfolio/src/DBHT.py +1089 -0
- riskfolio/src/GerberStatistic.py +240 -0
- riskfolio/src/HCPortfolio.py +1102 -0
- riskfolio/src/OwaWeights.py +433 -0
- riskfolio/src/ParamsEstimation.py +1989 -0
- riskfolio/src/PlotFunctions.py +5052 -0
- riskfolio/src/Portfolio.py +6164 -0
- riskfolio/src/Reports.py +692 -0
- riskfolio/src/RiskFunctions.py +3195 -0
- riskfolio/src/__init__.py +20 -0
- riskfolio/version.py +4 -0
- riskfolio_lib-7.2.0.dist-info/LICENSE.txt +27 -0
- riskfolio_lib-7.2.0.dist-info/METADATA +386 -0
- riskfolio_lib-7.2.0.dist-info/RECORD +22 -0
- riskfolio_lib-7.2.0.dist-info/WHEEL +6 -0
- riskfolio_lib-7.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,3195 @@
|
|
|
1
|
+
"""""" #
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Copyright (c) 2020-2026, Dany Cajas
|
|
5
|
+
All rights reserved.
|
|
6
|
+
This work is licensed under BSD 3-Clause "New" or "Revised" License.
|
|
7
|
+
License available at https://github.com/dcajasn/Riskfolio-Lib/blob/master/LICENSE.txt
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import cvxpy as cp
|
|
13
|
+
import riskfolio.src.OwaWeights as owa
|
|
14
|
+
import riskfolio.src.ParamsEstimation as pe
|
|
15
|
+
from scipy.optimize import minimize
|
|
16
|
+
from scipy.optimize import Bounds
|
|
17
|
+
from scipy.linalg import null_space
|
|
18
|
+
from numpy.linalg import pinv
|
|
19
|
+
from sklearn.decomposition import PCA
|
|
20
|
+
from sklearn.preprocessing import StandardScaler
|
|
21
|
+
import warnings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"MAD",
|
|
26
|
+
"SemiDeviation",
|
|
27
|
+
"Kurtosis",
|
|
28
|
+
"SemiKurtosis",
|
|
29
|
+
"VaR_Hist",
|
|
30
|
+
"CVaR_Hist",
|
|
31
|
+
"WR",
|
|
32
|
+
"LPM",
|
|
33
|
+
"Entropic_RM",
|
|
34
|
+
"EVaR_Hist",
|
|
35
|
+
"RLVaR_Hist",
|
|
36
|
+
"MDD_Abs",
|
|
37
|
+
"ADD_Abs",
|
|
38
|
+
"DaR_Abs",
|
|
39
|
+
"CDaR_Abs",
|
|
40
|
+
"EDaR_Abs",
|
|
41
|
+
"RLDaR_Abs",
|
|
42
|
+
"UCI_Abs",
|
|
43
|
+
"MDD_Rel",
|
|
44
|
+
"ADD_Rel",
|
|
45
|
+
"DaR_Rel",
|
|
46
|
+
"CDaR_Rel",
|
|
47
|
+
"EDaR_Rel",
|
|
48
|
+
"RLDaR_Rel",
|
|
49
|
+
"UCI_Rel",
|
|
50
|
+
"GMD",
|
|
51
|
+
"TG",
|
|
52
|
+
"RG",
|
|
53
|
+
"VRG",
|
|
54
|
+
"CVRG",
|
|
55
|
+
"TGRG",
|
|
56
|
+
"EVRG",
|
|
57
|
+
"RVRG",
|
|
58
|
+
"L_Moment",
|
|
59
|
+
"L_Moment_CRM",
|
|
60
|
+
"NEA",
|
|
61
|
+
"Sharpe_Risk",
|
|
62
|
+
"Sharpe",
|
|
63
|
+
"Risk_Contribution",
|
|
64
|
+
"Risk_Margin",
|
|
65
|
+
"Factors_Risk_Contribution",
|
|
66
|
+
"BrinsonAttribution",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def MAD(X):
|
|
71
|
+
r"""
|
|
72
|
+
Calculate the Mean Absolute Deviation (MAD) of a returns series.
|
|
73
|
+
|
|
74
|
+
.. math::
|
|
75
|
+
\text{MAD}(X) = \frac{1}{T}\sum_{t=1}^{T}
|
|
76
|
+
| X_{t} - \mathbb{E}(X_{t}) |
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
X : 1d-array
|
|
81
|
+
Returns series, must have Tx1 size.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
value : float
|
|
86
|
+
MAD of a returns series.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
a = np.array(X, ndmin=2)
|
|
91
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
92
|
+
a = a.T
|
|
93
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
94
|
+
raise ValueError("returns must have Tx1 size")
|
|
95
|
+
|
|
96
|
+
T, N = a.shape
|
|
97
|
+
mu = np.mean(a, axis=0).reshape(1, -1)
|
|
98
|
+
mu = np.repeat(mu, T, axis=0)
|
|
99
|
+
value = a - mu
|
|
100
|
+
value = np.mean(np.absolute(value), axis=0)
|
|
101
|
+
value = np.array(value).item()
|
|
102
|
+
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def SemiDeviation(X):
|
|
107
|
+
r"""
|
|
108
|
+
Calculate the Semi Deviation of a returns series.
|
|
109
|
+
|
|
110
|
+
.. math::
|
|
111
|
+
\text{SemiDev}(X) = \left [ \frac{1}{T-1}\sum_{t=1}^{T}
|
|
112
|
+
\min (X_{t} - \mathbb{E}(X_{t}), 0)^2 \right ]^{1/2}
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
X : 1d-array
|
|
117
|
+
Returns series, must have Tx1 size.
|
|
118
|
+
|
|
119
|
+
Raises
|
|
120
|
+
------
|
|
121
|
+
ValueError
|
|
122
|
+
When the value cannot be calculated.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
value : float
|
|
127
|
+
Semi Deviation of a returns series.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
a = np.array(X, ndmin=2)
|
|
131
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
132
|
+
a = a.T
|
|
133
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
134
|
+
raise ValueError("returns must have Tx1 size")
|
|
135
|
+
|
|
136
|
+
T, N = a.shape
|
|
137
|
+
mu = np.mean(a, axis=0).reshape(1, -1)
|
|
138
|
+
mu = np.repeat(mu, T, axis=0)
|
|
139
|
+
value = mu - a
|
|
140
|
+
value = np.sum(np.power(value[np.where(value >= 0)], 2)) / (T - 1)
|
|
141
|
+
value = np.power(value, 0.5).item()
|
|
142
|
+
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def Kurtosis(X):
|
|
147
|
+
r"""
|
|
148
|
+
Calculate the Square Root Kurtosis of a returns series.
|
|
149
|
+
|
|
150
|
+
.. math::
|
|
151
|
+
\text{Kurt}(X) = \left [ \frac{1}{T}\sum_{t=1}^{T}
|
|
152
|
+
(X_{t} - \mathbb{E}(X_{t}))^{4} \right ]^{1/2}
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
X : 1d-array
|
|
157
|
+
Returns series, must have Tx1 size.
|
|
158
|
+
|
|
159
|
+
Raises
|
|
160
|
+
------
|
|
161
|
+
ValueError
|
|
162
|
+
When the value cannot be calculated.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
value : float
|
|
167
|
+
Square Root Kurtosis of a returns series.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
a = np.array(X, ndmin=2)
|
|
171
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
172
|
+
a = a.T
|
|
173
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
174
|
+
raise ValueError("returns must have Tx1 size")
|
|
175
|
+
|
|
176
|
+
T, N = a.shape
|
|
177
|
+
mu = np.mean(a, axis=0).reshape(1, -1)
|
|
178
|
+
mu = np.repeat(mu, T, axis=0)
|
|
179
|
+
value = mu - a
|
|
180
|
+
value = np.sum(np.power(value, 4)) / T
|
|
181
|
+
value = np.power(value, 0.5).item()
|
|
182
|
+
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def SemiKurtosis(X):
|
|
187
|
+
r"""
|
|
188
|
+
Calculate the Semi Square Root Kurtosis of a returns series.
|
|
189
|
+
|
|
190
|
+
.. math::
|
|
191
|
+
\text{SemiKurt}(X) = \left [ \frac{1}{T}\sum_{t=1}^{T}
|
|
192
|
+
\min (X_{t} - \mathbb{E}(X_{t}), 0)^{4} \right ]^{1/2}
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
X : 1d-array
|
|
197
|
+
Returns series, must have Tx1 size.
|
|
198
|
+
|
|
199
|
+
Raises
|
|
200
|
+
------
|
|
201
|
+
ValueError
|
|
202
|
+
When the value cannot be calculated.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
value : float
|
|
207
|
+
Semi Square Root Kurtosis of a returns series.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
a = np.array(X, ndmin=2)
|
|
211
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
212
|
+
a = a.T
|
|
213
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
214
|
+
raise ValueError("returns must have Tx1 size")
|
|
215
|
+
|
|
216
|
+
T, N = a.shape
|
|
217
|
+
mu = np.mean(a, axis=0).reshape(1, -1)
|
|
218
|
+
mu = np.repeat(mu, T, axis=0)
|
|
219
|
+
value = mu - a
|
|
220
|
+
value = np.sum(np.power(value[np.where(value >= 0)], 4)) / T
|
|
221
|
+
value = np.power(value, 0.5).item()
|
|
222
|
+
|
|
223
|
+
return value
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def VaR_Hist(X, alpha=0.05):
|
|
227
|
+
r"""
|
|
228
|
+
Calculate the Value at Risk (VaR) of a returns series.
|
|
229
|
+
|
|
230
|
+
.. math::
|
|
231
|
+
\text{VaR}_{\alpha}(X) = -\inf_{t \in (0,T)} \left \{ X_{t} \in
|
|
232
|
+
\mathbb{R}: F_{X}(X_{t})>\alpha \right \}
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
X : 1d-array
|
|
237
|
+
Returns series, must have Tx1 size.
|
|
238
|
+
alpha : float, optional
|
|
239
|
+
Significance level of VaR. The default is 0.05.
|
|
240
|
+
Raises
|
|
241
|
+
------
|
|
242
|
+
ValueError
|
|
243
|
+
When the value cannot be calculated.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
value : float
|
|
248
|
+
VaR of a returns series.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
a = np.array(X, ndmin=2)
|
|
252
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
253
|
+
a = a.T
|
|
254
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
255
|
+
raise ValueError("returns must have Tx1 size")
|
|
256
|
+
|
|
257
|
+
sorted_a = np.sort(a, axis=0)
|
|
258
|
+
index = int(np.ceil(alpha * len(sorted_a)) - 1)
|
|
259
|
+
value = -sorted_a[index]
|
|
260
|
+
value = np.array(value).item()
|
|
261
|
+
|
|
262
|
+
return value
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def CVaR_Hist(X, alpha=0.05):
|
|
266
|
+
r"""
|
|
267
|
+
Calculate the Conditional Value at Risk (CVaR) of a returns series.
|
|
268
|
+
|
|
269
|
+
.. math::
|
|
270
|
+
\text{CVaR}_{\alpha}(X) = \text{VaR}_{\alpha}(X) +
|
|
271
|
+
\frac{1}{\alpha T} \sum_{t=1}^{T} \max(-X_{t} -
|
|
272
|
+
\text{VaR}_{\alpha}(X), 0)
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
X : 1d-array
|
|
277
|
+
Returns series, must have Tx1 size.
|
|
278
|
+
alpha : float, optional
|
|
279
|
+
Significance level of CVaR. The default is 0.05.
|
|
280
|
+
|
|
281
|
+
Raises
|
|
282
|
+
------
|
|
283
|
+
ValueError
|
|
284
|
+
When the value cannot be calculated.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
value : float
|
|
289
|
+
CVaR of a returns series.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
a = np.array(X, ndmin=2)
|
|
293
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
294
|
+
a = a.T
|
|
295
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
296
|
+
raise ValueError("returns must have Tx1 size")
|
|
297
|
+
|
|
298
|
+
sorted_a = np.sort(a, axis=0)
|
|
299
|
+
index = int(np.ceil(alpha * len(sorted_a)) - 1)
|
|
300
|
+
sum_var = 0
|
|
301
|
+
for i in range(0, index + 1):
|
|
302
|
+
sum_var = sum_var + sorted_a[i] - sorted_a[index]
|
|
303
|
+
|
|
304
|
+
value = -sorted_a[index] - sum_var / (alpha * len(sorted_a))
|
|
305
|
+
value = np.array(value).item()
|
|
306
|
+
|
|
307
|
+
return value
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def WR(X):
|
|
311
|
+
r"""
|
|
312
|
+
Calculate the Worst Realization (WR) or Worst Scenario of a returns series.
|
|
313
|
+
|
|
314
|
+
.. math::
|
|
315
|
+
\text{WR}(X) = \max(-X)
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
X : 1d-array
|
|
320
|
+
Returns series, must have Tx1 size.
|
|
321
|
+
|
|
322
|
+
Raises
|
|
323
|
+
------
|
|
324
|
+
ValueError
|
|
325
|
+
When the value cannot be calculated.
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
value : float
|
|
330
|
+
WR of a returns series.
|
|
331
|
+
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
a = np.array(X, ndmin=2)
|
|
335
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
336
|
+
a = a.T
|
|
337
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
338
|
+
raise ValueError("returns must have Tx1 size")
|
|
339
|
+
|
|
340
|
+
sorted_a = np.sort(a, axis=0)
|
|
341
|
+
value = -sorted_a[0]
|
|
342
|
+
value = np.array(value).item()
|
|
343
|
+
|
|
344
|
+
return value
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def LPM(X, MAR=0, p=1):
|
|
348
|
+
r"""
|
|
349
|
+
Calculate the First or Second Lower Partial Moment of a returns series.
|
|
350
|
+
|
|
351
|
+
.. math::
|
|
352
|
+
\text{LPM}(X, \text{MAR}, 1) &= \frac{1}{T}\sum_{t=1}^{T}
|
|
353
|
+
\max(\text{MAR} - X_{t}, 0) \\
|
|
354
|
+
\text{LPM}(X, \text{MAR}, 2) &= \left [ \frac{1}{T-1}\sum_{t=1}^{T}
|
|
355
|
+
\max(\text{MAR} - X_{t}, 0)^{2} \right ]^{\frac{1}{2}} \\
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
Where:
|
|
359
|
+
|
|
360
|
+
:math:`\text{MAR}` is the minimum acceptable return.
|
|
361
|
+
:math:`p` is the order of the :math:`\text{LPM}`.
|
|
362
|
+
|
|
363
|
+
Parameters
|
|
364
|
+
----------
|
|
365
|
+
X : 1d-array
|
|
366
|
+
Returns series, must have Tx1 size.
|
|
367
|
+
MAR : float, optional
|
|
368
|
+
Minimum acceptable return. The default is 0.
|
|
369
|
+
p : float, optional can be {1,2}
|
|
370
|
+
order of the :math:`\text{LPM}`. The default is 1.
|
|
371
|
+
|
|
372
|
+
Raises
|
|
373
|
+
------
|
|
374
|
+
ValueError
|
|
375
|
+
When the value cannot be calculated.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
value : float
|
|
380
|
+
p-th Lower Partial Moment of a returns series.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
a = np.array(X, ndmin=2)
|
|
385
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
386
|
+
a = a.T
|
|
387
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
388
|
+
raise ValueError("returns must have Tx1 size")
|
|
389
|
+
if p not in [1, 2]:
|
|
390
|
+
raise ValueError("p can only be 1 or 2")
|
|
391
|
+
|
|
392
|
+
value = MAR - a
|
|
393
|
+
|
|
394
|
+
if p == 2:
|
|
395
|
+
n = value.shape[0] - 1
|
|
396
|
+
else:
|
|
397
|
+
n = value.shape[0]
|
|
398
|
+
|
|
399
|
+
value = np.sum(np.power(value[np.where(value >= 0)], p)) / n
|
|
400
|
+
value = np.power(value, 1 / p).item()
|
|
401
|
+
|
|
402
|
+
return value
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def Entropic_RM(X, z=1, alpha=0.05):
|
|
406
|
+
r"""
|
|
407
|
+
Calculate the Entropic Risk Measure (ERM) of a returns series.
|
|
408
|
+
|
|
409
|
+
.. math::
|
|
410
|
+
\text{ERM}_{\alpha}(X) = z\ln \left (\frac{M_X(z^{-1})}{\alpha} \right )
|
|
411
|
+
|
|
412
|
+
Where:
|
|
413
|
+
|
|
414
|
+
:math:`M_X(z)` is the moment generating function of X.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
X : 1d-array
|
|
419
|
+
Returns series, must have Tx1 size.
|
|
420
|
+
z : float, optional
|
|
421
|
+
Risk aversion parameter, must be greater than zero. The default is 1.
|
|
422
|
+
alpha : float, optional
|
|
423
|
+
Significance level of EVaR. The default is 0.05.
|
|
424
|
+
|
|
425
|
+
Raises
|
|
426
|
+
------
|
|
427
|
+
ValueError
|
|
428
|
+
When the value cannot be calculated.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
value : float
|
|
433
|
+
ERM of a returns series.
|
|
434
|
+
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
a = np.array(X, ndmin=2)
|
|
438
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
439
|
+
a = a.T
|
|
440
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
441
|
+
raise ValueError("returns must have Tx1 size")
|
|
442
|
+
|
|
443
|
+
value = np.mean(np.exp(-1 / z * a), axis=0)
|
|
444
|
+
value = z * (np.log(value) + np.log(1 / alpha))
|
|
445
|
+
value = np.array(value).item()
|
|
446
|
+
|
|
447
|
+
return value
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _Entropic_RM(z, X, alpha=0.05):
|
|
451
|
+
a = np.array(X, ndmin=2)
|
|
452
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
453
|
+
a = a.T
|
|
454
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
455
|
+
raise ValueError("returns must have Tx1 size")
|
|
456
|
+
|
|
457
|
+
a = a.flatten()
|
|
458
|
+
value = np.mean(np.exp(-1 / z * a), axis=0)
|
|
459
|
+
value = z * (np.log(value) + np.log(1 / alpha))
|
|
460
|
+
value = np.array(value).item()
|
|
461
|
+
|
|
462
|
+
return value
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def EVaR_Hist(X, alpha=0.05, solver="CLARABEL"):
|
|
466
|
+
r"""
|
|
467
|
+
Calculate the Entropic Value at Risk (EVaR) of a returns series.
|
|
468
|
+
|
|
469
|
+
.. math::
|
|
470
|
+
\text{EVaR}_{\alpha}(X) = \inf_{z>0} \left \{ z
|
|
471
|
+
\ln \left (\frac{M_X(z^{-1})}{\alpha} \right ) \right \}
|
|
472
|
+
|
|
473
|
+
Where:
|
|
474
|
+
|
|
475
|
+
:math:`M_X(t)` is the moment generating function of X.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
X : 1d-array
|
|
480
|
+
Returns series, must have Tx1 size.
|
|
481
|
+
alpha : float, optional
|
|
482
|
+
Significance level of EVaR. The default is 0.05.
|
|
483
|
+
solver: str, optional
|
|
484
|
+
Solver available for CVXPY that supports exponential cone programming.
|
|
485
|
+
Used to calculate EVaR, EVRG and EDaR. The default value is 'CLARABEL'.
|
|
486
|
+
|
|
487
|
+
Raises
|
|
488
|
+
------
|
|
489
|
+
ValueError
|
|
490
|
+
When the value cannot be calculated.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
(value, z) : tuple
|
|
495
|
+
EVaR of a returns series and value of z that minimize EVaR.
|
|
496
|
+
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
solvers = ["CLARABEL", "MOSEK", "COPT", "SCS", "ECOS"]
|
|
500
|
+
if solver not in solvers:
|
|
501
|
+
raise ValueError("Only solvers that support exponential cone are allowed")
|
|
502
|
+
else:
|
|
503
|
+
solvers.remove(solver)
|
|
504
|
+
solvers.insert(0, solver)
|
|
505
|
+
|
|
506
|
+
a = np.array(X, ndmin=2)
|
|
507
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
508
|
+
a = a.T
|
|
509
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
510
|
+
raise ValueError("returns must have Tx1 size")
|
|
511
|
+
|
|
512
|
+
T, N = a.shape
|
|
513
|
+
|
|
514
|
+
# Primal Formulation
|
|
515
|
+
t = cp.Variable((1, 1))
|
|
516
|
+
z = cp.Variable((1, 1), nonneg=True)
|
|
517
|
+
ui = cp.Variable((T, 1))
|
|
518
|
+
ones = np.ones((T, 1))
|
|
519
|
+
|
|
520
|
+
constraints = [
|
|
521
|
+
cp.sum(ui) <= z,
|
|
522
|
+
cp.ExpCone(-a - t, ones @ z, ui),
|
|
523
|
+
]
|
|
524
|
+
|
|
525
|
+
risk = t + z * np.log(1 / (alpha * T))
|
|
526
|
+
objective = cp.Minimize(risk * 1000)
|
|
527
|
+
prob = cp.Problem(objective, constraints)
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
for solver_i in solvers:
|
|
531
|
+
prob.solve(solver=solver_i)
|
|
532
|
+
if risk.value is not None:
|
|
533
|
+
break
|
|
534
|
+
except:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
if risk.value is None:
|
|
538
|
+
value = None
|
|
539
|
+
else:
|
|
540
|
+
value = risk.value.item()
|
|
541
|
+
t = z.value.item()
|
|
542
|
+
|
|
543
|
+
if value is None:
|
|
544
|
+
warnings.filterwarnings("ignore")
|
|
545
|
+
|
|
546
|
+
# Primal Formulation with Scipy
|
|
547
|
+
bnd = Bounds([1e-24], [np.inf])
|
|
548
|
+
result = minimize(
|
|
549
|
+
_Entropic_RM, [1], args=(X, alpha), method="SLSQP", bounds=bnd, tol=1e-12
|
|
550
|
+
)
|
|
551
|
+
t = result.x
|
|
552
|
+
t = t.item()
|
|
553
|
+
value = _Entropic_RM(t, X, alpha)
|
|
554
|
+
|
|
555
|
+
return (value, t)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def RLVaR_Hist(X, alpha=0.05, kappa=0.3, solver="CLARABEL"):
|
|
559
|
+
r"""
|
|
560
|
+
Calculate the Relativistic Value at Risk (RLVaR) of a returns series.
|
|
561
|
+
I recommend only use this function with MOSEK solver.
|
|
562
|
+
|
|
563
|
+
.. math::
|
|
564
|
+
\text{RLVaR}^{\kappa}_{\alpha}(X) & = \left \{
|
|
565
|
+
\begin{array}{ll}
|
|
566
|
+
\underset{z, t, \psi, \theta, \varepsilon, \omega}{\text{inf}} & t + z \ln_{\kappa} \left ( \frac{1}{\alpha T} \right ) + \sum^T_{i=1} \left ( \psi_{i} + \theta_{i} \right ) \\
|
|
567
|
+
\text{s.t.} & -X - t + \varepsilon + \omega \leq 0\\
|
|
568
|
+
& z \geq 0 \\
|
|
569
|
+
& \left ( z \left ( \frac{1+\kappa}{2\kappa} \right ), \psi_{i} \left ( \frac{1+\kappa}{\kappa} \right ), \varepsilon_{i} \right) \in \mathcal{P}_3^{1/(1+\kappa),\, \kappa/(1+\kappa)} \\
|
|
570
|
+
& \left ( \omega_{i}\left ( \frac{1}{1-\kappa} \right ), \theta_{i}\left ( \frac{1}{\kappa} \right), -z \left ( \frac{1}{2\kappa} \right ) \right ) \in \mathcal{P}_3^{1-\kappa,\, \kappa} \\
|
|
571
|
+
\end{array} \right .
|
|
572
|
+
|
|
573
|
+
Where:
|
|
574
|
+
|
|
575
|
+
:math:`\mathcal{P}_3^{\alpha,\, 1-\alpha}` is the power cone 3D.
|
|
576
|
+
|
|
577
|
+
:math:`\kappa` is the deformation parameter.
|
|
578
|
+
|
|
579
|
+
Parameters
|
|
580
|
+
----------
|
|
581
|
+
X : 1d-array
|
|
582
|
+
Returns series, must have Tx1 size.
|
|
583
|
+
alpha : float, optional
|
|
584
|
+
Significance level of EVaR. The default is 0.05.
|
|
585
|
+
kappa : float, optional
|
|
586
|
+
Deformation parameter of RLVaR, must be between 0 and 1. The default is 0.3.
|
|
587
|
+
solver: str, optional
|
|
588
|
+
Solver available for CVXPY that supports power cone programming. Used
|
|
589
|
+
to calculate RLVaR and RLDaR. The default value is 'CLARABEL'.
|
|
590
|
+
|
|
591
|
+
Raises
|
|
592
|
+
------
|
|
593
|
+
ValueError
|
|
594
|
+
When the value cannot be calculated.
|
|
595
|
+
|
|
596
|
+
Returns
|
|
597
|
+
-------
|
|
598
|
+
value : tuple
|
|
599
|
+
RLVaR of a returns series.
|
|
600
|
+
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
solvers = ["CLARABEL", "MOSEK", "SCS"]
|
|
604
|
+
if solver not in solvers:
|
|
605
|
+
raise ValueError("Only solvers that support 3D-power cone are allowed")
|
|
606
|
+
else:
|
|
607
|
+
solvers.remove(solver)
|
|
608
|
+
solvers.insert(0, solver)
|
|
609
|
+
|
|
610
|
+
a = np.array(X * 100, ndmin=2)
|
|
611
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
612
|
+
a = a.T
|
|
613
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
614
|
+
raise ValueError("returns must have Tx1 size")
|
|
615
|
+
|
|
616
|
+
T, N = a.shape
|
|
617
|
+
|
|
618
|
+
# Dual Formulation
|
|
619
|
+
Z = cp.Variable((T, 1))
|
|
620
|
+
nu = cp.Variable((T, 1))
|
|
621
|
+
tau = cp.Variable((T, 1))
|
|
622
|
+
ones = np.ones((T, 1))
|
|
623
|
+
|
|
624
|
+
c = ((1 / (alpha * T)) ** kappa - (1 / (alpha * T)) ** (-kappa)) / (2 * kappa)
|
|
625
|
+
|
|
626
|
+
constraints = [
|
|
627
|
+
cp.sum(Z) == 1,
|
|
628
|
+
cp.sum(nu - tau) / (2 * kappa) <= c,
|
|
629
|
+
cp.PowCone3D(nu, ones, Z, 1 / (1 + kappa)),
|
|
630
|
+
cp.PowCone3D(Z, ones, tau, 1 - kappa),
|
|
631
|
+
]
|
|
632
|
+
risk = Z.T @ (-a)
|
|
633
|
+
|
|
634
|
+
objective = cp.Maximize(risk)
|
|
635
|
+
prob = cp.Problem(objective, constraints)
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
for solver_i in solvers:
|
|
639
|
+
prob.solve(solver=solver_i)
|
|
640
|
+
if risk.value is not None:
|
|
641
|
+
break
|
|
642
|
+
except:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
if risk.value is None:
|
|
646
|
+
value = None
|
|
647
|
+
else:
|
|
648
|
+
value = risk.value.item()
|
|
649
|
+
|
|
650
|
+
if value is None:
|
|
651
|
+
# Primal Formulation
|
|
652
|
+
t = cp.Variable((1, 1))
|
|
653
|
+
z = cp.Variable((1, 1))
|
|
654
|
+
omega = cp.Variable((T, 1))
|
|
655
|
+
psi = cp.Variable((T, 1))
|
|
656
|
+
theta = cp.Variable((T, 1))
|
|
657
|
+
nu = cp.Variable((T, 1))
|
|
658
|
+
|
|
659
|
+
ones = np.ones((T, 1))
|
|
660
|
+
constraints = [
|
|
661
|
+
cp.PowCone3D(
|
|
662
|
+
z * (1 + kappa) / (2 * kappa) * ones,
|
|
663
|
+
psi * (1 + kappa) / kappa,
|
|
664
|
+
nu,
|
|
665
|
+
1 / (1 + kappa),
|
|
666
|
+
),
|
|
667
|
+
cp.PowCone3D(
|
|
668
|
+
omega / (1 - kappa), theta / kappa, -z / (2 * kappa) * ones, (1 - kappa)
|
|
669
|
+
),
|
|
670
|
+
-a - t + nu + omega <= 0,
|
|
671
|
+
z >= 0,
|
|
672
|
+
]
|
|
673
|
+
|
|
674
|
+
c = ((1 / (alpha * T)) ** kappa - (1 / (alpha * T)) ** (-kappa)) / (2 * kappa)
|
|
675
|
+
risk = t + c * z + cp.sum(psi + theta)
|
|
676
|
+
|
|
677
|
+
objective = cp.Minimize(risk * 1000)
|
|
678
|
+
prob = cp.Problem(objective, constraints)
|
|
679
|
+
|
|
680
|
+
try:
|
|
681
|
+
for solver_i in solvers:
|
|
682
|
+
prob.solve(solver=solver_i)
|
|
683
|
+
if risk.value is not None:
|
|
684
|
+
break
|
|
685
|
+
except:
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
if risk.value is None:
|
|
689
|
+
value = 0
|
|
690
|
+
else:
|
|
691
|
+
value = risk.value.item()
|
|
692
|
+
|
|
693
|
+
return value / 100
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def MDD_Abs(X):
|
|
697
|
+
r"""
|
|
698
|
+
Calculate the Maximum Drawdown (MDD) of a returns series
|
|
699
|
+
using uncompounded cumulative returns.
|
|
700
|
+
|
|
701
|
+
.. math::
|
|
702
|
+
\text{MDD}(X) = \max_{j \in (0,T)} \left [\max_{t \in (0,j)}
|
|
703
|
+
\left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} \right ]
|
|
704
|
+
|
|
705
|
+
Parameters
|
|
706
|
+
----------
|
|
707
|
+
X : 1d-array
|
|
708
|
+
Returns series, must have Tx1 size.
|
|
709
|
+
|
|
710
|
+
Raises
|
|
711
|
+
------
|
|
712
|
+
ValueError
|
|
713
|
+
When the value cannot be calculated.
|
|
714
|
+
|
|
715
|
+
Returns
|
|
716
|
+
-------
|
|
717
|
+
value : float
|
|
718
|
+
MDD of an uncompounded cumulative returns.
|
|
719
|
+
|
|
720
|
+
"""
|
|
721
|
+
|
|
722
|
+
a = np.array(X, ndmin=2)
|
|
723
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
724
|
+
a = a.T
|
|
725
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
726
|
+
raise ValueError("returns must have Tx1 size")
|
|
727
|
+
|
|
728
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
729
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
730
|
+
value = 0
|
|
731
|
+
peak = -99999
|
|
732
|
+
for i in NAV:
|
|
733
|
+
if i > peak:
|
|
734
|
+
peak = i
|
|
735
|
+
DD = peak - i
|
|
736
|
+
if DD > value:
|
|
737
|
+
value = DD
|
|
738
|
+
|
|
739
|
+
value = np.array(value).item()
|
|
740
|
+
|
|
741
|
+
return value
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def ADD_Abs(X):
|
|
745
|
+
r"""
|
|
746
|
+
Calculate the Average Drawdown (ADD) of a returns series
|
|
747
|
+
using uncompounded cumulative returns.
|
|
748
|
+
|
|
749
|
+
.. math::
|
|
750
|
+
\text{ADD}(X) = \frac{1}{T}\sum_{j=0}^{T}\left [ \max_{t \in (0,j)}
|
|
751
|
+
\left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i} \right ]
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
X : 1d-array
|
|
756
|
+
Returns series, must have Tx1 size.
|
|
757
|
+
|
|
758
|
+
Raises
|
|
759
|
+
------
|
|
760
|
+
ValueError
|
|
761
|
+
When the value cannot be calculated.
|
|
762
|
+
|
|
763
|
+
Returns
|
|
764
|
+
-------
|
|
765
|
+
value : float
|
|
766
|
+
ADD of an uncompounded cumulative returns.
|
|
767
|
+
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
a = np.array(X, ndmin=2)
|
|
771
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
772
|
+
a = a.T
|
|
773
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
774
|
+
raise ValueError("returns must have Tx1 size")
|
|
775
|
+
|
|
776
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
777
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
778
|
+
value = 0
|
|
779
|
+
peak = -99999
|
|
780
|
+
n = 0
|
|
781
|
+
for i in NAV:
|
|
782
|
+
if i > peak:
|
|
783
|
+
peak = i
|
|
784
|
+
DD = peak - i
|
|
785
|
+
if DD > 0:
|
|
786
|
+
value += DD
|
|
787
|
+
n += 1
|
|
788
|
+
if n == 0:
|
|
789
|
+
value = 0
|
|
790
|
+
else:
|
|
791
|
+
value = value / (n - 1)
|
|
792
|
+
|
|
793
|
+
value = np.array(value).item()
|
|
794
|
+
|
|
795
|
+
return value
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def DaR_Abs(X, alpha=0.05):
|
|
799
|
+
r"""
|
|
800
|
+
Calculate the Drawdown at Risk (DaR) of a returns series
|
|
801
|
+
using uncompounded cumulative returns.
|
|
802
|
+
|
|
803
|
+
.. math::
|
|
804
|
+
\text{DaR}_{\alpha}(X) & = \max_{j \in (0,T)} \left \{ \text{DD}(X,j)
|
|
805
|
+
\in \mathbb{R}: F_{\text{DD}} \left ( \text{DD}(X,j) \right )< 1-\alpha
|
|
806
|
+
\right \} \\
|
|
807
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i}
|
|
808
|
+
\right )- \sum_{i=0}^{j}X_{i}
|
|
809
|
+
|
|
810
|
+
Parameters
|
|
811
|
+
----------
|
|
812
|
+
X : 1d-array
|
|
813
|
+
Returns series, must have Tx1 size..
|
|
814
|
+
alpha : float, optional
|
|
815
|
+
Significance level of DaR. The default is 0.05.
|
|
816
|
+
|
|
817
|
+
Raises
|
|
818
|
+
------
|
|
819
|
+
ValueError
|
|
820
|
+
When the value cannot be calculated.
|
|
821
|
+
|
|
822
|
+
Returns
|
|
823
|
+
-------
|
|
824
|
+
value : float
|
|
825
|
+
DaR of an uncompounded cumulative returns series.
|
|
826
|
+
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
a = np.array(X, ndmin=2)
|
|
830
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
831
|
+
a = a.T
|
|
832
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
833
|
+
raise ValueError("returns must have Tx1 size")
|
|
834
|
+
|
|
835
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
836
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
837
|
+
DD = []
|
|
838
|
+
peak = -99999
|
|
839
|
+
for i in NAV:
|
|
840
|
+
if i > peak:
|
|
841
|
+
peak = i
|
|
842
|
+
DD.append(-(peak - i))
|
|
843
|
+
del DD[0]
|
|
844
|
+
sorted_DD = np.sort(np.array(DD), axis=0)
|
|
845
|
+
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
|
|
846
|
+
value = -sorted_DD[index]
|
|
847
|
+
value = np.array(value).item()
|
|
848
|
+
|
|
849
|
+
return value
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def CDaR_Abs(X, alpha=0.05):
|
|
853
|
+
r"""
|
|
854
|
+
Calculate the Conditional Drawdown at Risk (CDaR) of a returns series
|
|
855
|
+
using uncompounded cumulative returns.
|
|
856
|
+
|
|
857
|
+
.. math::
|
|
858
|
+
\text{CDaR}_{\alpha}(X) = \text{DaR}_{\alpha}(X) + \frac{1}{\alpha T}
|
|
859
|
+
\sum_{j=0}^{T} \max \left [ \max_{t \in (0,j)}
|
|
860
|
+
\left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i}
|
|
861
|
+
- \text{DaR}_{\alpha}(X), 0 \right ]
|
|
862
|
+
|
|
863
|
+
Where:
|
|
864
|
+
|
|
865
|
+
:math:`\text{DaR}_{\alpha}` is the Drawdown at Risk of an uncompounded
|
|
866
|
+
cumulated return series :math:`X`.
|
|
867
|
+
|
|
868
|
+
Parameters
|
|
869
|
+
----------
|
|
870
|
+
X : 1d-array
|
|
871
|
+
Returns series, must have Tx1 size..
|
|
872
|
+
alpha : float, optional
|
|
873
|
+
Significance level of CDaR. The default is 0.05.
|
|
874
|
+
|
|
875
|
+
Raises
|
|
876
|
+
------
|
|
877
|
+
ValueError
|
|
878
|
+
When the value cannot be calculated.
|
|
879
|
+
|
|
880
|
+
Returns
|
|
881
|
+
-------
|
|
882
|
+
value : float
|
|
883
|
+
CDaR of an uncompounded cumulative returns series.
|
|
884
|
+
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
a = np.array(X, ndmin=2)
|
|
888
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
889
|
+
a = a.T
|
|
890
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
891
|
+
raise ValueError("returns must have Tx1 size")
|
|
892
|
+
|
|
893
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
894
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
895
|
+
DD = []
|
|
896
|
+
peak = -99999
|
|
897
|
+
for i in NAV:
|
|
898
|
+
if i > peak:
|
|
899
|
+
peak = i
|
|
900
|
+
DD.append(-(peak - i))
|
|
901
|
+
del DD[0]
|
|
902
|
+
sorted_DD = np.sort(np.array(DD), axis=0)
|
|
903
|
+
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
|
|
904
|
+
sum_var = 0
|
|
905
|
+
for i in range(0, index + 1):
|
|
906
|
+
sum_var = sum_var + sorted_DD[i] - sorted_DD[index]
|
|
907
|
+
value = -sorted_DD[index] - sum_var / (alpha * len(sorted_DD))
|
|
908
|
+
value = np.array(value).item()
|
|
909
|
+
|
|
910
|
+
return value
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def EDaR_Abs(X, alpha=0.05, solver="CLARABEL"):
|
|
914
|
+
r"""
|
|
915
|
+
Calculate the Entropic Drawdown at Risk (EDaR) of a returns series
|
|
916
|
+
using uncompounded cumulative returns.
|
|
917
|
+
|
|
918
|
+
.. math::
|
|
919
|
+
\text{EDaR}_{\alpha}(X) & = \inf_{z>0} \left \{ z
|
|
920
|
+
\ln \left (\frac{M_{\text{DD}(X)}(z^{-1})}{\alpha} \right ) \right \} \\
|
|
921
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i}
|
|
922
|
+
\right )- \sum_{i=0}^{j}X_{i} \\
|
|
923
|
+
|
|
924
|
+
Parameters
|
|
925
|
+
----------
|
|
926
|
+
X : 1d-array
|
|
927
|
+
Returns series, must have Tx1 size..
|
|
928
|
+
alpha : float, optional
|
|
929
|
+
Significance level of EDaR. The default is 0.05.
|
|
930
|
+
|
|
931
|
+
Raises
|
|
932
|
+
------
|
|
933
|
+
ValueError
|
|
934
|
+
When the value cannot be calculated.
|
|
935
|
+
|
|
936
|
+
Returns
|
|
937
|
+
-------
|
|
938
|
+
(value, z) : tuple
|
|
939
|
+
EDaR of an uncompounded cumulative returns series
|
|
940
|
+
and value of z that minimize EDaR.
|
|
941
|
+
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
a = np.array(X, ndmin=2)
|
|
945
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
946
|
+
a = a.T
|
|
947
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
948
|
+
raise ValueError("returns must have Tx1 size")
|
|
949
|
+
|
|
950
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
951
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
952
|
+
DD = []
|
|
953
|
+
peak = -99999
|
|
954
|
+
for i in NAV:
|
|
955
|
+
if i > peak:
|
|
956
|
+
peak = i
|
|
957
|
+
DD.append(-(peak - i))
|
|
958
|
+
del DD[0]
|
|
959
|
+
|
|
960
|
+
(value, t) = EVaR_Hist(np.array(DD), alpha=alpha, solver=solver)
|
|
961
|
+
|
|
962
|
+
return (value, t)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def RLDaR_Abs(X, alpha=0.05, kappa=0.3, solver="CLARABEL"):
|
|
966
|
+
r"""
|
|
967
|
+
Calculate the Relativistic Drawdown at Risk (RLDaR) of a returns series
|
|
968
|
+
using uncompounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
969
|
+
|
|
970
|
+
.. math::
|
|
971
|
+
\text{RLDaR}^{\kappa}_{\alpha}(X) & = \text{RLVaR}^{\kappa}_{\alpha}(\text{DD}(X)) \\
|
|
972
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \sum_{i=0}^{t}X_{i}
|
|
973
|
+
\right )- \sum_{i=0}^{j}X_{i} \\
|
|
974
|
+
|
|
975
|
+
Parameters
|
|
976
|
+
----------
|
|
977
|
+
X : 1d-array
|
|
978
|
+
Returns series, must have Tx1 size.
|
|
979
|
+
alpha : float, optional
|
|
980
|
+
Significance level of EVaR. The default is 0.05.
|
|
981
|
+
kappa : float, optional
|
|
982
|
+
Deformation parameter of RLDaR, must be between 0 and 1. The default is 0.3.
|
|
983
|
+
solver: str, optional
|
|
984
|
+
Solver available for CVXPY that supports power cone programming. Used
|
|
985
|
+
to calculate RLVaR, RVRG and RLDaR. The default value is 'CLARABEL'.
|
|
986
|
+
|
|
987
|
+
Raises
|
|
988
|
+
------
|
|
989
|
+
ValueError
|
|
990
|
+
When the value cannot be calculated.
|
|
991
|
+
|
|
992
|
+
Returns
|
|
993
|
+
-------
|
|
994
|
+
value : tuple
|
|
995
|
+
RLDaR of an uncompounded cumulative returns series.
|
|
996
|
+
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
a = np.array(X, ndmin=2)
|
|
1000
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1001
|
+
a = a.T
|
|
1002
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1003
|
+
raise ValueError("returns must have Tx1 size")
|
|
1004
|
+
|
|
1005
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
1006
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
1007
|
+
DD = []
|
|
1008
|
+
peak = -99999
|
|
1009
|
+
for i in NAV:
|
|
1010
|
+
if i > peak:
|
|
1011
|
+
peak = i
|
|
1012
|
+
DD.append(-(peak - i))
|
|
1013
|
+
del DD[0]
|
|
1014
|
+
|
|
1015
|
+
value = RLVaR_Hist(np.array(DD), alpha=alpha, kappa=kappa, solver=solver)
|
|
1016
|
+
|
|
1017
|
+
return value
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def UCI_Abs(X):
|
|
1021
|
+
r"""
|
|
1022
|
+
Calculate the Ulcer Index (UCI) of a returns series
|
|
1023
|
+
using uncompounded cumulative returns.
|
|
1024
|
+
|
|
1025
|
+
.. math::
|
|
1026
|
+
\text{UCI}(X) =\sqrt{\frac{1}{T}\sum_{j=0}^{T} \left [ \max_{t \in
|
|
1027
|
+
(0,j)} \left ( \sum_{i=0}^{t}X_{i} \right ) - \sum_{i=0}^{j}X_{i}
|
|
1028
|
+
\right ] ^2}
|
|
1029
|
+
|
|
1030
|
+
Parameters
|
|
1031
|
+
----------
|
|
1032
|
+
X : 1d-array
|
|
1033
|
+
Returns series, must have Tx1 size.
|
|
1034
|
+
|
|
1035
|
+
Raises
|
|
1036
|
+
------
|
|
1037
|
+
ValueError
|
|
1038
|
+
When the value cannot be calculated.
|
|
1039
|
+
|
|
1040
|
+
Returns
|
|
1041
|
+
-------
|
|
1042
|
+
value : float
|
|
1043
|
+
Ulcer Index of an uncompounded cumulative returns.
|
|
1044
|
+
|
|
1045
|
+
"""
|
|
1046
|
+
|
|
1047
|
+
a = np.array(X, ndmin=2)
|
|
1048
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1049
|
+
a = a.T
|
|
1050
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1051
|
+
raise ValueError("returns must have Tx1 size")
|
|
1052
|
+
|
|
1053
|
+
prices = np.insert(np.array(a), 0, 1, axis=0)
|
|
1054
|
+
NAV = np.cumsum(np.array(prices), axis=0)
|
|
1055
|
+
value = 0
|
|
1056
|
+
peak = -99999
|
|
1057
|
+
n = 0
|
|
1058
|
+
for i in NAV:
|
|
1059
|
+
if i > peak:
|
|
1060
|
+
peak = i
|
|
1061
|
+
DD = peak - i
|
|
1062
|
+
if DD > 0:
|
|
1063
|
+
value += DD**2
|
|
1064
|
+
n += 1
|
|
1065
|
+
if n == 0:
|
|
1066
|
+
value = 0
|
|
1067
|
+
else:
|
|
1068
|
+
value = np.sqrt(value / (n - 1))
|
|
1069
|
+
|
|
1070
|
+
value = np.array(value).item()
|
|
1071
|
+
|
|
1072
|
+
return value
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def MDD_Rel(X):
|
|
1076
|
+
r"""
|
|
1077
|
+
Calculate the Maximum Drawdown (MDD) of a returns series
|
|
1078
|
+
using cumpounded cumulative returns.
|
|
1079
|
+
|
|
1080
|
+
.. math::
|
|
1081
|
+
\text{MDD}(X) = \max_{j \in (0,T)}\left[\max_{t \in (0,j)}
|
|
1082
|
+
\left ( \prod_{i=0}^{t}(1+X_{i}) \right ) - \prod_{i=0}^{j}(1+X_{i})
|
|
1083
|
+
\right]
|
|
1084
|
+
|
|
1085
|
+
Parameters
|
|
1086
|
+
----------
|
|
1087
|
+
X : 1d-array
|
|
1088
|
+
Returns series, must have Tx1 size.
|
|
1089
|
+
|
|
1090
|
+
Raises
|
|
1091
|
+
------
|
|
1092
|
+
ValueError
|
|
1093
|
+
When the value cannot be calculated.
|
|
1094
|
+
|
|
1095
|
+
Returns
|
|
1096
|
+
-------
|
|
1097
|
+
value : float
|
|
1098
|
+
MDD of a cumpounded cumulative returns.
|
|
1099
|
+
|
|
1100
|
+
"""
|
|
1101
|
+
|
|
1102
|
+
a = np.array(X, ndmin=2)
|
|
1103
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1104
|
+
a = a.T
|
|
1105
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1106
|
+
raise ValueError("returns must have Tx1 size")
|
|
1107
|
+
|
|
1108
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1109
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1110
|
+
value = 0
|
|
1111
|
+
peak = -99999
|
|
1112
|
+
for i in NAV:
|
|
1113
|
+
if i > peak:
|
|
1114
|
+
peak = i
|
|
1115
|
+
DD = (peak - i) / peak
|
|
1116
|
+
if DD > value:
|
|
1117
|
+
value = DD
|
|
1118
|
+
|
|
1119
|
+
value = np.array(value).item()
|
|
1120
|
+
|
|
1121
|
+
return value
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def ADD_Rel(X):
|
|
1125
|
+
r"""
|
|
1126
|
+
Calculate the Average Drawdown (ADD) of a returns series
|
|
1127
|
+
using cumpounded cumulative returns.
|
|
1128
|
+
|
|
1129
|
+
.. math::
|
|
1130
|
+
\text{ADD}(X) = \frac{1}{T}\sum_{j=0}^{T} \left [ \max_{t \in (0,j)}
|
|
1131
|
+
\left ( \prod_{i=0}^{t}(1+X_{i}) \right )- \prod_{i=0}^{j}(1+X_{i})
|
|
1132
|
+
\right ]
|
|
1133
|
+
|
|
1134
|
+
Parameters
|
|
1135
|
+
----------
|
|
1136
|
+
X : 1d-array
|
|
1137
|
+
Returns series, must have Tx1 size.
|
|
1138
|
+
|
|
1139
|
+
Raises
|
|
1140
|
+
------
|
|
1141
|
+
ValueError
|
|
1142
|
+
When the value cannot be calculated.
|
|
1143
|
+
|
|
1144
|
+
Returns
|
|
1145
|
+
-------
|
|
1146
|
+
value : float
|
|
1147
|
+
ADD of a cumpounded cumulative returns.
|
|
1148
|
+
|
|
1149
|
+
"""
|
|
1150
|
+
|
|
1151
|
+
a = np.array(X, ndmin=2)
|
|
1152
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1153
|
+
a = a.T
|
|
1154
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1155
|
+
raise ValueError("returns must have Tx1 size")
|
|
1156
|
+
|
|
1157
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1158
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1159
|
+
value = 0
|
|
1160
|
+
peak = -99999
|
|
1161
|
+
n = 0
|
|
1162
|
+
for i in NAV:
|
|
1163
|
+
if i > peak:
|
|
1164
|
+
peak = i
|
|
1165
|
+
DD = (peak - i) / peak
|
|
1166
|
+
if DD > 0:
|
|
1167
|
+
value += DD
|
|
1168
|
+
n += 1
|
|
1169
|
+
if n == 0:
|
|
1170
|
+
value = 0
|
|
1171
|
+
else:
|
|
1172
|
+
value = value / (n - 1)
|
|
1173
|
+
|
|
1174
|
+
value = np.array(value).item()
|
|
1175
|
+
|
|
1176
|
+
return value
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def DaR_Rel(X, alpha=0.05):
|
|
1180
|
+
r"""
|
|
1181
|
+
Calculate the Drawdown at Risk (DaR) of a returns series
|
|
1182
|
+
using cumpounded cumulative returns.
|
|
1183
|
+
|
|
1184
|
+
.. math::
|
|
1185
|
+
\text{DaR}_{\alpha}(X) & = \max_{j \in (0,T)} \left \{ \text{DD}(X,j)
|
|
1186
|
+
\in \mathbb{R}: F_{\text{DD}} \left ( \text{DD}(X,j) \right )< 1 - \alpha
|
|
1187
|
+
\right \} \\
|
|
1188
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \prod_{i=0}^{t}(1+X_{i})
|
|
1189
|
+
\right )- \prod_{i=0}^{j}(1+X_{i})
|
|
1190
|
+
|
|
1191
|
+
Parameters
|
|
1192
|
+
----------
|
|
1193
|
+
X : 1d-array
|
|
1194
|
+
Returns series, must have Tx1 size..
|
|
1195
|
+
alpha : float, optional
|
|
1196
|
+
Significance level of DaR. The default is 0.05.
|
|
1197
|
+
|
|
1198
|
+
Raises
|
|
1199
|
+
------
|
|
1200
|
+
ValueError
|
|
1201
|
+
When the value cannot be calculated.
|
|
1202
|
+
|
|
1203
|
+
Returns
|
|
1204
|
+
-------
|
|
1205
|
+
value : float
|
|
1206
|
+
DaR of a cumpounded cumulative returns series.
|
|
1207
|
+
|
|
1208
|
+
"""
|
|
1209
|
+
|
|
1210
|
+
a = np.array(X, ndmin=2)
|
|
1211
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1212
|
+
a = a.T
|
|
1213
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1214
|
+
raise ValueError("X must have Tx1 size")
|
|
1215
|
+
|
|
1216
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1217
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1218
|
+
DD = []
|
|
1219
|
+
peak = -99999
|
|
1220
|
+
for i in NAV:
|
|
1221
|
+
if i > peak:
|
|
1222
|
+
peak = i
|
|
1223
|
+
DD.append(-(peak - i) / peak)
|
|
1224
|
+
del DD[0]
|
|
1225
|
+
sorted_DD = np.sort(np.array(DD), axis=0)
|
|
1226
|
+
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
|
|
1227
|
+
value = -sorted_DD[index]
|
|
1228
|
+
value = np.array(value).item()
|
|
1229
|
+
|
|
1230
|
+
return value
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def CDaR_Rel(X, alpha=0.05):
|
|
1234
|
+
r"""
|
|
1235
|
+
Calculate the Conditional Drawdown at Risk (CDaR) of a returns series
|
|
1236
|
+
using cumpounded cumulative returns.
|
|
1237
|
+
|
|
1238
|
+
.. math::
|
|
1239
|
+
\text{CDaR}_{\alpha}(X) = \text{DaR}_{\alpha}(X) + \frac{1}{\alpha T}
|
|
1240
|
+
\sum_{i=0}^{T} \max \left [ \max_{t \in (0,T)}
|
|
1241
|
+
\left ( \prod_{i=0}^{t}(1+X_{i}) \right )- \prod_{i=0}^{j}(1+X_{i})
|
|
1242
|
+
- \text{DaR}_{\alpha}(X), 0 \right ]
|
|
1243
|
+
|
|
1244
|
+
Where:
|
|
1245
|
+
|
|
1246
|
+
:math:`\text{DaR}_{\alpha}` is the Drawdown at Risk of a cumpound
|
|
1247
|
+
cumulated return series :math:`X`.
|
|
1248
|
+
|
|
1249
|
+
Parameters
|
|
1250
|
+
----------
|
|
1251
|
+
X : 1d-array
|
|
1252
|
+
Returns series, must have Tx1 size..
|
|
1253
|
+
alpha : float, optional
|
|
1254
|
+
Significance level of CDaR. The default is 0.05.
|
|
1255
|
+
|
|
1256
|
+
Raises
|
|
1257
|
+
------
|
|
1258
|
+
ValueError
|
|
1259
|
+
When the value cannot be calculated.
|
|
1260
|
+
|
|
1261
|
+
Returns
|
|
1262
|
+
-------
|
|
1263
|
+
value : float
|
|
1264
|
+
CDaR of a cumpounded cumulative returns series.
|
|
1265
|
+
|
|
1266
|
+
"""
|
|
1267
|
+
|
|
1268
|
+
a = np.array(X, ndmin=2)
|
|
1269
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1270
|
+
a = a.T
|
|
1271
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1272
|
+
raise ValueError("X must have Tx1 size")
|
|
1273
|
+
|
|
1274
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1275
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1276
|
+
DD = []
|
|
1277
|
+
peak = -99999
|
|
1278
|
+
for i in NAV:
|
|
1279
|
+
if i > peak:
|
|
1280
|
+
peak = i
|
|
1281
|
+
DD.append(-(peak - i) / peak)
|
|
1282
|
+
del DD[0]
|
|
1283
|
+
sorted_DD = np.sort(np.array(DD), axis=0)
|
|
1284
|
+
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
|
|
1285
|
+
sum_var = 0
|
|
1286
|
+
for i in range(0, index + 1):
|
|
1287
|
+
sum_var = sum_var + sorted_DD[i] - sorted_DD[index]
|
|
1288
|
+
value = -sorted_DD[index] - sum_var / (alpha * len(sorted_DD))
|
|
1289
|
+
value = np.array(value).item()
|
|
1290
|
+
|
|
1291
|
+
return value
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def EDaR_Rel(X, alpha=0.05, solver="CLARABEL"):
|
|
1295
|
+
r"""
|
|
1296
|
+
Calculate the Entropic Drawdown at Risk (EDaR) of a returns series
|
|
1297
|
+
using cumpounded cumulative returns.
|
|
1298
|
+
|
|
1299
|
+
.. math::
|
|
1300
|
+
\text{EDaR}_{\alpha}(X) & = \inf_{z>0} \left \{ z
|
|
1301
|
+
\ln \left (\frac{M_{\text{DD}(X)}(z^{-1})}{\alpha} \right ) \right \} \\
|
|
1302
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \prod_{i=0}^{t}(1+X_{i})
|
|
1303
|
+
\right )- \prod_{i=0}^{j}(1+X_{i})
|
|
1304
|
+
|
|
1305
|
+
Parameters
|
|
1306
|
+
----------
|
|
1307
|
+
X : 1d-array
|
|
1308
|
+
Returns series, must have Tx1 size..
|
|
1309
|
+
alpha : float, optional
|
|
1310
|
+
Significance level of EDaR. The default is 0.05.
|
|
1311
|
+
|
|
1312
|
+
Raises
|
|
1313
|
+
------
|
|
1314
|
+
ValueError
|
|
1315
|
+
When the value cannot be calculated.
|
|
1316
|
+
|
|
1317
|
+
Returns
|
|
1318
|
+
-------
|
|
1319
|
+
(value, z) : tuple
|
|
1320
|
+
EDaR of a cumpounded cumulative returns series
|
|
1321
|
+
and value of z that minimize EDaR.
|
|
1322
|
+
|
|
1323
|
+
"""
|
|
1324
|
+
|
|
1325
|
+
a = np.array(X, ndmin=2)
|
|
1326
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1327
|
+
a = a.T
|
|
1328
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1329
|
+
raise ValueError("X must have Tx1 size")
|
|
1330
|
+
|
|
1331
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1332
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1333
|
+
DD = []
|
|
1334
|
+
peak = -99999
|
|
1335
|
+
for i in NAV:
|
|
1336
|
+
if i > peak:
|
|
1337
|
+
peak = i
|
|
1338
|
+
DD.append(-(peak - i) / peak)
|
|
1339
|
+
del DD[0]
|
|
1340
|
+
|
|
1341
|
+
(value, t) = EVaR_Hist(np.array(DD), alpha=alpha, solver=solver)
|
|
1342
|
+
|
|
1343
|
+
return (value, t)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def RLDaR_Rel(X, alpha=0.05, kappa=0.3, solver="CLARABEL"):
|
|
1347
|
+
r"""
|
|
1348
|
+
Calculate the Relativistic Drawdown at Risk (RLDaR) of a returns series
|
|
1349
|
+
using compounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
1350
|
+
|
|
1351
|
+
.. math::
|
|
1352
|
+
\text{RLDaR}^{\kappa}_{\alpha}(X) & = \text{RLVaR}^{\kappa}_{\alpha}(\text{DD}(X)) \\
|
|
1353
|
+
\text{DD}(X,j) & = \max_{t \in (0,j)} \left ( \prod_{i=0}^{t}(1+X_{i})
|
|
1354
|
+
\right )- \prod_{i=0}^{j}(1+X_{i}) \\
|
|
1355
|
+
|
|
1356
|
+
Parameters
|
|
1357
|
+
----------
|
|
1358
|
+
X : 1d-array
|
|
1359
|
+
Returns series, must have Tx1 size.
|
|
1360
|
+
alpha : float, optional
|
|
1361
|
+
Significance level of RLDaR. The default is 0.05.
|
|
1362
|
+
kappa : float, optional
|
|
1363
|
+
Deformation parameter of RLDaR, must be between 0 and 1. The default is 0.3.
|
|
1364
|
+
solver: str, optional
|
|
1365
|
+
Solver available for CVXPY that supports power cone programming. Used
|
|
1366
|
+
to calculate RLVaR, RVRG and RLDaR. The default value is 'CLARABEL'.
|
|
1367
|
+
|
|
1368
|
+
Raises
|
|
1369
|
+
------
|
|
1370
|
+
ValueError
|
|
1371
|
+
When the value cannot be calculated.
|
|
1372
|
+
|
|
1373
|
+
Returns
|
|
1374
|
+
-------
|
|
1375
|
+
value : tuple
|
|
1376
|
+
RLDaR of a compounded cumulative returns series.
|
|
1377
|
+
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
a = np.array(X, ndmin=2)
|
|
1381
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1382
|
+
a = a.T
|
|
1383
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1384
|
+
raise ValueError("X must have Tx1 size")
|
|
1385
|
+
|
|
1386
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1387
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1388
|
+
DD = []
|
|
1389
|
+
peak = -99999
|
|
1390
|
+
for i in NAV:
|
|
1391
|
+
if i > peak:
|
|
1392
|
+
peak = i
|
|
1393
|
+
DD.append(-(peak - i) / peak)
|
|
1394
|
+
del DD[0]
|
|
1395
|
+
|
|
1396
|
+
value = RLVaR_Hist(np.array(DD), alpha=alpha, kappa=kappa, solver=solver)
|
|
1397
|
+
|
|
1398
|
+
return value
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def UCI_Rel(X):
|
|
1402
|
+
r"""
|
|
1403
|
+
Calculate the Ulcer Index (UCI) of a returns series
|
|
1404
|
+
using cumpounded cumulative returns.
|
|
1405
|
+
|
|
1406
|
+
.. math::
|
|
1407
|
+
\text{UCI}(X) =\sqrt{\frac{1}{T}\sum_{j=0}^{T} \left [ \max_{t \in
|
|
1408
|
+
(0,j)} \left ( \prod_{i=0}^{t}(1+X_{i}) \right )- \prod_{i=0}^{j}
|
|
1409
|
+
(1+X_{i}) \right ] ^2}
|
|
1410
|
+
|
|
1411
|
+
Parameters
|
|
1412
|
+
----------
|
|
1413
|
+
X : 1d-array
|
|
1414
|
+
Returns series, must have Tx1 size.
|
|
1415
|
+
|
|
1416
|
+
Raises
|
|
1417
|
+
------
|
|
1418
|
+
ValueError
|
|
1419
|
+
When the value cannot be calculated.
|
|
1420
|
+
|
|
1421
|
+
Returns
|
|
1422
|
+
-------
|
|
1423
|
+
value : float
|
|
1424
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1425
|
+
|
|
1426
|
+
"""
|
|
1427
|
+
|
|
1428
|
+
a = np.array(X, ndmin=2)
|
|
1429
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1430
|
+
a = a.T
|
|
1431
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1432
|
+
raise ValueError("returns must have Tx1 size")
|
|
1433
|
+
|
|
1434
|
+
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
|
|
1435
|
+
NAV = np.cumprod(prices, axis=0)
|
|
1436
|
+
value = 0
|
|
1437
|
+
peak = -99999
|
|
1438
|
+
n = 0
|
|
1439
|
+
for i in NAV:
|
|
1440
|
+
if i > peak:
|
|
1441
|
+
peak = i
|
|
1442
|
+
DD = (peak - i) / peak
|
|
1443
|
+
if DD > 0:
|
|
1444
|
+
value += DD**2
|
|
1445
|
+
n += 1
|
|
1446
|
+
if n == 0:
|
|
1447
|
+
value = 0
|
|
1448
|
+
else:
|
|
1449
|
+
value = np.sqrt(value / (n - 1))
|
|
1450
|
+
|
|
1451
|
+
value = np.array(value).item()
|
|
1452
|
+
|
|
1453
|
+
return value
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def GMD(X):
|
|
1457
|
+
r"""
|
|
1458
|
+
Calculate the Gini Mean Difference (GMD) of a returns series.
|
|
1459
|
+
|
|
1460
|
+
Parameters
|
|
1461
|
+
----------
|
|
1462
|
+
X : 1d-array
|
|
1463
|
+
Returns series, must have Tx1 size.
|
|
1464
|
+
|
|
1465
|
+
Raises
|
|
1466
|
+
------
|
|
1467
|
+
ValueError
|
|
1468
|
+
When the value cannot be calculated.
|
|
1469
|
+
|
|
1470
|
+
Returns
|
|
1471
|
+
-------
|
|
1472
|
+
value : float
|
|
1473
|
+
Gini Mean Difference of a returns series.
|
|
1474
|
+
|
|
1475
|
+
"""
|
|
1476
|
+
|
|
1477
|
+
a = np.array(X, ndmin=2)
|
|
1478
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1479
|
+
a = a.T
|
|
1480
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1481
|
+
raise ValueError("returns must have Tx1 size")
|
|
1482
|
+
|
|
1483
|
+
T = a.shape[0]
|
|
1484
|
+
w_ = owa.owa_gmd(T)
|
|
1485
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1486
|
+
|
|
1487
|
+
return value
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
def TG(X, alpha=0.05, a_sim=100):
|
|
1491
|
+
r"""
|
|
1492
|
+
Calculate the Tail Gini of a returns series.
|
|
1493
|
+
|
|
1494
|
+
Parameters
|
|
1495
|
+
----------
|
|
1496
|
+
X : 1d-array
|
|
1497
|
+
Returns series, must have Tx1 size.
|
|
1498
|
+
alpha : float, optional
|
|
1499
|
+
Significance level of Tail Gini. The default is 0.05.
|
|
1500
|
+
a_sim : float, optional
|
|
1501
|
+
Number of CVaRs used to approximate Tail Gini. The default is 100.
|
|
1502
|
+
|
|
1503
|
+
Raises
|
|
1504
|
+
------
|
|
1505
|
+
ValueError
|
|
1506
|
+
When the value cannot be calculated.
|
|
1507
|
+
|
|
1508
|
+
Returns
|
|
1509
|
+
-------
|
|
1510
|
+
value : float
|
|
1511
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1512
|
+
|
|
1513
|
+
"""
|
|
1514
|
+
|
|
1515
|
+
a = np.array(X, ndmin=2)
|
|
1516
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1517
|
+
a = a.T
|
|
1518
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1519
|
+
raise ValueError("returns must have Tx1 size")
|
|
1520
|
+
|
|
1521
|
+
T = a.shape[0]
|
|
1522
|
+
w_ = owa.owa_tg(T, alpha, a_sim)
|
|
1523
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1524
|
+
|
|
1525
|
+
return value
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def RG(X):
|
|
1529
|
+
r"""
|
|
1530
|
+
Calculate the range of a returns series.
|
|
1531
|
+
|
|
1532
|
+
Parameters
|
|
1533
|
+
----------
|
|
1534
|
+
X : 1d-array
|
|
1535
|
+
Returns series, must have Tx1 size.
|
|
1536
|
+
|
|
1537
|
+
Raises
|
|
1538
|
+
------
|
|
1539
|
+
ValueError
|
|
1540
|
+
When the value cannot be calculated.
|
|
1541
|
+
|
|
1542
|
+
Returns
|
|
1543
|
+
-------
|
|
1544
|
+
value : float
|
|
1545
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1546
|
+
|
|
1547
|
+
"""
|
|
1548
|
+
|
|
1549
|
+
a = np.array(X, ndmin=2)
|
|
1550
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1551
|
+
a = a.T
|
|
1552
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1553
|
+
raise ValueError("returns must have Tx1 size")
|
|
1554
|
+
|
|
1555
|
+
T = a.shape[0]
|
|
1556
|
+
w_ = owa.owa_rg(T)
|
|
1557
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1558
|
+
|
|
1559
|
+
return value
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
def VRG(X, alpha=0.05, beta=None):
|
|
1563
|
+
r"""
|
|
1564
|
+
Calculate the CVaR range of a returns series.
|
|
1565
|
+
|
|
1566
|
+
Parameters
|
|
1567
|
+
----------
|
|
1568
|
+
X : 1d-array
|
|
1569
|
+
Returns series, must have Tx1 size.
|
|
1570
|
+
alpha : float, optional
|
|
1571
|
+
Significance level of VaR of losses. The default is 0.05.
|
|
1572
|
+
beta : float, optional
|
|
1573
|
+
Significance level of VaR of gains. If None it duplicates alpha value.
|
|
1574
|
+
The default is None.
|
|
1575
|
+
|
|
1576
|
+
Raises
|
|
1577
|
+
------
|
|
1578
|
+
ValueError
|
|
1579
|
+
When the value cannot be calculated.
|
|
1580
|
+
|
|
1581
|
+
Returns
|
|
1582
|
+
-------
|
|
1583
|
+
value : float
|
|
1584
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1585
|
+
|
|
1586
|
+
"""
|
|
1587
|
+
|
|
1588
|
+
a = np.array(X, ndmin=2)
|
|
1589
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1590
|
+
a = a.T
|
|
1591
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1592
|
+
raise ValueError("returns must have Tx1 size")
|
|
1593
|
+
|
|
1594
|
+
if beta is None:
|
|
1595
|
+
beta = alpha
|
|
1596
|
+
|
|
1597
|
+
value_L = VaR_Hist(a, alpha=alpha)
|
|
1598
|
+
value_G = VaR_Hist(-a, alpha=beta)
|
|
1599
|
+
|
|
1600
|
+
value = value_L + value_G
|
|
1601
|
+
|
|
1602
|
+
return value
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
def CVRG(X, alpha=0.05, beta=None):
|
|
1606
|
+
r"""
|
|
1607
|
+
Calculate the CVaR range of a returns series.
|
|
1608
|
+
|
|
1609
|
+
Parameters
|
|
1610
|
+
----------
|
|
1611
|
+
X : 1d-array
|
|
1612
|
+
Returns series, must have Tx1 size.
|
|
1613
|
+
alpha : float, optional
|
|
1614
|
+
Significance level of CVaR of losses. The default is 0.05.
|
|
1615
|
+
beta : float, optional
|
|
1616
|
+
Significance level of CVaR of gains. If None it duplicates alpha value.
|
|
1617
|
+
The default is None.
|
|
1618
|
+
|
|
1619
|
+
Raises
|
|
1620
|
+
------
|
|
1621
|
+
ValueError
|
|
1622
|
+
When the value cannot be calculated.
|
|
1623
|
+
|
|
1624
|
+
Returns
|
|
1625
|
+
-------
|
|
1626
|
+
value : float
|
|
1627
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1628
|
+
|
|
1629
|
+
"""
|
|
1630
|
+
|
|
1631
|
+
a = np.array(X, ndmin=2)
|
|
1632
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1633
|
+
a = a.T
|
|
1634
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1635
|
+
raise ValueError("returns must have Tx1 size")
|
|
1636
|
+
|
|
1637
|
+
T = a.shape[0]
|
|
1638
|
+
w_ = owa.owa_cvrg(T, alpha=alpha, beta=beta)
|
|
1639
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1640
|
+
|
|
1641
|
+
return value
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def TGRG(X, alpha=0.05, a_sim=100, beta=None, b_sim=None):
|
|
1645
|
+
r"""
|
|
1646
|
+
Calculate the Tail Gini range of a returns series.
|
|
1647
|
+
|
|
1648
|
+
Parameters
|
|
1649
|
+
----------
|
|
1650
|
+
X : 1d-array
|
|
1651
|
+
Returns series, must have Tx1 size.
|
|
1652
|
+
alpha : float, optional
|
|
1653
|
+
Significance level of Tail Gini of losses. The default is 0.05.
|
|
1654
|
+
a_sim : float, optional
|
|
1655
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
1656
|
+
beta : float, optional
|
|
1657
|
+
Significance level of Tail Gini of gains. If None it duplicates alpha value.
|
|
1658
|
+
The default is None.
|
|
1659
|
+
b_sim : float, optional
|
|
1660
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
|
1661
|
+
The default is None.
|
|
1662
|
+
|
|
1663
|
+
Raises
|
|
1664
|
+
------
|
|
1665
|
+
ValueError
|
|
1666
|
+
When the value cannot be calculated.
|
|
1667
|
+
|
|
1668
|
+
Returns
|
|
1669
|
+
-------
|
|
1670
|
+
value : float
|
|
1671
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1672
|
+
|
|
1673
|
+
"""
|
|
1674
|
+
|
|
1675
|
+
a = np.array(X, ndmin=2)
|
|
1676
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1677
|
+
a = a.T
|
|
1678
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1679
|
+
raise ValueError("returns must have Tx1 size")
|
|
1680
|
+
|
|
1681
|
+
T = a.shape[0]
|
|
1682
|
+
w_ = owa.owa_tgrg(T, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
1683
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1684
|
+
|
|
1685
|
+
return value
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def EVRG(X, alpha=0.05, beta=None, solver="CLARABEL"):
|
|
1689
|
+
r"""
|
|
1690
|
+
Calculate the CVaR range of a returns series.
|
|
1691
|
+
|
|
1692
|
+
Parameters
|
|
1693
|
+
----------
|
|
1694
|
+
X : 1d-array
|
|
1695
|
+
Returns series, must have Tx1 size.
|
|
1696
|
+
alpha : float, optional
|
|
1697
|
+
Significance level of EVaR of losses. The default is 0.05.
|
|
1698
|
+
beta : float, optional
|
|
1699
|
+
Significance level of EVaR of gains. If None it duplicates alpha value.
|
|
1700
|
+
The default is None.
|
|
1701
|
+
solver: str, optional
|
|
1702
|
+
Solver available for CVXPY that supports exponential cone programming.
|
|
1703
|
+
Used to calculate EVaR, EVRG and EDaR. The default value is 'CLARABEL'.
|
|
1704
|
+
|
|
1705
|
+
Raises
|
|
1706
|
+
------
|
|
1707
|
+
ValueError
|
|
1708
|
+
When the value cannot be calculated.
|
|
1709
|
+
|
|
1710
|
+
Returns
|
|
1711
|
+
-------
|
|
1712
|
+
value : float
|
|
1713
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1714
|
+
|
|
1715
|
+
"""
|
|
1716
|
+
|
|
1717
|
+
a = np.array(X, ndmin=2)
|
|
1718
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1719
|
+
a = a.T
|
|
1720
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1721
|
+
raise ValueError("returns must have Tx1 size")
|
|
1722
|
+
|
|
1723
|
+
if beta is None:
|
|
1724
|
+
beta = alpha
|
|
1725
|
+
|
|
1726
|
+
value_L = EVaR_Hist(a, alpha=alpha, solver=solver)[0]
|
|
1727
|
+
value_G = EVaR_Hist(-a, alpha=beta, solver=solver)[0]
|
|
1728
|
+
|
|
1729
|
+
value = value_L + value_G
|
|
1730
|
+
|
|
1731
|
+
return value
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def RVRG(X, alpha=0.05, beta=None, kappa=0.3, kappa_g=None, solver="CLARABEL"):
|
|
1735
|
+
r"""
|
|
1736
|
+
Calculate the CVaR range of a returns series.
|
|
1737
|
+
|
|
1738
|
+
Parameters
|
|
1739
|
+
----------
|
|
1740
|
+
X : 1d-array
|
|
1741
|
+
Returns series, must have Tx1 size.
|
|
1742
|
+
alpha : float, optional
|
|
1743
|
+
Significance level of RLVaR of losses. The default is 0.05.
|
|
1744
|
+
beta : float, optional
|
|
1745
|
+
Significance level of RLVaR of gains. If None it duplicates alpha value.
|
|
1746
|
+
The default is None.
|
|
1747
|
+
kappa : float, optional
|
|
1748
|
+
Deformation parameter of RLVaR for losses, must be between 0 and 1.
|
|
1749
|
+
The default is 0.3.
|
|
1750
|
+
kappa_g : float, optional
|
|
1751
|
+
Deformation parameter of RLVaR for gains, must be between 0 and 1.
|
|
1752
|
+
The default is None.
|
|
1753
|
+
solver: str, optional
|
|
1754
|
+
Solver available for CVXPY that supports power cone programming.
|
|
1755
|
+
Used to calculate EVaR, EVRG and EDaR. The default value is 'CLARABEL'.
|
|
1756
|
+
|
|
1757
|
+
Raises
|
|
1758
|
+
------
|
|
1759
|
+
ValueError
|
|
1760
|
+
When the value cannot be calculated.
|
|
1761
|
+
|
|
1762
|
+
Returns
|
|
1763
|
+
-------
|
|
1764
|
+
value : float
|
|
1765
|
+
Ulcer Index of a cumpounded cumulative returns.
|
|
1766
|
+
|
|
1767
|
+
"""
|
|
1768
|
+
|
|
1769
|
+
a = np.array(X, ndmin=2)
|
|
1770
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1771
|
+
a = a.T
|
|
1772
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1773
|
+
raise ValueError("returns must have Tx1 size")
|
|
1774
|
+
|
|
1775
|
+
if beta is None:
|
|
1776
|
+
beta = alpha
|
|
1777
|
+
if kappa_g is None:
|
|
1778
|
+
kappa_g = kappa
|
|
1779
|
+
|
|
1780
|
+
value_L = RLVaR_Hist(a, alpha=alpha, kappa=kappa, solver=solver)
|
|
1781
|
+
value_G = RLVaR_Hist(-a, alpha=beta, kappa=kappa_g, solver=solver)
|
|
1782
|
+
|
|
1783
|
+
value = value_L + value_G
|
|
1784
|
+
|
|
1785
|
+
return value
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def L_Moment(X, k=2):
|
|
1789
|
+
r"""
|
|
1790
|
+
Calculate the kth l-moment of a returns series.
|
|
1791
|
+
|
|
1792
|
+
.. math:
|
|
1793
|
+
\lambda_k = {\tbinom{T}{k}}^{-1} \mathop{\sum \sum \ldots \sum}_{1
|
|
1794
|
+
\leq i_{1} < i_{2} \cdots < i_{k} \leq n} \frac{1}{k}
|
|
1795
|
+
\sum^{k-1}_{j=0} (-1)^{j} \binom{k-1}{j} y_{[i_{k-j}]} \\
|
|
1796
|
+
|
|
1797
|
+
Where $y_{[i]}$ is the ith-ordered statistic.
|
|
1798
|
+
|
|
1799
|
+
Parameters
|
|
1800
|
+
----------
|
|
1801
|
+
X : 1d-array
|
|
1802
|
+
Returns series, must have Tx1 size.
|
|
1803
|
+
k : int
|
|
1804
|
+
Order of the l-moment. Must be an integer higher or equal than 1.
|
|
1805
|
+
|
|
1806
|
+
Raises
|
|
1807
|
+
------
|
|
1808
|
+
ValueError
|
|
1809
|
+
When the value cannot be calculated.
|
|
1810
|
+
|
|
1811
|
+
Returns
|
|
1812
|
+
-------
|
|
1813
|
+
value : float
|
|
1814
|
+
Kth l-moment of a returns series.
|
|
1815
|
+
|
|
1816
|
+
"""
|
|
1817
|
+
|
|
1818
|
+
a = np.array(X, ndmin=2)
|
|
1819
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1820
|
+
a = a.T
|
|
1821
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1822
|
+
raise ValueError("returns must have Tx1 size")
|
|
1823
|
+
|
|
1824
|
+
T = a.shape[0]
|
|
1825
|
+
w_ = owa.owa_l_moment(T, k=k)
|
|
1826
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1827
|
+
|
|
1828
|
+
return value
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def L_Moment_CRM(X, k=4, method="MSD", g=0.5, max_phi=0.5, solver="CLARABEL"):
|
|
1832
|
+
r"""
|
|
1833
|
+
Calculate a custom convex risk measure that is a weighted average of
|
|
1834
|
+
first k-th l-moments.
|
|
1835
|
+
|
|
1836
|
+
Parameters
|
|
1837
|
+
----------
|
|
1838
|
+
X : 1d-array
|
|
1839
|
+
Returns series, must have Tx1 size.
|
|
1840
|
+
k : int
|
|
1841
|
+
Order of the l-moment. Must be an integer higher or equal than 2.
|
|
1842
|
+
method : str, optional
|
|
1843
|
+
Method to calculate the weights used to combine the l-moments with
|
|
1844
|
+
order higher than 2. The default value is 'MSD'. Possible values are:
|
|
1845
|
+
|
|
1846
|
+
- 'CRRA': Normalized Constant Relative Risk Aversion coefficients.
|
|
1847
|
+
- 'ME': Maximum Entropy.
|
|
1848
|
+
- 'MSS': Minimum Sum Squares.
|
|
1849
|
+
- 'MSD': Minimum Square Distance.
|
|
1850
|
+
|
|
1851
|
+
g : float, optional
|
|
1852
|
+
Risk aversion coefficient of CRRA utility function. The default is 0.5.
|
|
1853
|
+
max_phi : float, optional
|
|
1854
|
+
Maximum weight constraint of L-moments.
|
|
1855
|
+
The default is 0.5.
|
|
1856
|
+
solver: str, optional
|
|
1857
|
+
Solver available for CVXPY. Used to calculate 'ME', 'MSS' and 'MSD' weights.
|
|
1858
|
+
The default value is None.
|
|
1859
|
+
|
|
1860
|
+
Raises
|
|
1861
|
+
------
|
|
1862
|
+
ValueError
|
|
1863
|
+
When the value cannot be calculated.
|
|
1864
|
+
|
|
1865
|
+
Returns
|
|
1866
|
+
-------
|
|
1867
|
+
value : float
|
|
1868
|
+
Custom convex risk measure that is a weighted average of first k-th l-moments of a returns series.
|
|
1869
|
+
|
|
1870
|
+
"""
|
|
1871
|
+
if k < 2 or (not isinstance(k, int)):
|
|
1872
|
+
raise ValueError("k must be an integer higher equal than 2")
|
|
1873
|
+
if method not in ["CRRA", "ME", "MSS", "MSD"]:
|
|
1874
|
+
raise ValueError("Available methods are 'CRRA', 'ME', 'MSS' and 'MSD'")
|
|
1875
|
+
if g >= 1 or g <= 0:
|
|
1876
|
+
raise ValueError("The risk aversion coefficient mus be between 0 and 1")
|
|
1877
|
+
if max_phi >= 1 or max_phi <= 0:
|
|
1878
|
+
raise ValueError(
|
|
1879
|
+
"The constraint on maximum weight of L-moments must be between 0 and 1"
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
a = np.array(X, ndmin=2)
|
|
1883
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1884
|
+
a = a.T
|
|
1885
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1886
|
+
raise ValueError("returns must have Tx1 size")
|
|
1887
|
+
|
|
1888
|
+
T = a.shape[0]
|
|
1889
|
+
w_ = owa.owa_l_moment_crm(
|
|
1890
|
+
T, k=k, method=method, g=g, max_phi=max_phi, solver=solver
|
|
1891
|
+
)
|
|
1892
|
+
value = (w_.T @ np.sort(a, axis=0)).item()
|
|
1893
|
+
|
|
1894
|
+
return value
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
def NEA(w):
|
|
1898
|
+
r"""
|
|
1899
|
+
Calculate the number of effective assets (NEA) that is the inverse of the
|
|
1900
|
+
Herfindahl Hirschman index (HHI).
|
|
1901
|
+
|
|
1902
|
+
Parameters
|
|
1903
|
+
----------
|
|
1904
|
+
w : DataFrame or Series of shape (n_assets, 1)
|
|
1905
|
+
Portfolio weights, where n_assets is the number of assets.
|
|
1906
|
+
|
|
1907
|
+
Raises
|
|
1908
|
+
------
|
|
1909
|
+
ValueError
|
|
1910
|
+
When the value cannot be calculated.
|
|
1911
|
+
|
|
1912
|
+
Returns
|
|
1913
|
+
-------
|
|
1914
|
+
value : float
|
|
1915
|
+
The NEA of the portfolio.
|
|
1916
|
+
"""
|
|
1917
|
+
|
|
1918
|
+
a = np.array(w, ndmin=2)
|
|
1919
|
+
if a.shape[0] == 1 and a.shape[1] > 1:
|
|
1920
|
+
a = a.T
|
|
1921
|
+
if a.shape[0] > 1 and a.shape[1] > 1:
|
|
1922
|
+
raise ValueError("w must have n_assets x 1 size")
|
|
1923
|
+
|
|
1924
|
+
value = 1 / np.sum(a**2)
|
|
1925
|
+
|
|
1926
|
+
return value
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
###############################################################################
|
|
1930
|
+
# Risk Adjusted Return Ratios
|
|
1931
|
+
###############################################################################
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def Sharpe_Risk(
|
|
1935
|
+
returns,
|
|
1936
|
+
w=None,
|
|
1937
|
+
cov=None,
|
|
1938
|
+
rm="MV",
|
|
1939
|
+
rf=0,
|
|
1940
|
+
alpha=0.05,
|
|
1941
|
+
a_sim=100,
|
|
1942
|
+
beta=None,
|
|
1943
|
+
b_sim=None,
|
|
1944
|
+
kappa=0.3,
|
|
1945
|
+
kappa_g=None,
|
|
1946
|
+
solver="CLARABEL",
|
|
1947
|
+
):
|
|
1948
|
+
r"""
|
|
1949
|
+
Calculate the risk measure available on the Sharpe function.
|
|
1950
|
+
|
|
1951
|
+
Parameters
|
|
1952
|
+
----------
|
|
1953
|
+
w : DataFrame or 1d-array of shape (n_assets, 1)
|
|
1954
|
+
Weights matrix, where n_assets is the number of assets.
|
|
1955
|
+
cov : DataFrame of shape (n_assets, n_assets)
|
|
1956
|
+
Covariance matrix, where n_assets is the number of assets.
|
|
1957
|
+
returns : DataFrame or nd-array of shape (n_samples, n_features)
|
|
1958
|
+
Features matrix, where n_samples is the number of samples and
|
|
1959
|
+
n_features is the number of features.
|
|
1960
|
+
rm : str, optional
|
|
1961
|
+
Risk measure used in the denominator of the ratio. The default is
|
|
1962
|
+
'MV'. Possible values are:
|
|
1963
|
+
|
|
1964
|
+
- 'MV': Standard Deviation.
|
|
1965
|
+
- 'KT': Square Root Kurtosis.
|
|
1966
|
+
- 'MAD': Mean Absolute Deviation.
|
|
1967
|
+
- 'GMD': Gini Mean Difference.
|
|
1968
|
+
- 'MSV': Semi Standard Deviation.
|
|
1969
|
+
- 'SKT': Square Root Semi Kurtosis.
|
|
1970
|
+
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
1971
|
+
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
1972
|
+
- 'VaR': Value at Risk.
|
|
1973
|
+
- 'CVaR': Conditional Value at Risk.
|
|
1974
|
+
- 'TG': Tail Gini.
|
|
1975
|
+
- 'EVaR': Entropic Value at Risk.
|
|
1976
|
+
- 'RLVaR': Relativistic Value at Risk. I recommend only use this function with MOSEK solver.
|
|
1977
|
+
- 'WR': Worst Realization (Minimax).
|
|
1978
|
+
- 'RG': Range of returns.
|
|
1979
|
+
- 'VRG' VaR range of returns.
|
|
1980
|
+
- 'CVRG': CVaR range of returns.
|
|
1981
|
+
- 'TGRG': Tail Gini range of returns.
|
|
1982
|
+
- 'EVRG': EVaR range of returns.
|
|
1983
|
+
- 'RVRG': RLVaR range of returns. I recommend only use this function with MOSEK solver.
|
|
1984
|
+
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
|
1985
|
+
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
1986
|
+
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
|
1987
|
+
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
1988
|
+
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
1989
|
+
- 'RLDaR': Relativistic Drawdown at Risk of uncompounded cumulative returns. I recommend only use this risk measure with MOSEK solver.
|
|
1990
|
+
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
1991
|
+
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
|
1992
|
+
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
|
1993
|
+
- 'DaR_Rel': Drawdown at Risk of compounded cumulative returns.
|
|
1994
|
+
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
|
1995
|
+
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
|
1996
|
+
- 'RLDaR_Rel': Relativistic Drawdown at Risk of compounded cumulative returns. I recommend only use this risk measure with MOSEK solver.
|
|
1997
|
+
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
|
1998
|
+
|
|
1999
|
+
rf : float, optional
|
|
2000
|
+
Risk free rate. The default is 0.
|
|
2001
|
+
alpha : float, optional
|
|
2002
|
+
Significance level of VaR, CVaR, EVaR, RLVaR, DaR, CDaR, EDaR, RLDaR
|
|
2003
|
+
and Tail Gini of losses. The default is 0.05.
|
|
2004
|
+
a_sim : float, optional
|
|
2005
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
2006
|
+
beta : float, optional
|
|
2007
|
+
Significance level of CVaR and Tail Gini of gains. If None it
|
|
2008
|
+
duplicates alpha value. The default is None.
|
|
2009
|
+
b_sim : float, optional
|
|
2010
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it
|
|
2011
|
+
duplicates a_sim value. The default is None.
|
|
2012
|
+
kappa : float, optional
|
|
2013
|
+
Deformation parameter of RLVaR and RLDaR for losses, must be between 0 and 1.
|
|
2014
|
+
The default is 0.3.
|
|
2015
|
+
kappa_g : float, optional
|
|
2016
|
+
Deformation parameter of RLVaR and RLDaR for gains, must be between 0 and 1.
|
|
2017
|
+
The default is None.
|
|
2018
|
+
solver: str, optional
|
|
2019
|
+
Solver available for CVXPY that supports exponential and power cone
|
|
2020
|
+
programming. Used to calculate RLVaR and RLDaR. The default value is
|
|
2021
|
+
'CLARABEL'.
|
|
2022
|
+
|
|
2023
|
+
Raises
|
|
2024
|
+
------
|
|
2025
|
+
ValueError
|
|
2026
|
+
When the value cannot be calculated.
|
|
2027
|
+
|
|
2028
|
+
Returns
|
|
2029
|
+
-------
|
|
2030
|
+
value : float
|
|
2031
|
+
Risk measure of the portfolio.
|
|
2032
|
+
|
|
2033
|
+
"""
|
|
2034
|
+
|
|
2035
|
+
if isinstance(returns, pd.Series):
|
|
2036
|
+
returns_ = returns.to_frame()
|
|
2037
|
+
elif isinstance(returns, pd.DataFrame):
|
|
2038
|
+
returns_ = returns.to_numpy()
|
|
2039
|
+
else:
|
|
2040
|
+
returns_ = np.array(returns, ndmin=2)
|
|
2041
|
+
|
|
2042
|
+
if returns_.shape[1] == 1:
|
|
2043
|
+
w_ = np.array([[1]])
|
|
2044
|
+
else:
|
|
2045
|
+
if w is None:
|
|
2046
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2047
|
+
else:
|
|
2048
|
+
w_ = np.array(w, ndmin=2)
|
|
2049
|
+
|
|
2050
|
+
if w_.shape[0] == 1 and w_.shape[1] > 1:
|
|
2051
|
+
w_ = w_.T
|
|
2052
|
+
if w_.shape[0] > 1 and w_.shape[1] > 1:
|
|
2053
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2054
|
+
|
|
2055
|
+
if cov is None:
|
|
2056
|
+
cov_ = np.array(np.cov(returns_, rowvar=False), ndmin=2)
|
|
2057
|
+
else:
|
|
2058
|
+
cov_ = np.array(cov, ndmin=2)
|
|
2059
|
+
|
|
2060
|
+
a = returns_ @ w_
|
|
2061
|
+
if rm == "MV":
|
|
2062
|
+
risk = w_.T @ cov_ @ w_
|
|
2063
|
+
risk = np.sqrt(risk.item())
|
|
2064
|
+
elif rm == "MAD":
|
|
2065
|
+
risk = MAD(a)
|
|
2066
|
+
elif rm == "GMD":
|
|
2067
|
+
risk = GMD(a)
|
|
2068
|
+
elif rm == "MSV":
|
|
2069
|
+
risk = SemiDeviation(a)
|
|
2070
|
+
elif rm == "FLPM":
|
|
2071
|
+
risk = LPM(a, MAR=rf, p=1)
|
|
2072
|
+
elif rm == "SLPM":
|
|
2073
|
+
risk = LPM(a, MAR=rf, p=2)
|
|
2074
|
+
elif rm == "VaR":
|
|
2075
|
+
risk = VaR_Hist(a, alpha=alpha)
|
|
2076
|
+
elif rm == "CVaR":
|
|
2077
|
+
risk = CVaR_Hist(a, alpha=alpha)
|
|
2078
|
+
elif rm == "TG":
|
|
2079
|
+
risk = TG(a, alpha=alpha, a_sim=a_sim)
|
|
2080
|
+
elif rm == "EVaR":
|
|
2081
|
+
risk = EVaR_Hist(a, alpha=alpha, solver=solver)[0]
|
|
2082
|
+
elif rm == "RLVaR":
|
|
2083
|
+
risk = RLVaR_Hist(a, alpha=alpha, kappa=kappa, solver=solver)
|
|
2084
|
+
elif rm == "WR":
|
|
2085
|
+
risk = WR(a)
|
|
2086
|
+
elif rm == "RG":
|
|
2087
|
+
risk = RG(a)
|
|
2088
|
+
elif rm == "VRG":
|
|
2089
|
+
risk = VRG(a, alpha=alpha, beta=beta)
|
|
2090
|
+
elif rm == "CVRG":
|
|
2091
|
+
risk = CVRG(a, alpha=alpha, beta=beta)
|
|
2092
|
+
elif rm == "TGRG":
|
|
2093
|
+
risk = TGRG(a, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
2094
|
+
elif rm == "EVRG":
|
|
2095
|
+
risk = EVRG(a, alpha=alpha, beta=beta, solver=solver)
|
|
2096
|
+
elif rm == "RVRG":
|
|
2097
|
+
risk = RVRG(
|
|
2098
|
+
a, alpha=alpha, beta=beta, kappa=kappa, kappa_g=kappa_g, solver=solver
|
|
2099
|
+
)
|
|
2100
|
+
elif rm == "MDD":
|
|
2101
|
+
risk = MDD_Abs(a)
|
|
2102
|
+
elif rm == "ADD":
|
|
2103
|
+
risk = ADD_Abs(a)
|
|
2104
|
+
elif rm == "DaR":
|
|
2105
|
+
risk = DaR_Abs(a, alpha=alpha)
|
|
2106
|
+
elif rm == "CDaR":
|
|
2107
|
+
risk = CDaR_Abs(a, alpha=alpha)
|
|
2108
|
+
elif rm == "EDaR":
|
|
2109
|
+
risk = EDaR_Abs(a, alpha=alpha)[0]
|
|
2110
|
+
elif rm == "RLDaR":
|
|
2111
|
+
risk = RLDaR_Abs(a, alpha=alpha, kappa=kappa, solver=solver)
|
|
2112
|
+
elif rm == "UCI":
|
|
2113
|
+
risk = UCI_Abs(a)
|
|
2114
|
+
elif rm == "MDD_Rel":
|
|
2115
|
+
risk = MDD_Rel(a)
|
|
2116
|
+
elif rm == "ADD_Rel":
|
|
2117
|
+
risk = ADD_Rel(a)
|
|
2118
|
+
elif rm == "DaR_Rel":
|
|
2119
|
+
risk = DaR_Rel(a, alpha=alpha)
|
|
2120
|
+
elif rm == "CDaR_Rel":
|
|
2121
|
+
risk = CDaR_Rel(a, alpha=alpha)
|
|
2122
|
+
elif rm == "EDaR_Rel":
|
|
2123
|
+
risk = EDaR_Rel(a, alpha=alpha)[0]
|
|
2124
|
+
elif rm == "RLDaR_Rel":
|
|
2125
|
+
risk = RLDaR_Rel(a, alpha=alpha, kappa=kappa, solver=solver)
|
|
2126
|
+
elif rm == "UCI_Rel":
|
|
2127
|
+
risk = UCI_Rel(a)
|
|
2128
|
+
elif rm == "KT":
|
|
2129
|
+
risk = Kurtosis(a)
|
|
2130
|
+
elif rm == "SKT":
|
|
2131
|
+
risk = SemiKurtosis(a)
|
|
2132
|
+
|
|
2133
|
+
value = risk
|
|
2134
|
+
|
|
2135
|
+
return value
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def Sharpe(
|
|
2139
|
+
returns,
|
|
2140
|
+
w=None,
|
|
2141
|
+
mu=None,
|
|
2142
|
+
cov=None,
|
|
2143
|
+
rm="MV",
|
|
2144
|
+
rf=0,
|
|
2145
|
+
alpha=0.05,
|
|
2146
|
+
a_sim=100,
|
|
2147
|
+
beta=None,
|
|
2148
|
+
b_sim=None,
|
|
2149
|
+
kappa=0.3,
|
|
2150
|
+
kappa_g=None,
|
|
2151
|
+
solver="CLARABEL",
|
|
2152
|
+
):
|
|
2153
|
+
r"""
|
|
2154
|
+
Calculate the Risk Adjusted Return Ratio from a portfolio returns series.
|
|
2155
|
+
|
|
2156
|
+
.. math::
|
|
2157
|
+
\text{Sharpe}(X) = \frac{\mathbb{E}(X) -
|
|
2158
|
+
r_{f}}{\phi(X)}
|
|
2159
|
+
|
|
2160
|
+
Where:
|
|
2161
|
+
|
|
2162
|
+
:math:`X` is the vector of portfolio returns.
|
|
2163
|
+
|
|
2164
|
+
:math:`r_{f}` is the risk free rate, when the risk measure is
|
|
2165
|
+
|
|
2166
|
+
:math:`\text{LPM}` uses instead of :math:`r_{f}` the :math:`\text{MAR}`.
|
|
2167
|
+
|
|
2168
|
+
:math:`\phi(X)` is a convex risk measure. The risk measures availabe are:
|
|
2169
|
+
|
|
2170
|
+
Parameters
|
|
2171
|
+
----------
|
|
2172
|
+
|
|
2173
|
+
returns : DataFrame or nd-array of shape (n_samples, n_features)
|
|
2174
|
+
Features matrix, where n_samples is the number of samples and
|
|
2175
|
+
n_features is the number of features.
|
|
2176
|
+
w : DataFrame or 1d-array of shape (n_assets, 1)
|
|
2177
|
+
Weights matrix, where n_assets is the number of assets.
|
|
2178
|
+
mu : DataFrame or nd-array of shape (1, n_assets)
|
|
2179
|
+
Vector of expected returns, where n_assets is the number of assets.
|
|
2180
|
+
cov : DataFrame of shape (n_assets, n_assets)
|
|
2181
|
+
Covariance matrix, where n_assets is the number of assets.
|
|
2182
|
+
rm : str, optional
|
|
2183
|
+
Risk measure used in the denominator of the ratio. The default is
|
|
2184
|
+
'MV'. Possible values are:
|
|
2185
|
+
|
|
2186
|
+
- 'MV': Standard Deviation.
|
|
2187
|
+
- 'KT': Square Root Kurtosis.
|
|
2188
|
+
- 'MAD': Mean Absolute Deviation.
|
|
2189
|
+
- 'GMD': Gini Mean Difference.
|
|
2190
|
+
- 'MSV': Semi Standard Deviation.
|
|
2191
|
+
- 'SKT': Square Root Semi Kurtosis.
|
|
2192
|
+
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
2193
|
+
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
2194
|
+
- 'VaR': Value at Risk.
|
|
2195
|
+
- 'CVaR': Conditional Value at Risk.
|
|
2196
|
+
- 'TG': Tail Gini.
|
|
2197
|
+
- 'EVaR': Entropic Value at Risk.
|
|
2198
|
+
- 'RLVaR': Relativistic Value at Risk. I recommend only use this function with MOSEK solver.
|
|
2199
|
+
- 'WR': Worst Realization (Minimax).
|
|
2200
|
+
- 'RG': Range of returns.
|
|
2201
|
+
- 'VRG' VaR range of returns.
|
|
2202
|
+
- 'CVRG': CVaR range of returns.
|
|
2203
|
+
- 'TGRG': Tail Gini range of returns.
|
|
2204
|
+
- 'EVRG': EVaR range of returns.
|
|
2205
|
+
- 'RVRG': RLVaR range of returns. I recommend only use this function with MOSEK solver.
|
|
2206
|
+
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
|
2207
|
+
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
2208
|
+
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
|
2209
|
+
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
2210
|
+
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
2211
|
+
- 'RLDaR': Relativistic Drawdown at Risk of uncompounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2212
|
+
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
2213
|
+
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
|
2214
|
+
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
|
2215
|
+
- 'DaR_Rel': Drawdown at Risk of compounded cumulative returns.
|
|
2216
|
+
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
|
2217
|
+
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
|
2218
|
+
- 'RLDaR_Rel': Relativistic Drawdown at Risk of compounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2219
|
+
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
|
2220
|
+
|
|
2221
|
+
rf : float, optional
|
|
2222
|
+
Risk free rate. The default is 0.
|
|
2223
|
+
alpha : float, optional
|
|
2224
|
+
Significance level of VaR, CVaR, EVaR, RLVaR, DaR, CDaR, EDaR, RLDaR and Tail Gini of losses.
|
|
2225
|
+
The default is 0.05.
|
|
2226
|
+
a_sim : float, optional
|
|
2227
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
2228
|
+
beta : float, optional
|
|
2229
|
+
Significance level of CVaR and Tail Gini of gains. If None it duplicates alpha value.
|
|
2230
|
+
The default is None.
|
|
2231
|
+
b_sim : float, optional
|
|
2232
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
|
2233
|
+
The default is None.
|
|
2234
|
+
kappa : float, optional
|
|
2235
|
+
Deformation parameter of RLVaR and RLDaR for losses, must be between 0 and 1.
|
|
2236
|
+
The default is 0.3.
|
|
2237
|
+
kappa_g : float, optional
|
|
2238
|
+
Deformation parameter of RLVaR and RLDaR for gains, must be between 0 and 1.
|
|
2239
|
+
The default is None.
|
|
2240
|
+
solver: str, optional
|
|
2241
|
+
Solver available for CVXPY that supports power cone programming. Used to calculate RLVaR and RLDaR.
|
|
2242
|
+
The default value is None.
|
|
2243
|
+
|
|
2244
|
+
Raises
|
|
2245
|
+
------
|
|
2246
|
+
ValueError
|
|
2247
|
+
When the value cannot be calculated.
|
|
2248
|
+
|
|
2249
|
+
Returns
|
|
2250
|
+
-------
|
|
2251
|
+
value : float
|
|
2252
|
+
Risk adjusted return ratio of :math:`X`.
|
|
2253
|
+
|
|
2254
|
+
"""
|
|
2255
|
+
|
|
2256
|
+
if isinstance(returns, pd.Series):
|
|
2257
|
+
returns_ = returns.to_frame()
|
|
2258
|
+
elif isinstance(returns, pd.DataFrame):
|
|
2259
|
+
returns_ = returns.to_numpy()
|
|
2260
|
+
else:
|
|
2261
|
+
returns_ = np.array(returns, ndmin=2)
|
|
2262
|
+
|
|
2263
|
+
if returns_.shape[1] == 1:
|
|
2264
|
+
w_ = np.array([[1]])
|
|
2265
|
+
else:
|
|
2266
|
+
if w is None:
|
|
2267
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2268
|
+
else:
|
|
2269
|
+
w_ = np.array(w, ndmin=2)
|
|
2270
|
+
|
|
2271
|
+
if w_.shape[0] == 1 and w_.shape[1] > 1:
|
|
2272
|
+
w_ = w_.T
|
|
2273
|
+
if w_.shape[0] > 1 and w_.shape[1] > 1:
|
|
2274
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2275
|
+
|
|
2276
|
+
if cov is None:
|
|
2277
|
+
cov_ = np.array(np.cov(returns_, rowvar=False), ndmin=2)
|
|
2278
|
+
else:
|
|
2279
|
+
cov_ = np.array(cov, ndmin=2)
|
|
2280
|
+
|
|
2281
|
+
if mu is None:
|
|
2282
|
+
mu_ = np.array(np.mean(returns_, axis=0), ndmin=2)
|
|
2283
|
+
else:
|
|
2284
|
+
mu_ = np.array(mu, ndmin=2)
|
|
2285
|
+
|
|
2286
|
+
ret = mu_ @ w_
|
|
2287
|
+
ret = ret.item()
|
|
2288
|
+
|
|
2289
|
+
risk = Sharpe_Risk(
|
|
2290
|
+
returns=returns_,
|
|
2291
|
+
w=w_,
|
|
2292
|
+
cov=cov_,
|
|
2293
|
+
rm=rm,
|
|
2294
|
+
rf=rf,
|
|
2295
|
+
alpha=alpha,
|
|
2296
|
+
a_sim=a_sim,
|
|
2297
|
+
beta=beta,
|
|
2298
|
+
b_sim=b_sim,
|
|
2299
|
+
kappa=kappa,
|
|
2300
|
+
kappa_g=kappa_g,
|
|
2301
|
+
solver=solver,
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
value = (ret - rf) / risk
|
|
2305
|
+
|
|
2306
|
+
return value
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
###############################################################################
|
|
2310
|
+
# Risk Contribution Vectors
|
|
2311
|
+
###############################################################################
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
def Risk_Contribution(
|
|
2315
|
+
w,
|
|
2316
|
+
returns,
|
|
2317
|
+
cov=None,
|
|
2318
|
+
rm="MV",
|
|
2319
|
+
rf=0,
|
|
2320
|
+
alpha=0.05,
|
|
2321
|
+
a_sim=100,
|
|
2322
|
+
beta=None,
|
|
2323
|
+
b_sim=None,
|
|
2324
|
+
kappa=0.3,
|
|
2325
|
+
kappa_g=None,
|
|
2326
|
+
solver="CLARABEL",
|
|
2327
|
+
):
|
|
2328
|
+
r"""
|
|
2329
|
+
Calculate the risk contribution for each asset based on the selected risk measure.
|
|
2330
|
+
|
|
2331
|
+
Parameters
|
|
2332
|
+
----------
|
|
2333
|
+
w : DataFrame or Series of shape (n_assets, 1)
|
|
2334
|
+
Portfolio weights, where n_assets is the number of assets.
|
|
2335
|
+
returns : DataFrame or nd-array of shape (n_samples, n_features)
|
|
2336
|
+
Features matrix, where n_samples is the number of samples and
|
|
2337
|
+
n_features is the number of features.
|
|
2338
|
+
cov : DataFrame of shape (n_assets, n_assets)
|
|
2339
|
+
Covariance matrix, where n_assets is the number of assets.
|
|
2340
|
+
rm : str, optional
|
|
2341
|
+
Risk measure used in the denominator of the ratio. The default is
|
|
2342
|
+
'MV'. Possible values are:
|
|
2343
|
+
|
|
2344
|
+
- 'MV': Standard Deviation.
|
|
2345
|
+
- 'KT': Square Root Kurtosis.
|
|
2346
|
+
- 'MAD': Mean Absolute Deviation.
|
|
2347
|
+
- 'GMD': Gini Mean Difference.
|
|
2348
|
+
- 'MSV': Semi Standard Deviation.
|
|
2349
|
+
- 'SKT': Square Root Semi Kurtosis.
|
|
2350
|
+
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
2351
|
+
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
2352
|
+
- 'VaR': Value at Risk.
|
|
2353
|
+
- 'CVaR': Conditional Value at Risk.
|
|
2354
|
+
- 'TG': Tail Gini.
|
|
2355
|
+
- 'EVaR': Entropic Value at Risk.
|
|
2356
|
+
- 'RLVaR': Relativistic Value at Risk. I recommend only use this function with MOSEK solver.
|
|
2357
|
+
- 'WR': Worst Realization (Minimax).
|
|
2358
|
+
- 'RG': Range of returns.
|
|
2359
|
+
- 'VRG' VaR range of returns.
|
|
2360
|
+
- 'CVRG': CVaR range of returns.
|
|
2361
|
+
- 'TGRG': Tail Gini range of returns.
|
|
2362
|
+
- 'EVRG': EVaR range of returns.
|
|
2363
|
+
- 'RVRG': RLVaR range of returns. I recommend only use this function with MOSEK solver.
|
|
2364
|
+
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
|
2365
|
+
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
2366
|
+
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
|
2367
|
+
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
2368
|
+
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
2369
|
+
- 'RLDaR': Relativistic Drawdown at Risk of uncompounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2370
|
+
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
2371
|
+
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
|
2372
|
+
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
|
2373
|
+
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
|
2374
|
+
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
|
2375
|
+
- 'RLDaR_Rel': Relativistic Drawdown at Risk of compounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2376
|
+
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
|
2377
|
+
|
|
2378
|
+
rf : float, optional
|
|
2379
|
+
Risk free rate. The default is 0.
|
|
2380
|
+
alpha : float, optional
|
|
2381
|
+
Significance level of VaR, CVaR, EVaR, RLVaR, DaR, CDaR, EDaR, RLDaR and Tail Gini of losses.
|
|
2382
|
+
The default is 0.05.
|
|
2383
|
+
a_sim : float, optional
|
|
2384
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
2385
|
+
beta : float, optional
|
|
2386
|
+
Significance level of CVaR and Tail Gini of gains. If None it duplicates alpha value.
|
|
2387
|
+
The default is None.
|
|
2388
|
+
b_sim : float, optional
|
|
2389
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
|
2390
|
+
The default is None.
|
|
2391
|
+
kappa : float, optional
|
|
2392
|
+
Deformation parameter of RLVaR and RLDaR for losses, must be between 0 and 1.
|
|
2393
|
+
The default is 0.3.
|
|
2394
|
+
kappa_g : float, optional
|
|
2395
|
+
Deformation parameter of RLVaR and RLDaR for gains, must be between 0 and 1.
|
|
2396
|
+
The default is None.
|
|
2397
|
+
solver: str, optional
|
|
2398
|
+
Solver available for CVXPY that supports power cone programming. Used to calculate RLVaR and RLDaR.
|
|
2399
|
+
The default value is None.
|
|
2400
|
+
|
|
2401
|
+
Raises
|
|
2402
|
+
------
|
|
2403
|
+
ValueError
|
|
2404
|
+
When the value cannot be calculated.
|
|
2405
|
+
|
|
2406
|
+
Returns
|
|
2407
|
+
-------
|
|
2408
|
+
value : float
|
|
2409
|
+
Risk measure of the portfolio.
|
|
2410
|
+
|
|
2411
|
+
"""
|
|
2412
|
+
|
|
2413
|
+
w_ = np.array(w, ndmin=2)
|
|
2414
|
+
if w_.shape[0] == 1 and w_.shape[1] > 1:
|
|
2415
|
+
w_ = w_.T
|
|
2416
|
+
if w_.shape[0] > 1 and w_.shape[1] > 1:
|
|
2417
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2418
|
+
|
|
2419
|
+
if isinstance(returns, pd.Series):
|
|
2420
|
+
returns_ = returns.to_frame()
|
|
2421
|
+
returns_ = returns.to_numpy()
|
|
2422
|
+
elif isinstance(returns, pd.DataFrame):
|
|
2423
|
+
returns_ = returns.to_numpy()
|
|
2424
|
+
else:
|
|
2425
|
+
returns_ = np.array(returns, ndmin=2)
|
|
2426
|
+
|
|
2427
|
+
if cov is None:
|
|
2428
|
+
cov_ = np.array(np.cov(returns_, rowvar=False), ndmin=2)
|
|
2429
|
+
else:
|
|
2430
|
+
cov_ = np.array(cov, ndmin=2)
|
|
2431
|
+
|
|
2432
|
+
RC = []
|
|
2433
|
+
if rm in ["EVaR", "EDaR", "RLVaR", "RLDaR", "EVRG", "RVRG"]:
|
|
2434
|
+
d_i = 0.0001
|
|
2435
|
+
else:
|
|
2436
|
+
d_i = 0.0000001
|
|
2437
|
+
|
|
2438
|
+
for i in range(0, w_.shape[0]):
|
|
2439
|
+
delta = np.zeros((w_.shape[0], 1))
|
|
2440
|
+
delta[i, 0] = d_i
|
|
2441
|
+
w_1 = w_ + delta
|
|
2442
|
+
w_2 = w_ - delta
|
|
2443
|
+
a_1 = returns_ @ w_1
|
|
2444
|
+
a_2 = returns_ @ w_2
|
|
2445
|
+
if rm == "MV":
|
|
2446
|
+
risk_1 = w_1.T @ cov_ @ w_1
|
|
2447
|
+
risk_1 = np.sqrt(risk_1.item())
|
|
2448
|
+
risk_2 = w_2.T @ cov_ @ w_2
|
|
2449
|
+
risk_2 = np.sqrt(risk_2.item())
|
|
2450
|
+
elif rm == "MAD":
|
|
2451
|
+
risk_1 = MAD(a_1)
|
|
2452
|
+
risk_2 = MAD(a_2)
|
|
2453
|
+
elif rm == "GMD":
|
|
2454
|
+
risk_1 = GMD(a_1)
|
|
2455
|
+
risk_2 = GMD(a_2)
|
|
2456
|
+
elif rm == "MSV":
|
|
2457
|
+
risk_1 = SemiDeviation(a_1)
|
|
2458
|
+
risk_2 = SemiDeviation(a_2)
|
|
2459
|
+
elif rm == "FLPM":
|
|
2460
|
+
risk_1 = LPM(a_1, MAR=rf, p=1)
|
|
2461
|
+
risk_2 = LPM(a_2, MAR=rf, p=1)
|
|
2462
|
+
elif rm == "SLPM":
|
|
2463
|
+
risk_1 = LPM(a_1, MAR=rf, p=2)
|
|
2464
|
+
risk_2 = LPM(a_2, MAR=rf, p=2)
|
|
2465
|
+
elif rm == "VaR":
|
|
2466
|
+
risk_1 = VaR_Hist(a_1, alpha=alpha)
|
|
2467
|
+
risk_2 = VaR_Hist(a_2, alpha=alpha)
|
|
2468
|
+
elif rm == "CVaR":
|
|
2469
|
+
risk_1 = CVaR_Hist(a_1, alpha=alpha)
|
|
2470
|
+
risk_2 = CVaR_Hist(a_2, alpha=alpha)
|
|
2471
|
+
elif rm == "TG":
|
|
2472
|
+
risk_1 = TG(a_1, alpha=alpha, a_sim=a_sim)
|
|
2473
|
+
risk_2 = TG(a_2, alpha=alpha, a_sim=a_sim)
|
|
2474
|
+
elif rm == "EVaR":
|
|
2475
|
+
risk_1 = EVaR_Hist(a_1, alpha=alpha, solver=solver)[0]
|
|
2476
|
+
risk_2 = EVaR_Hist(a_2, alpha=alpha, solver=solver)[0]
|
|
2477
|
+
elif rm == "RLVaR":
|
|
2478
|
+
risk_1 = RLVaR_Hist(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2479
|
+
risk_2 = RLVaR_Hist(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2480
|
+
elif rm == "WR":
|
|
2481
|
+
risk_1 = WR(a_1)
|
|
2482
|
+
risk_2 = WR(a_2)
|
|
2483
|
+
elif rm == "VRG":
|
|
2484
|
+
risk_1 = VRG(a_1, alpha=alpha, beta=beta)
|
|
2485
|
+
risk_2 = VRG(a_2, alpha=alpha, beta=beta)
|
|
2486
|
+
elif rm == "CVRG":
|
|
2487
|
+
risk_1 = CVRG(a_1, alpha=alpha, beta=beta)
|
|
2488
|
+
risk_2 = CVRG(a_2, alpha=alpha, beta=beta)
|
|
2489
|
+
elif rm == "TGRG":
|
|
2490
|
+
risk_1 = TGRG(a_1, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
2491
|
+
risk_2 = TGRG(a_2, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
2492
|
+
elif rm == "EVRG":
|
|
2493
|
+
risk_1 = EVRG(a_1, alpha=alpha, beta=beta, solver=solver)
|
|
2494
|
+
risk_2 = EVRG(a_2, alpha=alpha, beta=beta, solver=solver)
|
|
2495
|
+
elif rm == "RVRG":
|
|
2496
|
+
risk_1 = RVRG(
|
|
2497
|
+
a_1, alpha=alpha, beta=beta, kappa=kappa, kappa_g=kappa_g, solver=solver
|
|
2498
|
+
)
|
|
2499
|
+
risk_2 = RVRG(
|
|
2500
|
+
a_2, alpha=alpha, beta=beta, kappa=kappa, kappa_g=kappa_g, solver=solver
|
|
2501
|
+
)
|
|
2502
|
+
elif rm == "RG":
|
|
2503
|
+
risk_1 = RG(a_1)
|
|
2504
|
+
risk_2 = RG(a_2)
|
|
2505
|
+
elif rm == "MDD":
|
|
2506
|
+
risk_1 = MDD_Abs(a_1)
|
|
2507
|
+
risk_2 = MDD_Abs(a_2)
|
|
2508
|
+
elif rm == "ADD":
|
|
2509
|
+
risk_1 = ADD_Abs(a_1)
|
|
2510
|
+
risk_2 = ADD_Abs(a_2)
|
|
2511
|
+
elif rm == "DaR":
|
|
2512
|
+
risk_1 = DaR_Abs(a_1, alpha=alpha)
|
|
2513
|
+
risk_2 = DaR_Abs(a_2, alpha=alpha)
|
|
2514
|
+
elif rm == "CDaR":
|
|
2515
|
+
risk_1 = CDaR_Abs(a_1, alpha=alpha)
|
|
2516
|
+
risk_2 = CDaR_Abs(a_2, alpha=alpha)
|
|
2517
|
+
elif rm == "EDaR":
|
|
2518
|
+
risk_1 = EDaR_Abs(a_1, alpha=alpha)[0]
|
|
2519
|
+
risk_2 = EDaR_Abs(a_2, alpha=alpha)[0]
|
|
2520
|
+
elif rm == "RLDaR":
|
|
2521
|
+
risk_1 = RLDaR_Abs(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2522
|
+
risk_2 = RLDaR_Abs(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2523
|
+
elif rm == "UCI":
|
|
2524
|
+
risk_1 = UCI_Abs(a_1)
|
|
2525
|
+
risk_2 = UCI_Abs(a_2)
|
|
2526
|
+
elif rm == "MDD_Rel":
|
|
2527
|
+
risk_1 = MDD_Rel(a_1)
|
|
2528
|
+
risk_2 = MDD_Rel(a_2)
|
|
2529
|
+
elif rm == "ADD_Rel":
|
|
2530
|
+
risk_1 = ADD_Rel(a_1)
|
|
2531
|
+
risk_2 = ADD_Rel(a_2)
|
|
2532
|
+
elif rm == "DaR_Rel":
|
|
2533
|
+
risk_1 = DaR_Rel(a_1, alpha=alpha)
|
|
2534
|
+
risk_2 = DaR_Rel(a_2, alpha=alpha)
|
|
2535
|
+
elif rm == "CDaR_Rel":
|
|
2536
|
+
risk_1 = CDaR_Rel(a_1, alpha=alpha)
|
|
2537
|
+
risk_2 = CDaR_Rel(a_2, alpha=alpha)
|
|
2538
|
+
elif rm == "EDaR_Rel":
|
|
2539
|
+
risk_1 = EDaR_Rel(a_1, alpha=alpha)[0]
|
|
2540
|
+
risk_2 = EDaR_Rel(a_2, alpha=alpha)[0]
|
|
2541
|
+
elif rm == "RLDaR_Rel":
|
|
2542
|
+
risk_1 = RLDaR_Rel(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2543
|
+
risk_2 = RLDaR_Rel(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2544
|
+
elif rm == "UCI_Rel":
|
|
2545
|
+
risk_1 = UCI_Rel(a_1)
|
|
2546
|
+
risk_2 = UCI_Rel(a_2)
|
|
2547
|
+
elif rm == "KT":
|
|
2548
|
+
risk_1 = Kurtosis(a_1) * 0.5
|
|
2549
|
+
risk_2 = Kurtosis(a_2) * 0.5
|
|
2550
|
+
elif rm == "SKT":
|
|
2551
|
+
risk_1 = SemiKurtosis(a_1) * 0.5
|
|
2552
|
+
risk_2 = SemiKurtosis(a_2) * 0.5
|
|
2553
|
+
|
|
2554
|
+
RC_i = (risk_1 - risk_2) / (2 * d_i) * w_[i, 0]
|
|
2555
|
+
RC.append(RC_i)
|
|
2556
|
+
|
|
2557
|
+
RC = np.array(RC, ndmin=1)
|
|
2558
|
+
|
|
2559
|
+
return RC
|
|
2560
|
+
|
|
2561
|
+
|
|
2562
|
+
def Risk_Margin(
|
|
2563
|
+
w,
|
|
2564
|
+
returns,
|
|
2565
|
+
cov=None,
|
|
2566
|
+
rm="MV",
|
|
2567
|
+
rf=0,
|
|
2568
|
+
alpha=0.05,
|
|
2569
|
+
a_sim=100,
|
|
2570
|
+
beta=None,
|
|
2571
|
+
b_sim=None,
|
|
2572
|
+
kappa=0.3,
|
|
2573
|
+
kappa_g=None,
|
|
2574
|
+
solver="CLARABEL",
|
|
2575
|
+
):
|
|
2576
|
+
r"""
|
|
2577
|
+
Calculate the risk margin for each asset based on the risk measure
|
|
2578
|
+
selected.
|
|
2579
|
+
|
|
2580
|
+
Parameters
|
|
2581
|
+
----------
|
|
2582
|
+
w : DataFrame or Series of shape (n_assets, 1)
|
|
2583
|
+
Portfolio weights, where n_assets is the number of assets.
|
|
2584
|
+
returns : DataFrame or nd-array of shape (n_samples, n_features)
|
|
2585
|
+
Features matrix, where n_samples is the number of samples and
|
|
2586
|
+
n_features is the number of features.
|
|
2587
|
+
cov : DataFrame of shape (n_assets, n_assets)
|
|
2588
|
+
Covariance matrix, where n_assets is the number of assets.
|
|
2589
|
+
rm : str, optional
|
|
2590
|
+
Risk measure used in the denominator of the ratio. The default is
|
|
2591
|
+
'MV'. Possible values are:
|
|
2592
|
+
|
|
2593
|
+
- 'MV': Standard Deviation.
|
|
2594
|
+
- 'KT': Square Root Kurtosis.
|
|
2595
|
+
- 'MAD': Mean Absolute Deviation.
|
|
2596
|
+
- 'GMD': Gini Mean Difference.
|
|
2597
|
+
- 'MSV': Semi Standard Deviation.
|
|
2598
|
+
- 'SKT': Square Root Semi Kurtosis.
|
|
2599
|
+
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
2600
|
+
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
2601
|
+
- 'VaR': Value at Risk.
|
|
2602
|
+
- 'CVaR': Conditional Value at Risk.
|
|
2603
|
+
- 'TG': Tail Gini.
|
|
2604
|
+
- 'EVaR': Entropic Value at Risk.
|
|
2605
|
+
- 'RLVaR': Relativistic Value at Risk. I recommend only use this function with MOSEK solver.
|
|
2606
|
+
- 'WR': Worst Realization (Minimax).
|
|
2607
|
+
- 'RG': Range of returns.
|
|
2608
|
+
- 'VRG' VaR range of returns.
|
|
2609
|
+
- 'CVRG': CVaR range of returns.
|
|
2610
|
+
- 'TGRG': Tail Gini range of returns.
|
|
2611
|
+
- 'EVRG': EVaR range of returns.
|
|
2612
|
+
- 'RVRG': RLVaR range of returns.
|
|
2613
|
+
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
|
2614
|
+
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
2615
|
+
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
|
2616
|
+
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
2617
|
+
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
2618
|
+
- 'RLDaR': Relativistic Drawdown at Risk of uncompounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2619
|
+
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
2620
|
+
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
|
2621
|
+
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
|
2622
|
+
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
|
2623
|
+
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
|
2624
|
+
- 'RLDaR_Rel': Relativistic Drawdown at Risk of compounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2625
|
+
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
|
2626
|
+
|
|
2627
|
+
rf : float, optional
|
|
2628
|
+
Risk free rate. The default is 0.
|
|
2629
|
+
alpha : float, optional
|
|
2630
|
+
Significance level of VaR, CVaR, EVaR, RLVaR, DaR, CDaR, EDaR, RLDaR
|
|
2631
|
+
and Tail Gini of losses. The default is 0.05.
|
|
2632
|
+
a_sim : float, optional
|
|
2633
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
2634
|
+
beta : float, optional
|
|
2635
|
+
Significance level of VaR, CVaR, Tail Gini, EVaR and RLVaR of gains. If
|
|
2636
|
+
None it duplicates alpha value. The default is None.
|
|
2637
|
+
b_sim : float, optional
|
|
2638
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it
|
|
2639
|
+
duplicates a_sim value.
|
|
2640
|
+
The default is None.
|
|
2641
|
+
kappa : float, optional
|
|
2642
|
+
Deformation parameter of RLVaR and RLDaR for losses, must be between 0 and 1.
|
|
2643
|
+
The default is 0.3.
|
|
2644
|
+
kappa_g : float, optional
|
|
2645
|
+
Deformation parameter of RLVaR and RLDaR for gains, must be between 0 and 1.
|
|
2646
|
+
The default is None.
|
|
2647
|
+
solver: str, optional
|
|
2648
|
+
Solver available for CVXPY that supports exponential and power cone
|
|
2649
|
+
programming. Used to calculate EVaR, EVRG, EDaR, RLVaR, RVRG and RLDaR.
|
|
2650
|
+
The default value is None.
|
|
2651
|
+
|
|
2652
|
+
Raises
|
|
2653
|
+
------
|
|
2654
|
+
ValueError
|
|
2655
|
+
When the value cannot be calculated.
|
|
2656
|
+
|
|
2657
|
+
Returns
|
|
2658
|
+
-------
|
|
2659
|
+
value : float
|
|
2660
|
+
Risk margin of the portfolio.
|
|
2661
|
+
|
|
2662
|
+
"""
|
|
2663
|
+
|
|
2664
|
+
w_ = np.array(w, ndmin=2)
|
|
2665
|
+
if w_.shape[0] == 1 and w_.shape[1] > 1:
|
|
2666
|
+
w_ = w_.T
|
|
2667
|
+
if w_.shape[0] > 1 and w_.shape[1] > 1:
|
|
2668
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2669
|
+
|
|
2670
|
+
if isinstance(returns, pd.Series):
|
|
2671
|
+
returns_ = returns.to_frame()
|
|
2672
|
+
elif isinstance(returns, pd.DataFrame):
|
|
2673
|
+
returns_ = returns.to_numpy()
|
|
2674
|
+
else:
|
|
2675
|
+
returns_ = np.array(returns, ndmin=2)
|
|
2676
|
+
|
|
2677
|
+
if cov is None:
|
|
2678
|
+
cov_ = np.array(np.cov(returns_, rowvar=False), ndmin=2)
|
|
2679
|
+
else:
|
|
2680
|
+
cov_ = np.array(cov, ndmin=2)
|
|
2681
|
+
|
|
2682
|
+
RM = []
|
|
2683
|
+
if rm in ["RLVaR", "RLDaR"]:
|
|
2684
|
+
d_i = 0.0001
|
|
2685
|
+
else:
|
|
2686
|
+
d_i = 0.0000001
|
|
2687
|
+
|
|
2688
|
+
for i in range(0, w_.shape[0]):
|
|
2689
|
+
delta = np.zeros((w_.shape[0], 1))
|
|
2690
|
+
delta[i, 0] = d_i
|
|
2691
|
+
w_1 = w_ + delta
|
|
2692
|
+
w_2 = w_ - delta
|
|
2693
|
+
a_1 = returns_ @ w_1
|
|
2694
|
+
a_2 = returns_ @ w_2
|
|
2695
|
+
if rm == "MV":
|
|
2696
|
+
risk_1 = w_1.T @ cov_ @ w_1
|
|
2697
|
+
risk_1 = np.sqrt(risk_1.item())
|
|
2698
|
+
risk_2 = w_2.T @ cov_ @ w_2
|
|
2699
|
+
risk_2 = np.sqrt(risk_2.item())
|
|
2700
|
+
elif rm == "MAD":
|
|
2701
|
+
risk_1 = MAD(a_1)
|
|
2702
|
+
risk_2 = MAD(a_2)
|
|
2703
|
+
elif rm == "GMD":
|
|
2704
|
+
risk_1 = GMD(a_1)
|
|
2705
|
+
risk_2 = GMD(a_2)
|
|
2706
|
+
elif rm == "MSV":
|
|
2707
|
+
risk_1 = SemiDeviation(a_1)
|
|
2708
|
+
risk_2 = SemiDeviation(a_2)
|
|
2709
|
+
elif rm == "FLPM":
|
|
2710
|
+
risk_1 = LPM(a_1, MAR=rf, p=1)
|
|
2711
|
+
risk_2 = LPM(a_2, MAR=rf, p=1)
|
|
2712
|
+
elif rm == "SLPM":
|
|
2713
|
+
risk_1 = LPM(a_1, MAR=rf, p=2)
|
|
2714
|
+
risk_2 = LPM(a_2, MAR=rf, p=2)
|
|
2715
|
+
elif rm == "VaR":
|
|
2716
|
+
risk_1 = VaR_Hist(a_1, alpha=alpha)
|
|
2717
|
+
risk_2 = VaR_Hist(a_2, alpha=alpha)
|
|
2718
|
+
elif rm == "CVaR":
|
|
2719
|
+
risk_1 = CVaR_Hist(a_1, alpha=alpha)
|
|
2720
|
+
risk_2 = CVaR_Hist(a_2, alpha=alpha)
|
|
2721
|
+
elif rm == "TG":
|
|
2722
|
+
risk_1 = TG(a_1, alpha=alpha, a_sim=a_sim)
|
|
2723
|
+
risk_2 = TG(a_2, alpha=alpha, a_sim=a_sim)
|
|
2724
|
+
elif rm == "EVaR":
|
|
2725
|
+
risk_1 = EVaR_Hist(a_1, alpha=alpha, solver=solver)[0]
|
|
2726
|
+
risk_2 = EVaR_Hist(a_2, alpha=alpha, solver=solver)[0]
|
|
2727
|
+
elif rm == "RLVaR":
|
|
2728
|
+
risk_1 = RLVaR_Hist(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2729
|
+
risk_2 = RLVaR_Hist(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2730
|
+
elif rm == "WR":
|
|
2731
|
+
risk_1 = WR(a_1)
|
|
2732
|
+
risk_2 = WR(a_2)
|
|
2733
|
+
elif rm == "VRG":
|
|
2734
|
+
risk_1 = VRG(a_1, alpha=alpha, beta=beta)
|
|
2735
|
+
risk_2 = VRG(a_2, alpha=alpha, beta=beta)
|
|
2736
|
+
elif rm == "CVRG":
|
|
2737
|
+
risk_1 = CVRG(a_1, alpha=alpha, beta=beta)
|
|
2738
|
+
risk_2 = CVRG(a_2, alpha=alpha, beta=beta)
|
|
2739
|
+
elif rm == "TGRG":
|
|
2740
|
+
risk_1 = TGRG(a_1, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
2741
|
+
risk_2 = TGRG(a_2, alpha=alpha, a_sim=a_sim, beta=beta, b_sim=b_sim)
|
|
2742
|
+
elif rm == "EVRG":
|
|
2743
|
+
risk_1 = EVRG(a_1, alpha=alpha, beta=beta, solver=solver)
|
|
2744
|
+
risk_2 = EVRG(a_2, alpha=alpha, beta=beta, solver=solver)
|
|
2745
|
+
elif rm == "RVRG":
|
|
2746
|
+
risk_1 = RVRG(
|
|
2747
|
+
a_1, alpha=alpha, beta=beta, kappa=kappa, kappa_g=kappa_g, solver=solver
|
|
2748
|
+
)
|
|
2749
|
+
risk_2 = RVRG(
|
|
2750
|
+
a_2, alpha=alpha, beta=beta, kappa=kappa, kappa_g=kappa_g, solver=solver
|
|
2751
|
+
)
|
|
2752
|
+
elif rm == "RG":
|
|
2753
|
+
risk_1 = RG(a_1)
|
|
2754
|
+
risk_2 = RG(a_2)
|
|
2755
|
+
elif rm == "MDD":
|
|
2756
|
+
risk_1 = MDD_Abs(a_1)
|
|
2757
|
+
risk_2 = MDD_Abs(a_2)
|
|
2758
|
+
elif rm == "ADD":
|
|
2759
|
+
risk_1 = ADD_Abs(a_1)
|
|
2760
|
+
risk_2 = ADD_Abs(a_2)
|
|
2761
|
+
elif rm == "DaR":
|
|
2762
|
+
risk_1 = DaR_Abs(a_1, alpha=alpha)
|
|
2763
|
+
risk_2 = DaR_Abs(a_2, alpha=alpha)
|
|
2764
|
+
elif rm == "CDaR":
|
|
2765
|
+
risk_1 = CDaR_Abs(a_1, alpha=alpha)
|
|
2766
|
+
risk_2 = CDaR_Abs(a_2, alpha=alpha)
|
|
2767
|
+
elif rm == "EDaR":
|
|
2768
|
+
risk_1 = EDaR_Abs(a_1, alpha=alpha, solver=solver)[0]
|
|
2769
|
+
risk_2 = EDaR_Abs(a_2, alpha=alpha, solver=solver)[0]
|
|
2770
|
+
elif rm == "RLDaR":
|
|
2771
|
+
risk_1 = RLDaR_Abs(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2772
|
+
risk_2 = RLDaR_Abs(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2773
|
+
elif rm == "UCI":
|
|
2774
|
+
risk_1 = UCI_Abs(a_1)
|
|
2775
|
+
risk_2 = UCI_Abs(a_2)
|
|
2776
|
+
elif rm == "MDD_Rel":
|
|
2777
|
+
risk_1 = MDD_Rel(a_1)
|
|
2778
|
+
risk_2 = MDD_Rel(a_2)
|
|
2779
|
+
elif rm == "ADD_Rel":
|
|
2780
|
+
risk_1 = ADD_Rel(a_1)
|
|
2781
|
+
risk_2 = ADD_Rel(a_2)
|
|
2782
|
+
elif rm == "DaR_Rel":
|
|
2783
|
+
risk_1 = DaR_Rel(a_1, alpha=alpha)
|
|
2784
|
+
risk_2 = DaR_Rel(a_2, alpha=alpha)
|
|
2785
|
+
elif rm == "CDaR_Rel":
|
|
2786
|
+
risk_1 = CDaR_Rel(a_1, alpha=alpha)
|
|
2787
|
+
risk_2 = CDaR_Rel(a_2, alpha=alpha)
|
|
2788
|
+
elif rm == "EDaR_Rel":
|
|
2789
|
+
risk_1 = EDaR_Rel(a_1, alpha=alpha, solver=solver)[0]
|
|
2790
|
+
risk_2 = EDaR_Rel(a_2, alpha=alpha, solver=solver)[0]
|
|
2791
|
+
elif rm == "RLDaR_Rel":
|
|
2792
|
+
risk_1 = RLDaR_Rel(a_1, alpha=alpha, kappa=kappa, solver=solver)
|
|
2793
|
+
risk_2 = RLDaR_Rel(a_2, alpha=alpha, kappa=kappa, solver=solver)
|
|
2794
|
+
elif rm == "UCI_Rel":
|
|
2795
|
+
risk_1 = UCI_Rel(a_1)
|
|
2796
|
+
risk_2 = UCI_Rel(a_2)
|
|
2797
|
+
elif rm == "KT":
|
|
2798
|
+
risk_1 = Kurtosis(a_1) * 0.5
|
|
2799
|
+
risk_2 = Kurtosis(a_2) * 0.5
|
|
2800
|
+
elif rm == "SKT":
|
|
2801
|
+
risk_1 = SemiKurtosis(a_1) * 0.5
|
|
2802
|
+
risk_2 = SemiKurtosis(a_2) * 0.5
|
|
2803
|
+
|
|
2804
|
+
RM_i = (risk_1 - risk_2) / (2 * d_i)
|
|
2805
|
+
RM.append(RM_i)
|
|
2806
|
+
|
|
2807
|
+
RM = np.array(RM, ndmin=1)
|
|
2808
|
+
|
|
2809
|
+
return RM
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
def Factors_Risk_Contribution(
|
|
2813
|
+
w,
|
|
2814
|
+
returns,
|
|
2815
|
+
factors,
|
|
2816
|
+
cov=None,
|
|
2817
|
+
B=None,
|
|
2818
|
+
const=False,
|
|
2819
|
+
rm="MV",
|
|
2820
|
+
rf=0,
|
|
2821
|
+
alpha=0.05,
|
|
2822
|
+
a_sim=100,
|
|
2823
|
+
beta=None,
|
|
2824
|
+
b_sim=None,
|
|
2825
|
+
kappa=0.3,
|
|
2826
|
+
kappa_g=None,
|
|
2827
|
+
solver="CLARABEL",
|
|
2828
|
+
feature_selection="stepwise",
|
|
2829
|
+
stepwise="Forward",
|
|
2830
|
+
criterion="pvalue",
|
|
2831
|
+
threshold=0.05,
|
|
2832
|
+
n_components=0.95,
|
|
2833
|
+
):
|
|
2834
|
+
r"""
|
|
2835
|
+
Calculate the risk contribution for each factor based on the selected risk measure.
|
|
2836
|
+
|
|
2837
|
+
Parameters
|
|
2838
|
+
----------
|
|
2839
|
+
w : DataFrame or Series of shape (n_assets, 1)
|
|
2840
|
+
Portfolio weights, where n_assets is the number of assets.
|
|
2841
|
+
returns : DataFrame or nd-array of shape (n_samples, n_features)
|
|
2842
|
+
Features matrix, where n_samples is the number of samples and
|
|
2843
|
+
n_features is the number of features.
|
|
2844
|
+
factors : DataFrame or nd-array of shape (n_samples, n_factors)
|
|
2845
|
+
Factors matrix, where n_samples is the number of samples and
|
|
2846
|
+
n_factors is the number of factors.
|
|
2847
|
+
cov : DataFrame of shape (n_assets, n_assets)
|
|
2848
|
+
Covariance matrix, where n_assets is the number of assets.
|
|
2849
|
+
B : DataFrame of shape (n_assets, n_factors), optional
|
|
2850
|
+
Loadings matrix, where n_assets is the number assets and n_factors is
|
|
2851
|
+
the number of risk factors. If is not specified, is estimated using
|
|
2852
|
+
stepwise regression. The default is None.
|
|
2853
|
+
const : bool, optional
|
|
2854
|
+
Indicate if the loadings matrix has a constant.
|
|
2855
|
+
The default is False.
|
|
2856
|
+
rm : str, optional
|
|
2857
|
+
Risk measure used in the denominator of the ratio. The default is
|
|
2858
|
+
'MV'. Possible values are:
|
|
2859
|
+
|
|
2860
|
+
- 'MV': Standard Deviation.
|
|
2861
|
+
- 'KT': Square Root Kurtosis.
|
|
2862
|
+
- 'MAD': Mean Absolute Deviation.
|
|
2863
|
+
- 'GMD': Gini Mean Difference.
|
|
2864
|
+
- 'MSV': Semi Standard Deviation.
|
|
2865
|
+
- 'SKT': Square Root Semi Kurtosis.
|
|
2866
|
+
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
2867
|
+
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
2868
|
+
- 'VaR': Value at Risk.
|
|
2869
|
+
- 'CVaR': Conditional Value at Risk.
|
|
2870
|
+
- 'TG': Tail Gini.
|
|
2871
|
+
- 'EVaR': Entropic Value at Risk.
|
|
2872
|
+
- 'RLVaR': Relativistic Value at Risk. I recommend only use this function with MOSEK solver.
|
|
2873
|
+
- 'WR': Worst Realization (Minimax).
|
|
2874
|
+
- 'RG': Range of returns.
|
|
2875
|
+
- 'VRG' VaR range of returns.
|
|
2876
|
+
- 'CVRG': CVaR range of returns.
|
|
2877
|
+
- 'TGRG': Tail Gini range of returns.
|
|
2878
|
+
- 'EVRG': EVaR range of returns.
|
|
2879
|
+
- 'RVRG': RLVaR range of returns. I recommend only use this function with MOSEK solver.
|
|
2880
|
+
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
|
2881
|
+
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
2882
|
+
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
|
2883
|
+
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
2884
|
+
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
2885
|
+
- 'RLDaR': Relativistic Drawdown at Risk of uncompounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2886
|
+
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
2887
|
+
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
|
2888
|
+
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
|
2889
|
+
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
|
2890
|
+
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
|
2891
|
+
- 'RLDaR_Rel': Relativistic Drawdown at Risk of compounded cumulative returns. I recommend only use this function with MOSEK solver.
|
|
2892
|
+
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
|
2893
|
+
|
|
2894
|
+
rf : float, optional
|
|
2895
|
+
Risk free rate. The default is 0.
|
|
2896
|
+
alpha : float, optional
|
|
2897
|
+
Significance level of VaR, CVaR, EVaR, RLVaR, DaR, CDaR, EDaR, RLDaR and Tail Gini of losses.
|
|
2898
|
+
The default is 0.05.
|
|
2899
|
+
a_sim : float, optional
|
|
2900
|
+
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
|
2901
|
+
beta : float, optional
|
|
2902
|
+
Significance level of CVaR and Tail Gini of gains. If None it duplicates alpha value.
|
|
2903
|
+
The default is None.
|
|
2904
|
+
b_sim : float, optional
|
|
2905
|
+
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
|
2906
|
+
The default is None.
|
|
2907
|
+
kappa : float, optional
|
|
2908
|
+
Deformation parameter of RLVaR and RLDaR for losses, must be between 0 and 1.
|
|
2909
|
+
The default is 0.3.
|
|
2910
|
+
kappa_g : float, optional
|
|
2911
|
+
Deformation parameter of RLVaR and RLDaR for gains, must be between 0 and 1.
|
|
2912
|
+
The default is None.
|
|
2913
|
+
solver: str, optional
|
|
2914
|
+
Solver available for CVXPY that supports power cone programming. Used to calculate RLVaR and RLDaR.
|
|
2915
|
+
The default value is None.
|
|
2916
|
+
feature_selection: str 'stepwise' or 'PCR', optional
|
|
2917
|
+
Indicate the method used to estimate the loadings matrix.
|
|
2918
|
+
The default is 'stepwise'.
|
|
2919
|
+
stepwise: str 'Forward' or 'Backward', optional
|
|
2920
|
+
Indicate the method used for stepwise regression.
|
|
2921
|
+
The default is 'Forward'.
|
|
2922
|
+
criterion : str, optional
|
|
2923
|
+
The default is 'pvalue'. Possible values of the criterion used to select
|
|
2924
|
+
the best features are:
|
|
2925
|
+
|
|
2926
|
+
- 'pvalue': select the features based on p-values.
|
|
2927
|
+
- 'AIC': select the features based on lowest Akaike Information Criterion.
|
|
2928
|
+
- 'SIC': select the features based on lowest Schwarz Information Criterion.
|
|
2929
|
+
- 'R2': select the features based on highest R Squared.
|
|
2930
|
+
- 'R2_A': select the features based on highest Adjusted R Squared.
|
|
2931
|
+
threshold : scalar, optional
|
|
2932
|
+
Is the maximum p-value for each variable that will be
|
|
2933
|
+
accepted in the model. The default is 0.05.
|
|
2934
|
+
n_components : int, float, None or str, optional
|
|
2935
|
+
if 1 < n_components (int), it represents the number of components that
|
|
2936
|
+
will be keep. if 0 < n_components < 1 (float), it represents the
|
|
2937
|
+
percentage of variance that the is explained by the components kept.
|
|
2938
|
+
See `PCA <https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html>`_
|
|
2939
|
+
for more details. The default is 0.95.
|
|
2940
|
+
|
|
2941
|
+
Raises
|
|
2942
|
+
------
|
|
2943
|
+
ValueError
|
|
2944
|
+
When the value cannot be calculated.
|
|
2945
|
+
|
|
2946
|
+
Returns
|
|
2947
|
+
-------
|
|
2948
|
+
value : float
|
|
2949
|
+
Risk measure of the portfolio.
|
|
2950
|
+
|
|
2951
|
+
"""
|
|
2952
|
+
w_ = np.array(w, ndmin=2)
|
|
2953
|
+
if w_.shape[0] == 1 and w_.shape[1] > 1:
|
|
2954
|
+
w_ = w_.T
|
|
2955
|
+
if w_.shape[0] > 1 and w_.shape[1] > 1:
|
|
2956
|
+
raise ValueError("weights must have n_assets x 1 size")
|
|
2957
|
+
|
|
2958
|
+
if returns.index.tolist() != factors.index.tolist():
|
|
2959
|
+
raise ValueError("returns and factors must have same dates.")
|
|
2960
|
+
|
|
2961
|
+
RM = Risk_Margin(
|
|
2962
|
+
w=w_,
|
|
2963
|
+
returns=returns,
|
|
2964
|
+
cov=cov,
|
|
2965
|
+
rm=rm,
|
|
2966
|
+
rf=rf,
|
|
2967
|
+
alpha=alpha,
|
|
2968
|
+
a_sim=a_sim,
|
|
2969
|
+
beta=beta,
|
|
2970
|
+
b_sim=b_sim,
|
|
2971
|
+
kappa=kappa,
|
|
2972
|
+
solver=solver,
|
|
2973
|
+
).reshape(-1, 1)
|
|
2974
|
+
|
|
2975
|
+
if B is None:
|
|
2976
|
+
B = pe.loadings_matrix(
|
|
2977
|
+
X=factors,
|
|
2978
|
+
Y=returns,
|
|
2979
|
+
feature_selection=feature_selection,
|
|
2980
|
+
stepwise=stepwise,
|
|
2981
|
+
criterion=criterion,
|
|
2982
|
+
threshold=threshold,
|
|
2983
|
+
n_components=n_components,
|
|
2984
|
+
)
|
|
2985
|
+
const = True
|
|
2986
|
+
elif not isinstance(B, pd.DataFrame):
|
|
2987
|
+
raise ValueError("B must be a DataFrame")
|
|
2988
|
+
|
|
2989
|
+
if const == True or factors.shape[1] + 1 == B.shape[1]:
|
|
2990
|
+
B = B.iloc[:, 1:].to_numpy()
|
|
2991
|
+
|
|
2992
|
+
if feature_selection == "PCR":
|
|
2993
|
+
scaler = StandardScaler()
|
|
2994
|
+
scaler.fit(factors)
|
|
2995
|
+
factors_std = scaler.transform(factors)
|
|
2996
|
+
if n_components > 0 and n_components < 1:
|
|
2997
|
+
pca = PCA(n_components=n_components)
|
|
2998
|
+
elif n_components >= 1:
|
|
2999
|
+
pca = PCA(n_components=int(n_components))
|
|
3000
|
+
pca.fit(factors_std)
|
|
3001
|
+
V_p = pca.components_.T
|
|
3002
|
+
std = np.array(np.std(factors, axis=0, ddof=1), ndmin=2)
|
|
3003
|
+
B = (pinv(V_p) @ (B.T * std.T)).T
|
|
3004
|
+
|
|
3005
|
+
B1 = pinv(B.T)
|
|
3006
|
+
B2 = pinv(null_space(B.T).T)
|
|
3007
|
+
B3 = pinv(B2.T)
|
|
3008
|
+
|
|
3009
|
+
RC_F = (B.T @ w_) * (B1.T @ RM)
|
|
3010
|
+
RC_OF = np.array(((B2.T @ w_) * (B3.T @ RM)).sum(), ndmin=2)
|
|
3011
|
+
RC_F = np.vstack([RC_F, RC_OF]).ravel()
|
|
3012
|
+
|
|
3013
|
+
return RC_F
|
|
3014
|
+
|
|
3015
|
+
|
|
3016
|
+
def BrinsonAttribution(
|
|
3017
|
+
prices,
|
|
3018
|
+
w,
|
|
3019
|
+
wb,
|
|
3020
|
+
start,
|
|
3021
|
+
end,
|
|
3022
|
+
asset_classes,
|
|
3023
|
+
classes_col,
|
|
3024
|
+
method="nearest",
|
|
3025
|
+
):
|
|
3026
|
+
r"""
|
|
3027
|
+
Creates a DataFrame with the Brinson Performance Attribution per class and
|
|
3028
|
+
aggregate based on :cite:`f-Brinson1985`.
|
|
3029
|
+
|
|
3030
|
+
Parameters
|
|
3031
|
+
----------
|
|
3032
|
+
prices : DataFrame of shape (n_samples, n_assets)
|
|
3033
|
+
Assets prices DataFrame, where n_samples is the number of
|
|
3034
|
+
observations and n_assets is the number of assets.
|
|
3035
|
+
w : DataFrame or Series of shape (n_assets, 1)
|
|
3036
|
+
A portfolio specified by the user.
|
|
3037
|
+
wb : DataFrame or Series of shape (n_assets, 1)
|
|
3038
|
+
A benchmark specified by the user.
|
|
3039
|
+
start : str
|
|
3040
|
+
Start date in format 'YYYY-MM-DD' specified by the user.
|
|
3041
|
+
end : str
|
|
3042
|
+
End date in format 'YYYY-MM-DD' specified by the user.
|
|
3043
|
+
asset_classes : DataFrame of shape (n_assets, n_cols)
|
|
3044
|
+
Asset's classes DataFrame, where n_assets is the number of assets and
|
|
3045
|
+
n_cols is the number of columns of the DataFrame where the first column
|
|
3046
|
+
is the asset list and the next columns are the different asset's
|
|
3047
|
+
classes sets. It is only used when kind value is 'classes'. The default
|
|
3048
|
+
value is None.
|
|
3049
|
+
classes_col : str or int
|
|
3050
|
+
If value is str, it is the column name of the set of classes from
|
|
3051
|
+
asset_classes dataframe. If value is int, it is the column number of
|
|
3052
|
+
the set of classes from asset_classes dataframe. The default
|
|
3053
|
+
value is None.
|
|
3054
|
+
method : str
|
|
3055
|
+
Method used to calculate the nearest start or end dates in case one of
|
|
3056
|
+
them is not in prices DataFrame. The default value is 'nearest'.
|
|
3057
|
+
See `get_indexer <https://pandas.pydata.org/docs/reference/api/pandas.Index.get_indexer.html#pandas.Index.get_indexer>`__ for more details.
|
|
3058
|
+
|
|
3059
|
+
Raises
|
|
3060
|
+
------
|
|
3061
|
+
ValueError
|
|
3062
|
+
When the value cannot be calculated.
|
|
3063
|
+
|
|
3064
|
+
Returns
|
|
3065
|
+
-------
|
|
3066
|
+
BrinAttr : DataFrame
|
|
3067
|
+
A DataFrame with the Brinson Performance Attribution per class and aggregate.
|
|
3068
|
+
|
|
3069
|
+
(start_, end_) : tuple
|
|
3070
|
+
Start and end dates calculated using get_indexer method in string format.
|
|
3071
|
+
|
|
3072
|
+
|
|
3073
|
+
Example
|
|
3074
|
+
-------
|
|
3075
|
+
::
|
|
3076
|
+
|
|
3077
|
+
BrinAttr, (start, end) = BrinsonAttribution(
|
|
3078
|
+
prices=data,
|
|
3079
|
+
w=w,
|
|
3080
|
+
wb=wb,
|
|
3081
|
+
start='2019-01-07',
|
|
3082
|
+
end='2019-12-06',
|
|
3083
|
+
asset_classes=asset_classes,
|
|
3084
|
+
classes_col='Industry',
|
|
3085
|
+
)
|
|
3086
|
+
|
|
3087
|
+
.. image:: images/BrinAttr.png
|
|
3088
|
+
|
|
3089
|
+
|
|
3090
|
+
"""
|
|
3091
|
+
|
|
3092
|
+
if not isinstance(prices, pd.DataFrame):
|
|
3093
|
+
raise ValueError("prices must be a DataFrame")
|
|
3094
|
+
|
|
3095
|
+
if not isinstance(w, pd.DataFrame):
|
|
3096
|
+
if isinstance(w, pd.Series):
|
|
3097
|
+
wp_ = w.to_frame()
|
|
3098
|
+
else:
|
|
3099
|
+
raise ValueError("w must be a one column DataFrame or Series")
|
|
3100
|
+
else:
|
|
3101
|
+
if w.shape[0] == 1:
|
|
3102
|
+
wp_ = w.T.copy()
|
|
3103
|
+
elif w.shape[1] == 1:
|
|
3104
|
+
wp_ = w.copy()
|
|
3105
|
+
else:
|
|
3106
|
+
raise ValueError("w must be a one column DataFrame or Series")
|
|
3107
|
+
|
|
3108
|
+
if not isinstance(wb, pd.DataFrame):
|
|
3109
|
+
if isinstance(wb, pd.Series):
|
|
3110
|
+
wb_ = wb.to_frame()
|
|
3111
|
+
else:
|
|
3112
|
+
raise ValueError("w must be a one column DataFrame or Series")
|
|
3113
|
+
else:
|
|
3114
|
+
if wb.shape[0] == 1:
|
|
3115
|
+
wb_ = wb.T.copy()
|
|
3116
|
+
elif wb.shape[1] == 1:
|
|
3117
|
+
wb_ = wb.copy()
|
|
3118
|
+
else:
|
|
3119
|
+
raise ValueError("w must be a one column DataFrame or Series")
|
|
3120
|
+
|
|
3121
|
+
if not isinstance(asset_classes, pd.DataFrame):
|
|
3122
|
+
raise ValueError("asset_classes must be a DataFrame")
|
|
3123
|
+
else:
|
|
3124
|
+
if asset_classes.shape[1] < 2:
|
|
3125
|
+
raise ValueError("asset_classes must have at least two columns")
|
|
3126
|
+
classes = asset_classes.columns.tolist()
|
|
3127
|
+
if isinstance(classes_col, str) and classes_col in classes:
|
|
3128
|
+
col = classes_col
|
|
3129
|
+
elif isinstance(classes_col, int) and classes[classes_col] in classes:
|
|
3130
|
+
col = classes[classes_col]
|
|
3131
|
+
else:
|
|
3132
|
+
raise ValueError(
|
|
3133
|
+
"classes_col must be a valid column or column position of asset_classes"
|
|
3134
|
+
)
|
|
3135
|
+
|
|
3136
|
+
prices_ = prices.copy()
|
|
3137
|
+
prices_.index = prices_.index.tz_localize(None)
|
|
3138
|
+
|
|
3139
|
+
start_ = prices_.index.get_indexer([pd.Timestamp(start)], method=method)
|
|
3140
|
+
end_ = prices_.index.get_indexer([pd.Timestamp(end)], method=method)
|
|
3141
|
+
|
|
3142
|
+
p1 = prices_.iloc[start_].to_numpy().reshape(-1, 1)
|
|
3143
|
+
p2 = prices_.iloc[end_].to_numpy().reshape(-1, 1)
|
|
3144
|
+
p3 = p2 / p1 - 1
|
|
3145
|
+
|
|
3146
|
+
wp_ = wp_.to_numpy().reshape(-1, 1)
|
|
3147
|
+
wb_ = wb_.to_numpy().reshape(-1, 1)
|
|
3148
|
+
|
|
3149
|
+
Rp = (p3.T @ wp_).item()
|
|
3150
|
+
Rb = (p3.T @ wb_).item()
|
|
3151
|
+
|
|
3152
|
+
classes = asset_classes[col].tolist()
|
|
3153
|
+
unique_classes = list(set(classes))
|
|
3154
|
+
unique_classes.sort()
|
|
3155
|
+
|
|
3156
|
+
labels = [
|
|
3157
|
+
"Asset Allocation",
|
|
3158
|
+
"Security Selection",
|
|
3159
|
+
"Interaction",
|
|
3160
|
+
"Total Excess Return",
|
|
3161
|
+
]
|
|
3162
|
+
BrinAttr = pd.DataFrame([], index=labels)
|
|
3163
|
+
|
|
3164
|
+
for i in unique_classes:
|
|
3165
|
+
sets_i = []
|
|
3166
|
+
for j in classes:
|
|
3167
|
+
sets_i.append(i == j)
|
|
3168
|
+
sets_i = np.array(sets_i, dtype=int).reshape(-1, 1)
|
|
3169
|
+
|
|
3170
|
+
wb_i = (sets_i.T @ wb_).item()
|
|
3171
|
+
wp_i = (sets_i.T @ wp_).item()
|
|
3172
|
+
|
|
3173
|
+
Rb_i = (np.multiply(p3, sets_i).T @ wb_).item() / wb_i
|
|
3174
|
+
Rp_i = (np.multiply(p3, sets_i).T @ wp_).item() / wp_i
|
|
3175
|
+
|
|
3176
|
+
AAE_i = (wp_i - wb_i) * (Rb_i - Rb)
|
|
3177
|
+
SSE_i = wb_i * (Rp_i - Rb_i)
|
|
3178
|
+
IE_i = (wp_i - wb_i) * (Rp_i - Rb_i)
|
|
3179
|
+
TER_i = AAE_i + SSE_i + IE_i
|
|
3180
|
+
|
|
3181
|
+
BrinAttr_i = pd.DataFrame(
|
|
3182
|
+
[AAE_i, SSE_i, IE_i, TER_i], index=labels, columns=[i]
|
|
3183
|
+
)
|
|
3184
|
+
|
|
3185
|
+
BrinAttr = pd.concat([BrinAttr, BrinAttr_i], axis=1)
|
|
3186
|
+
|
|
3187
|
+
total = BrinAttr.sum(axis=1).to_frame()
|
|
3188
|
+
total.columns = ["Total"]
|
|
3189
|
+
|
|
3190
|
+
BrinAttr = pd.concat([BrinAttr, total], axis=1)
|
|
3191
|
+
|
|
3192
|
+
start_ = prices_.index.tolist()[start_.item()].strftime("%Y-%m-%d")
|
|
3193
|
+
end_ = prices_.index.tolist()[end_.item()].strftime("%Y-%m-%d")
|
|
3194
|
+
|
|
3195
|
+
return BrinAttr, (start_, end_)
|