Ejemplo n.º 1
0
    def optimization(
        self, model="Classic", rm="MV", obj="Sharpe", rf=0, l=2, hist=True
    ):
        r"""
        This method that calculates the optimum portfolio according to the
        optimization model selected by the user. The general problem that
        solves is:
        
        .. math::
            \begin{align}
            &\underset{x}{\text{optimize}} & & F(w)\\
            &\text{s. t.} & & Aw \geq B\\
            & & & \phi_{i}(w) \leq c_{i}\\
            \end{align}
        
        Where:
            
        :math:`F(w)` is the objective function.
    
        :math:`Aw \geq B` is a set of linear constraints.
    
        :math:`\phi_{i}(w) \leq c_{i}` are constraints on maximum values of
        several risk measures.
        
        Parameters
        ----------
        model : str can be 'Classic', 'BL' or 'FM'
            The model used for optimize the portfolio.
            The default is 'Classic'. Posible values are:

            - 'Classic': use estimates of expected return vector and covariance matrix that depends on historical data.
            - 'BL': use estimates of expected return vector and covariance matrix based on the Black Litterman model.
            - 'FM': use estimates of expected return vector and covariance matrix based on a Risk Factor model specified by the user.
            
        rm : str, optional
            The risk measure used to optimze the portfolio.
            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).
            - '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.
            
        obj : str can be {'MinRisk', 'Utility', 'Sharpe' or 'MaxRet'.
            Objective function of the optimization model.
            The default is 'Sharpe'. Posible values are:

            - 'MinRisk': Minimize the selected risk measure.
            - 'Utility': Maximize the Utility function :math:`mu w - l \phi_{i}(w)`.
            - 'Sharpe': Maximize the risk adjusted return ratio based on the selected risk measure.
            - 'MaxRet': Maximize the expected return of the portfolio.
                
        rf : float, optional
            Risk free rate, must be in the same period of assets returns.
            The default is 0.
        l : scalar, optional
            Risk aversion factor of the 'Utility' objective function.
            The default is 2.
        hist : bool, optional
            Indicate if uses historical or factor estimation of returns to 
            calculate risk measures that depends on scenarios (All except
            'MV' risk measure). The default is True.

        Returns
        -------
        w : DataFrame
            The weights of optimum portfolio.

        """

        # General model Variables

        mu = None
        sigma = None
        returns = None
        if model == "Classic":
            mu = np.matrix(self.mu)
            sigma = np.matrix(self.cov)
            returns = np.matrix(self.returns)
            nav = np.matrix(self.nav)
        elif model == "FM":
            mu = np.matrix(self.mu_fm)
            if hist == False:
                sigma = np.matrix(self.cov_fm)
                returns = np.matrix(self.returns_fm)
                nav = np.matrix(self.nav_fm)
            elif hist == True:
                sigma = np.matrix(self.cov)
                returns = np.matrix(self.returns)
                nav = np.matrix(self.nav)
        elif model == "BL":
            mu = np.matrix(self.mu_bl)
            if hist == False:
                sigma = np.matrix(self.cov_bl)
            elif hist == True:
                sigma = np.matrix(self.cov)
            returns = np.matrix(self.returns)
            nav = np.matrix(self.nav)
        elif model == "BL_FM":
            mu = np.matrix(self.mu_bl_fm)
            if hist == False:
                sigma = np.matrix(self.cov_bl_fm)
                returns = np.matrix(self.returns_fm)
                nav = np.matrix(self.nav_fm)
            elif hist == True:
                sigma = np.matrix(self.cov)
                returns = np.matrix(self.returns)
                nav = np.matrix(self.nav)

        # General Model Variables

        returns = np.matrix(returns)
        w = cv.Variable((mu.shape[1], 1))
        k = cv.Variable((1, 1))
        rf0 = cv.Parameter(nonneg=True)
        rf0.value = rf
        n = cv.Parameter(nonneg=True)
        n.value = returns.shape[0]
        ret = mu * w

        # MV Model Variables

        risk1 = cv.quad_form(w, sigma)
        returns_1 = af.cov_returns(sigma) * 1000
        n1 = cv.Parameter(nonneg=True)
        n1.value = returns_1.shape[0]
        risk1_1 = cv.norm(returns_1 * w, "fro") / cv.sqrt(n1 - 1)

        # MAD Model Variables

        madmodel = False
        Y = cv.Variable((returns.shape[0], 1))
        u = np.matrix(np.ones((returns.shape[0], 1)) * mu)
        a = returns - u
        risk2 = cv.sum(Y) / n
        # madconstraints=[a*w >= -Y, a*w <= Y, Y >= 0]
        madconstraints = [a * w <= Y, Y >= 0]

        # Semi Variance Model Variables

        risk3 = cv.norm(Y, "fro") / cv.sqrt(n - 1)

        # CVaR Model Variables

        alpha1 = self.alpha
        VaR = cv.Variable(1)
        alpha = cv.Parameter(nonneg=True)
        alpha.value = alpha1
        X = returns * w
        Z = cv.Variable((returns.shape[0], 1))
        risk4 = VaR + 1 / (alpha * n) * cv.sum(Z)
        cvarconstraints = [Z >= 0, Z >= -X - VaR]

        # Worst Realization (Minimax) Model Variables

        M = cv.Variable(1)
        risk5 = M
        wrconstraints = [-X <= M]

        # Lower Partial Moment Variables

        lpmmodel = False
        lpm = cv.Variable((returns.shape[0], 1))
        lpmconstraints = [lpm >= 0]

        if obj == "Sharpe":
            lpmconstraints += [lpm >= rf0 * k - X]
        else:
            lpmconstraints += [lpm >= rf0 - X]

        # First Lower Partial Moment (Omega) Model Variables

        risk6 = cv.sum(lpm) / n

        # Second Lower Partial Moment (Sortino) Model Variables

        risk7 = cv.norm(lpm, "fro") / cv.sqrt(n - 1)

        # Drawdown Model Variables

        drawdown = False
        if obj == "Sharpe":
            X1 = k + nav * w
        else:
            X1 = 1 + nav * w

        U = cv.Variable((nav.shape[0] + 1, 1))
        ddconstraints = [U[1:] >= X1, U[1:] >= U[:-1]]

        if obj == "Sharpe":
            ddconstraints += [U[1:] >= k, U[0] == k]
        else:
            ddconstraints += [U[1:] >= 1, U[0] == 1]

        # Maximum Drawdown Model Variables

        MDD = cv.Variable(1)
        risk8 = MDD
        mddconstraints = [MDD >= U[1:] - X1]

        # Average Drawdown Model Variables

        risk9 = 1 / n * cv.sum(U[1:] - X1)

        # Conditional Drawdown Model Variables

        CDaR = cv.Variable(1)
        Zd = cv.Variable((nav.shape[0], 1))
        risk10 = CDaR + 1 / (alpha * n) * cv.sum(Zd)
        cdarconstraints = [Zd >= U[1:] - X1 - CDaR, Zd >= 0]

        # Tracking Error Model Variables

        c = self.benchweights
        if self.kindbench == True:
            bench = np.matrix(returns) * c
        else:
            bench = self.benchindex

        if obj == "Sharpe":
            TE = cv.norm(returns * w - bench * k, "fro") / cv.sqrt(n - 1)
        else:
            TE = cv.norm(returns * w - bench, "fro") / cv.sqrt(n - 1)

        # Problem aditional linear constraints

        if obj == "Sharpe":
            constraints = [
                cv.sum(w) == self.upperlng * k,
                k >= 0,
                mu * w - rf0 * k == 1,
            ]
            if self.sht == False:
                constraints += [w <= self.upperlng * k, w * 1000 >= 0]
            elif self.sht == True:
                constraints += [
                    w <= self.upperlng * k,
                    w >= -self.uppersht * k,
                    cv.sum(cv.neg(w)) <= self.uppersht * k,
                ]
        else:
            constraints = [cv.sum(w) == self.upperlng]
            if self.sht == False:
                constraints += [w <= self.upperlng, w * 1000 >= 0]
            elif self.sht == True:
                constraints += [
                    w <= self.upperlng,
                    w >= -self.uppersht,
                    cv.sum(cv.neg(w)) <= self.uppersht,
                ]

        if self.ainequality is not None and self.binequality is not None:
            A = np.matrix(self.ainequality)
            B = np.matrix(self.binequality)
            if obj == "Sharpe":
                constraints += [A * w - B * k >= 0]
            else:
                constraints += [A * w - B >= 0]

        # Turnover Constraints

        if obj == "Sharpe":
            if self.allowTO == True:
                constraints += [cv.abs(w - c * k) * 1000 <= self.turnover * k * 1000]
        else:
            if self.allowTO == True:
                constraints += [cv.abs(w - c) * 1000 <= self.turnover * 1000]

        # Tracking error Constraints

        if obj == "Sharpe":
            if self.allowTE == True:
                constraints += [TE <= self.TE * k]
        else:
            if self.allowTE == True:
                constraints += [TE <= self.TE]

        # Problem risk Constraints

        if self.upperdev is not None:
            if obj == "Sharpe":
                constraints += [risk1_1 <= self.upperdev * k]
            else:
                constraints += [risk1 <= self.upperdev ** 2]

        if self.uppermad is not None:
            if obj == "Sharpe":
                constraints += [risk2 <= self.uppermad * k / 2]
            else:
                constraints += [risk2 <= self.uppermad / 2]
            madmodel = True

        if self.uppersdev is not None:
            if obj == "Sharpe":
                constraints += [risk3 <= self.uppersdev * k]
            else:
                constraints += [risk3 <= self.uppersdev]
            madmodel = True

        if self.upperCVaR is not None:
            if obj == "Sharpe":
                constraints += [risk4 <= self.upperCVaR * k]
            else:
                constraints += [risk4 <= self.upperCVaR]
            constraints += cvarconstraints

        if self.upperwr is not None:
            if obj == "Sharpe":
                constraints += [-X <= self.upperwr * k]
            else:
                constraints += [-X <= self.upperwr]
            constraints += wrconstraints

        if self.upperflpm is not None:
            if obj == "Sharpe":
                constraints += [risk6 <= self.upperflpm * k]
            else:
                constraints += [risk6 <= self.upperflpm]
            lpmmodel = True

        if self.upperslpm is not None:
            if obj == "Sharpe":
                constraints += [risk7 <= self.upperslpm * k]
            else:
                constraints += [risk7 <= self.upperslpm]
            lpmmodel = True

        if self.uppermdd is not None:
            if obj == "Sharpe":
                constraints += [U[1:] - X1 <= self.uppermdd * k]
            else:
                constraints += [U[1:] - X1 <= self.uppermdd]
            constraints += mddconstraints
            drawdown = True

        if self.upperadd is not None:
            if obj == "Sharpe":
                constraints += [risk9 <= self.upperadd * k]
            else:
                constraints += [risk9 <= self.upperadd]
            drawdown = True

        if self.upperCDaR is not None:
            if obj == "Sharpe":
                constraints += [risk10 <= self.upperCDaR * k]
            else:
                constraints += [risk10 <= self.upperCDaR]
            constraints += cdarconstraints
            drawdown = True

        # Defining risk function

        if rm == "MV":
            if model != "Classic":
                risk = risk1_1
            elif model == "Classic":
                risk = risk1
        elif rm == "MAD":
            risk = risk2
            madmodel = True
        elif rm == "MSV":
            risk = risk3
            madmodel = True
        elif rm == "CVaR":
            risk = risk4
            if self.upperCVaR is None:
                constraints += cvarconstraints
        elif rm == "WR":
            risk = risk5
            if self.upperwr is None:
                constraints += wrconstraints
        elif rm == "FLPM":
            risk = risk6
            lpmmodel = True
        elif rm == "SLPM":
            risk = risk7
            lpmmodel = True
        elif rm == "MDD":
            risk = risk8
            drawdown = True
            if self.uppermdd is None:
                constraints += mddconstraints
        elif rm == "ADD":
            risk = risk9
            drawdown = True
        elif rm == "CDaR":
            risk = risk10
            drawdown = True
            if self.upperCDaR is None:
                constraints += cdarconstraints

        if madmodel == True:
            constraints += madconstraints
        if lpmmodel == True:
            constraints += lpmconstraints
        if drawdown == True:
            constraints += ddconstraints

        # Frontier Variables

        portafolio = {}

        for i in self.assetslist:
            portafolio.update({i: []})

        # Optimization Process

        # Defining solvers
        solvers = [cv.ECOS, cv.SCS, cv.OSQP, cv.CVXOPT, cv.GLPK]

        # Defining objective function
        if obj == "Sharpe":
            if rm != "Classic":
                objective = cv.Minimize(risk)
            elif rm == "Classic":
                objective = cv.Minimize(risk * 1000)
        elif obj == "MinRisk":
            objective = cv.Minimize(risk)
        elif obj == "Utility":
            objective = cv.Maximize(ret - l * risk)
        elif obj == "MaxRet":
            objective = cv.Maximize(ret)

        try:
            prob = cv.Problem(objective, constraints)
            for solver in solvers:
                try:
                    prob.solve(
                        solver=solver, parallel=True, max_iters=2000, abstol=1e-10
                    )
                except:
                    pass
                if w.value is not None:
                    break

            if obj == "Sharpe":
                weights = np.matrix(w.value / k.value).T
            else:
                weights = np.matrix(w.value).T

            if self.sht == False:
                weights = np.abs(weights) / np.sum(np.abs(weights))

            for j in self.assetslist:
                portafolio[j].append(weights[0, self.assetslist.index(j)])

        except:
            pass

        optimum = pd.DataFrame(portafolio, index=["weights"], dtype=np.float64).T

        return optimum
