import numpy as np
from scipy.optimize import minimize
from scipy.optimize import Bounds
__all__ = [
"MAD",
"SemiDeviation",
"VaR_Hist",
"CVaR_Hist",
"WR",
"LPM",
"Entropic_RM",
"EVaR_Hist",
"MaxAbsDD",
"AvgAbsDD",
"ConAbsDD",
"MaxRelDD",
"AvgRelDD",
"ConRelDD",
"Sharpe_Risk",
"Sharpe",
]
[docs]def MAD(X):
r"""
Calculates the Mean Absolute Deviation (MAD) of a returns series.
.. math::
\text{MAD}(X) = \frac{1}{T}\sum_{t=1}^{T}
| X_{t} - \mathbb{E}(X_{t}) |
Parameters
----------
X : 1d-array
a returns series, must have Tx1 size.
Returns
-------
value : float
MAD of a returns series.
Raises
------
ValueError
When the value cannot be calculated.
Examples
--------
Examples should be written in doctest format, and should illustrate how
to use the function.
>>> print([i for i in example_generator(4)])
[0, 1, 2, 3]
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
value = np.mean(np.absolute(a - np.mean(a, axis=0)), axis=0)
value = value.item()
return value
[docs]def SemiDeviation(X):
r"""
Calculates the Semi Deviation of a returns series.
.. math::
\text{SemiDev}(X) = \left [ \frac{1}{T-1}\sum_{t=1}^{T}
(X_{t} - \mathbb{E}(X_{t}))^2 \right ]^{1/2}
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
Semi Deviation of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
mu = np.mean(a, axis=0)
value = mu - a
n = value.shape[0] - 1
value = np.sum(np.power(value[np.where(value <= mu)], 2)) / n
value = np.power(value, 0.5).item()
return value
[docs]def VaR_Hist(X, alpha=0.01):
r"""
Calculates the Value at Risk (VaR) of a returns series.
.. math::
\text{VaR}_{\alpha}(X) = -\inf_{t \in (0,T)} \left \{ X_{t} \in
\mathbb{R}: F_{X}(X_{t})>\alpha \right \}
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
alpha : float, optional
Significance level of VaR. The default is 0.01.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
VaR of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
sorted_a = np.sort(a, axis=0)
index = int(np.ceil(alpha * len(sorted_a)) - 1)
value = -sorted_a[index]
value = value.item()
return value
[docs]def CVaR_Hist(X, alpha=0.01):
r"""
Calculates the Conditional Value at Risk (CVaR) of a returns series.
.. math::
\text{CVaR}_{\alpha}(X) = \text{VaR}_{\alpha}(X) +
\frac{1}{\alpha T} \sum_{t=1}^{T} \max(-X_{t} -
\text{VaR}_{\alpha}(X), 0)
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
alpha : float, optional
Significance level of CVaR. The default is 0.01.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
CVaR of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
sorted_a = np.sort(a, axis=0)
index = int(np.ceil(alpha * len(sorted_a)) - 1)
sum_var = 0
for i in range(0, index + 1):
sum_var = sum_var + sorted_a[i] - sorted_a[index]
value = -sorted_a[index] - sum_var / (alpha * len(sorted_a))
value = value.item()
return value
[docs]def WR(X):
r"""
Calculates the Worst Realization (WR) or Worst Scenario of a returns series.
.. math::
\text{WR}(X) = \max(-X)
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
WR of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
sorted_a = np.sort(a, axis=0)
value = -sorted_a[0]
value = value.item()
return value
[docs]def LPM(X, MAR=0, p=1):
r"""
Calculates the p-th Lower Partial Moment of a returns series.
.. math::
\text{LPM}(X, \text{MAR}, p) = \left [ \frac{1}{T}\sum_{t=1}^{T}
\max(\text{MAR} - X_{t}, 0) \right ]^{\frac{1}{p}}
Where:
:math:`\text{MAR}` is the minimum acceptable return.
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
MAR : float, optional
Minimum acceptable return. The default is 0.
p : float, optional
order of the :math:`\text{LPM}`. The default is 1.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
p-th Lower Partial Moment of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
value = MAR - a
if p > 1:
n = value.shape[0] - 1
else:
n = value.shape[0]
value = np.sum(np.power(value[np.where(value > 0)], p)) / n
value = np.power(value, 1 / p).item()
return value
[docs]def Entropic_RM(X, theta=1):
r"""
Calculates the Entropic Risk Measure (ERM) of a returns series.
.. math::
\text{ERM}(X) = \theta \log\left(\mathbb{E}
[e^{-\frac{1}{\theta} X}]\right)
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
theta : float, optional
Risk aversion parameter, must be greater than zero. The default is 1.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
ERM of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
value = np.mean(np.exp(-1 / theta * np.array(a)), axis=0)
value = theta * (np.log(value))
value = value.item()
return value
def _Entropic_RM(X, theta=1, alpha=0.01):
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
value = np.mean(np.exp(-1 / theta * np.array(a)), axis=0)
value = theta * (np.log(value) - np.log(alpha))
value = value.item()
return value
[docs]def EVaR_Hist(X, alpha=0.01):
r"""
Calculates the Entropic Value at Risk (EVaR) of a returns series.
.. math::
\text{EVaR}_{\alpha}(X) = \inf_{z>0} \left \{ z^{-1}
\ln \left (\frac{M_X(z)}{\alpha} \right ) \right \}
Where:
:math:`M_X(z)` is the moment generating function of X.
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
alpha : float, optional
Significance level of EVaR. The default is 0.01.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
EVaR of a returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
bnd = Bounds([0.00000000001], [np.inf])
result = minimize(_Entropic_RM, [0.01], args=(X, alpha), bounds=bnd)
t = result.x
t = t.item()
value = _Entropic_RM(t, X, alpha)
return value
[docs]def MaxAbsDD(X):
r"""
Calculates the Maximum Drawdown (MDD) of a returns series
using uncumpound cumulated returns.
.. math::
\text{MDD}(X) = \max_{j \in (0,T)} \left [\max_{t \in (0,T)}
\left ( \sum_{i=0}^{t}X_{i} - \sum_{i=0}^{j}X_{i} \right ) \right ]
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
MDD of a uncumpound cumulated returns.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
prices = np.insert(np.array(a), 0, 1, axis=0)
NAV = np.cumsum(np.array(prices), axis=0)
value = 0
peak = -99999
for i in NAV:
if i > peak:
peak = i
DD = peak - i
if DD > value:
value = DD
value = value.item()
return value
[docs]def AvgAbsDD(X):
r"""
Calculates the Average Drawdown (ADD) of a returns series
using uncumpound cumulated returns.
.. math::
\text{ADD}(X) = \frac{1}{T}\sum_{i=0}^{T}\max_{t \in (0,T)}
\left ( \sum_{i=0}^{t}X_{i} - \sum_{i=0}^{j}X_{i} \right )
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
ADD of a uncumpound cumulated returns.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
prices = np.insert(np.array(a), 0, 1, axis=0)
NAV = np.cumsum(np.array(prices), axis=0)
value = 0
peak = -99999
n = 0
for i in NAV:
if i > peak:
peak = i
DD = peak - i
if DD > 0:
value += DD
n += 1
if n == 0:
value = 0
else:
value = value / n
value = value.item()
return value
[docs]def ConAbsDD(X, alpha=0.01):
r"""
Calculates the Conditional Drawdown at Risk (CDaR) of a returns series
using uncumpound cumulated returns.
.. math::
\text{CDaR}_{\alpha}(X) = \text{DaR}_{\alpha}(X) + \frac{1}{\alpha T}
\sum_{i=0}^{T} \max \left [ \max_{t \in (0,T)}
\left ( \sum_{i=0}^{t}X_{i} - \sum_{i=0}^{j}X_{i} \right )
- \text{DaR}_{\alpha}(X), 0 \right ]
Where:
:math:`\text{DaR}_{\alpha}` is the Drawdown at Risk of an uncumpound
cumulated return series :math:`X`.
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size..
alpha : float, optional
Significance level of CDaR. The default is 0.01.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
CDaR of a uncumpound cumulated returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
prices = np.insert(np.array(a), 0, 1, axis=0)
NAV = np.cumsum(np.array(prices), axis=0)
DD = []
peak = -99999
for i in NAV:
if i > peak:
peak = i
DD.append(-(peak - i))
del DD[0]
sorted_DD = np.sort(np.array(DD), axis=0)
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
sum_var = 0
for i in range(0, index + 1):
sum_var = sum_var + sorted_DD[i] - sorted_DD[index]
value = -sorted_DD[index] - sum_var / (alpha * len(sorted_DD))
value = value.item()
return value
[docs]def MaxRelDD(X):
r"""
Calculates the Maximum Drawdown (MDD) of a returns series
using cumpound cumulated returns.
.. math::
\text{MDD}(X) = \max_{j \in (0,T)}\left[\max_{t \in (0,T)}
\left ( \prod_{i=0}^{t}(1+X_{i}) - \prod_{i=0}^{j}(1+X_{i}) \right ) \right]
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
MDD of a cumpound cumulated returns.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
NAV = np.cumprod(prices, axis=0)
value = 0
peak = -99999
for i in NAV:
if i > peak:
peak = i
DD = (peak - i) / peak
if DD > value:
value = DD
value = value.item()
return value
[docs]def AvgRelDD(X):
r"""
Calculates the Average Drawdown (ADD) of a returns series
using cumpound acumulated returns.
.. math::
\text{ADD}(X) = \frac{1}{T}\sum_{i=0}^{T}\max_{t \in (0,T)}
\left ( \prod_{i=0}^{t}(1+X_{i}) - \prod_{i=0}^{j}(1+X_{i}) \right )
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
ADD of a cumpound acumulated returns.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("returns must have Tx1 size")
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
NAV = np.cumprod(prices, axis=0)
value = 0
peak = -99999
n = 0
for i in NAV:
if i > peak:
peak = i
DD = (peak - i) / peak
if DD > 0:
value += DD
n += 1
if n == 0:
value = 0
else:
value = value / n
value = value.item()
return value
[docs]def ConRelDD(X, alpha=0.01):
r"""
Calculates the Conditional Drawdown at Risk (CDaR) of a returns series
using cumpound cumulated returns.
.. math::
\text{CDaR}_{\alpha}(X) = \text{DaR}_{\alpha}(X) + \frac{1}{\alpha T}
\sum_{i=0}^{T} \max \left [ \max_{t \in (0,T)}
\left ( \prod_{i=0}^{t}(1+X_{i}) - \prod_{i=0}^{j}(1+X_{i}) \right )
- \text{DaR}_{\alpha}(X), 0 \right ]
Where:
:math:`\text{DaR}_{\alpha}` is the Drawdown at Risk of a cumpound
acumulated return series :math:`X`.
Parameters
----------
X : 1d-array
Returns series, must have Tx1 size..
alpha : float, optional
Significance level of CDaR. The default is 0.01.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
CDaR of a cumpound cumulated returns series.
"""
a = np.matrix(X)
if a.shape[0] == 1 and a.shape[1] > 1:
a = a.T
if a.shape[0] > 1 and a.shape[1] > 1:
raise ValueError("X must have Tx1 size")
prices = 1 + np.insert(np.array(a), 0, 0, axis=0)
NAV = np.cumprod(prices, axis=0)
DD = []
peak = -99999
for i in NAV:
if i > peak:
peak = i
DD.append(-(peak - i) / peak)
del DD[0]
sorted_DD = np.sort(np.array(DD), axis=0)
index = int(np.ceil(alpha * len(sorted_DD)) - 1)
sum_var = 0
for i in range(0, index + 1):
sum_var = sum_var + sorted_DD[i] - sorted_DD[index]
value = -sorted_DD[index] - sum_var / (alpha * len(sorted_DD))
value = value.item()
return value
###############################################################################
# Risk Adjusted Return Ratios
###############################################################################
[docs]def Sharpe_Risk(w, cov=None, returns=None, rm="MV", rf=0, alpha=0.01):
r"""
Calculate the risk measure available on the Sharpe function.
Parameters
----------
w : DataFrame or 1d-array of shape (n_assets, 1)
Weights matrix, where n_assets is the number of assets.
cov : DataFrame or nd-array of shape (n_features, n_features)
Covariance matrix, where n_features is the number of features.
returns : DataFrame or nd-array of shape (n_samples, n_features)
Features matrix, where n_samples is the number of samples and
n_features is the number of features.
rm : str, optional
Risk measure used in the denominator of the ratio. The default is
'MV'. Posible values are:
- 'MV': Standard Deviation.
- 'MAD': Mean Absolute Deviation.
- 'MSV': Semi Standard Deviation.
- 'FLPM': First Lower Partial Moment (Omega Ratio).
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
- 'VaR': Value at Risk.
- 'CVaR': Conditional Value at Risk.
- 'WR': Worst Realization (Minimax)
- 'MDD': Maximum Drawdown of uncompounded returns (Calmar Ratio).
- 'ADD': Average Drawdown of uncompounded returns.
- 'CDaR': Conditional Drawdown at Risk of uncompounded returns.
rf : float, optional
Risk free rate. The default is 0.
**kwargs : dict
Other arguments that depends on the risk measure.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
Risk measure of the portfolio.
"""
w_ = np.matrix(w)
if cov is not None:
cov_ = np.matrix(cov)
if returns is not None:
returns_ = np.matrix(returns)
a = returns_ * w_
if rm == "MV":
risk = w_.T * cov_ * w_
risk = np.sqrt(risk.item())
elif rm == "MAD":
risk = MAD(a)
elif rm == "MSV":
risk = SemiDeviation(a)
elif rm == "FLPM":
risk = LPM(a, MAR=rf, p=1)
elif rm == "SLPM":
risk = LPM(a, MAR=rf, p=2)
elif rm == "VaR":
risk = VaR_Hist(a, alpha=alpha)
elif rm == "CVaR":
risk = CVaR_Hist(a, alpha=alpha)
elif rm == "WR":
risk = WR(a)
elif rm == "MDD":
risk = MaxAbsDD(a)
elif rm == "ADD":
risk = AvgAbsDD(a)
elif rm == "CDaR":
risk = ConAbsDD(a, alpha=alpha)
value = risk
return value
[docs]def Sharpe(w, mu, cov=None, returns=None, rm="MV", rf=0, alpha=0.01):
r"""
Calculate the Risk Adjusted Return Ratio from a portfolio returns series.
.. math::
\text{Sharpe}(X) = \frac{\mathbb{E}(X) -
r_{f}}{\phi(X)}
Where:
:math:`X` is the vector of portfolio returns.
:math:`r_{f}` is the risk free rate, when the risk measure is
:math:`\text{LPM}` uses instead of :math:`r_{f}` the :math:`\text{MAR}`.
:math:`\phi(X)` is a convex risk measure. The risk measures availabe are:
Parameters
----------
w : DataFrame or 1d-array of shape (n_assets, 1)
Weights matrix, where n_assets is the number of assets.
mu : DataFrame or nd-array of shape (1, n_assets)
Vector of expected returns, where n_assets is the number of assets.
cov : DataFrame or nd-array of shape (n_features, n_features)
Covariance matrix, where n_features is the number of features.
returns : DataFrame or nd-array of shape (n_samples, n_features)
Features matrix, where n_samples is the number of samples and
n_features is the number of features.
rm : str, optional
Risk measure used in the denominator of the ratio. The default is
'MV'. Posible values are:
- 'MV': Standard Deviation.
- 'MAD': Mean Absolute Deviation.
- 'MSV': Semi Standard Deviation.
- 'FLPM': First Lower Partial Moment (Omega Ratio).
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
- 'VaR': Value at Risk.
- 'CVaR': Conditional Value at Risk.
- 'WR': Worst Realization (Minimax)
- 'MDD': Maximum Drawdown of uncompounded returns (Calmar Ratio).
- 'ADD': Average Drawdown of uncompounded returns.
- 'CDaR': Conditional Drawdown at Risk of uncompounded returns.
rf : float, optional
Risk free rate. The default is 0.
**kwargs : dict
Other arguments that depends on the risk measure.
Raises
------
ValueError
When the value cannot be calculated.
Returns
-------
value : float
Risk adjusted return ratio of :math:`X`.
"""
if cov is None and rm == "MV":
raise ValueError("covariance matrix is necessary to calculate the sharpe ratio")
elif returns is None and rm != "MV":
raise ValueError(
"returns scenarios are necessary to calculate the sharpe ratio"
)
w_ = np.matrix(w)
mu_ = np.matrix(mu)
if cov is not None:
cov_ = np.matrix(cov)
if returns is not None:
returns_ = np.matrix(returns)
ret = mu_ * w_
ret = ret.item()
risk = Sharpe_Risk(w, cov=cov_, returns=returns_, rm=rm, rf=rf, alpha=alpha)
value = (ret - rf) / risk
return value