RiskFunctions — Riskfolio-Lib 0.1 documentation

# Source code for RiskFunctions

    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",
    ]

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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

    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
    ###############################################################################

    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

    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