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