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.
@@ -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_)