Ejemplo n.º 2
0
    def rp_optimization(self,
                        model="Classic",
                        rm="MV",
                        rf=0,
                        b=None,
                        hist=True):
        r"""
        This method that calculates the risk parity portfolio according to the
        optimization model selected by the user. The general problem that
        solves is:
        
        .. math::
            \begin{align}
            &\underset{w}{\min} & & R(w)\\
            &\text{s.t.} & & b \log(w) \geq c\\
            & & & w \geq 0 \\
            \end{align}
        
        Where:
            
        :math:`R(w)` is the risk measure.
    
        :math:`b` is a vector of risk constraints.
        
        Parameters
        ----------
        model : str can be 'Classic' or 'FM'
            The model used for optimize the portfolio.
            The default is 'Classic'. Posible values are:

            - 'Classic': use estimates of expected return vector and covariance matrix that depends on historical data.
            - 'FM': use estimates of expected return vector and covariance matrix based on a Risk Factor model specified by the user.
            
        rm : str, optional
            The risk measure used to optimze the portfolio.
            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).
            - 'CVaR': Conditional Value at Risk.
            - 'CDaR': Conditional Drawdown at Risk of uncompounded returns.

        rf : float, optional
            Risk free rate, must be in the same period of assets returns.
            Used for 'FLPM' and 'SLPM'.
            The default is 0.                
        b : float, optional
            The vector of risk constraints per asset.
            The default is 1/n (number of assets).
        hist : bool, optional
            Indicate if uses historical or factor estimation of returns to 
            calculate risk measures that depends on scenarios (All except
            'MV' risk measure). The default is True.

        Returns
        -------
        w : DataFrame
            The weights of optimum portfolio.

        """

        # General model Variables

        mu = None
        sigma = None
        returns = None
        if model == "Classic":
            mu = np.array(self.mu, ndmin=2)
            sigma = np.array(self.cov, ndmin=2)
            returns = np.array(self.returns, ndmin=2)
            nav = np.array(self.nav, ndmin=2)
        elif model == "FM":
            mu = np.array(self.mu_fm, ndmin=2)
            if hist == False:
                sigma = np.array(self.cov_fm, ndmin=2)
                returns = np.array(self.returns_fm, ndmin=2)
                nav = np.array(self.nav_fm, ndmin=2)
            elif hist == True:
                sigma = np.array(self.cov, ndmin=2)
                returns = np.array(self.returns, ndmin=2)
                nav = np.array(self.nav, ndmin=2)

        # General Model Variables

        if b is None:
            b = np.ones((1, mu.shape[1]))
            b = b / mu.shape[1]

        returns = np.array(returns, ndmin=2)
        w = cv.Variable((mu.shape[1], 1))
        rf0 = rf
        n = returns.shape[0]

        # MV Model Variables

        risk1 = cv.quad_form(w, sigma)
        returns_1 = af.cov_returns(sigma) * 1000
        n1 = returns_1.shape[0]
        risk1_1 = cv.norm(returns_1 @ w, "fro") / cv.sqrt(n1 - 1)

        # MAD Model Variables

        Y = cv.Variable((returns.shape[0], 1))
        u = np.ones((returns.shape[0], 1)) * mu
        a = returns - u
        risk2 = cv.sum(Y) / n
        # madconstraints=[a*w >= -Y, a*w <= Y, Y >= 0]
        madconstraints = [a @ w <= Y, Y >= 0]

        # Semi Variance Model Variables

        risk3 = cv.norm(Y, "fro") / cv.sqrt(n - 1)

        # CVaR Model Variables

        alpha1 = self.alpha
        VaR = cv.Variable((1, 1))
        alpha = alpha1
        X = returns @ w
        Z = cv.Variable((returns.shape[0], 1))
        risk4 = VaR + 1 / (alpha * n) * cv.sum(Z)
        cvarconstraints = [Z >= 0, Z >= -X - VaR]

        # Lower Partial Moment Variables

        lpm = cv.Variable((returns.shape[0], 1))
        lpmconstraints = [lpm >= 0, lpm >= rf0 - X]

        # First Lower Partial Moment (Omega) Model Variables

        risk6 = cv.sum(lpm) / n

        # Second Lower Partial Moment (Sortino) Model Variables

        risk7 = cv.norm(lpm, "fro") / cv.sqrt(n - 1)

        # Drawdown Model Variables

        X1 = 1 + nav @ w
        U = cv.Variable((nav.shape[0] + 1, 1))
        ddconstraints = [
            U[1:] * 1000 >= X1 * 1000,
            U[1:] * 1000 >= U[:-1] * 1000,
            U[1:] * 1000 >= 1 * 1000,
            U[0] * 1000 == 1 * 1000,
        ]

        # Conditional Drawdown Model Variables

        CDaR = cv.Variable((1, 1))
        Zd = cv.Variable((nav.shape[0], 1))
        risk10 = CDaR + 1 / (alpha * n) * cv.sum(Zd)
        cdarconstraints = [
            Zd * 1000 >= U[1:] * 1000 - X1 * 1000 - CDaR * 1000,
            Zd * 1000 >= 0,
        ]

        # Defining risk function

        constraints = []

        if rm == "MV":
            if model != "Classic":
                risk = risk1_1
            elif model == "Classic":
                risk = risk1
        elif rm == "MAD":
            risk = risk2
            constraints += madconstraints
        elif rm == "MSV":
            risk = risk3
            constraints += madconstraints
        elif rm == "CVaR":
            risk = risk4
            constraints += cvarconstraints
        elif rm == "FLPM":
            risk = risk6
            constraints += lpmconstraints
        elif rm == "SLPM":
            risk = risk7
            constraints += lpmconstraints
        elif rm == "CDaR":
            risk = risk10
            constraints += ddconstraints
            constraints += cdarconstraints

        # Frontier Variables

        portafolio = {}

        for i in self.assetslist:
            portafolio.update({i: []})

        # Optimization Process

        # Defining solvers
        solvers = [cv.ECOS, cv.SCS, cv.OSQP, cv.CVXOPT]
        sol_params = {
            cv.ECOS: {
                "max_iters": 2000,
                "abstol": 1e-10
            },
            cv.SCS: {
                "max_iters": 2500,
                "eps": 1e-10
            },
            cv.OSQP: {
                "max_iter": 10000,
                "eps_abs": 1e-10
            },
            cv.CVXOPT: {
                "max_iters": 2000,
                "abstol": 1e-10
            },
        }

        # Defining objective function

        objective = cv.Minimize(risk * 1000)

        constraints += [b @ cv.log(w) * 1000 >= 1 * 1000, w * 1000 >= 0]

        try:
            prob = cv.Problem(objective, constraints)
            for solver in solvers:
                try:
                    prob.solve(solver=solver, **sol_params[solver])
                except:
                    pass
                if w.value is not None:
                    break

            weights = np.array(w.value, ndmin=2).T
            weights = np.abs(weights) / np.sum(np.abs(weights))

            for j in self.assetslist:
                portafolio[j].append(weights[0, self.assetslist.index(j)])

        except:
            pass

        rp_optimum = pd.DataFrame(portafolio,
                                  index=["weights"],
                                  dtype=np.float64).T

        return rp_optimum