def _naive_risk(self, returns, cov, rm="MV", rf=0): assets = returns.columns.tolist() n = len(assets) if rm == "equal": weight = np.ones((n, 1)) * 1 / n else: inv_risk = np.zeros((n, 1)) for i in assets: k = assets.index(i) w = np.zeros((n, 1)) w[k, 0] = 1 w = pd.DataFrame(w, columns=["weights"], index=assets) if rm == "vol": risk = rk.Sharpe_Risk(w, cov=cov, returns=returns, rm="MV", rf=rf, alpha=self.alpha) else: risk = rk.Sharpe_Risk(w, cov=cov, returns=returns, rm=rm, rf=rf, alpha=self.alpha) inv_risk[k, 0] = risk if rm == "MV": inv_risk = 1 / np.power(inv_risk, 2) else: inv_risk = 1 / inv_risk weight = inv_risk * (1 / np.sum(inv_risk)) weight = weight.reshape(-1, 1) return weight
def _hierarchical_recursive_bisection(self, Z, rm="MV", rf=0, linkage="ward", model="HERC"): # Transform linkage to tree and reverse order root, nodes = hr.to_tree(Z, rd=True) nodes = nodes[::-1] weight = pd.Series(1, index=self.cov.index) # Set initial weights to 1 clusters_inds = hr.fcluster(Z, self.k, criterion="maxclust") clusters = { i: [] for i in range(min(clusters_inds), max(clusters_inds) + 1) } for i, v in enumerate(clusters_inds): clusters[v].append(i) # Loop through k clusters for i in nodes[:self.k - 1]: if i.is_leaf() == False: # skip leaf-nodes left = i.get_left().pre_order( ) # lambda i: i.id) # get left cluster right = i.get_right().pre_order( ) # lambda i: i.id) # get right cluster left_set = set(left) right_set = set(right) left_risk = 0 right_risk = 0 # Allocate weight to clusters if rm == "equal": w_1 = 0.5 else: for j in clusters.keys(): if set(clusters[j]).issubset(left_set): # Left cluster left_cov = self.cov.iloc[clusters[j], clusters[j]] left_returns = self.returns.iloc[:, clusters[j]] left_weight = self._naive_risk(left_returns, left_cov, rm=rm, rf=rf) if rm == "vol": left_risk_ = rk.Sharpe_Risk( left_weight, cov=left_cov, returns=left_returns, rm="MV", rf=rf, alpha=self.alpha, ) else: left_risk_ = rk.Sharpe_Risk( left_weight, cov=left_cov, returns=left_returns, rm=rm, rf=rf, alpha=self.alpha, ) if rm == "MV": left_risk_ = np.power(left_risk_, 2) left_risk += left_risk_ if set(clusters[j]).issubset(right_set): # Right cluster right_cov = self.cov.iloc[clusters[j], clusters[j]] right_returns = self.returns.iloc[:, clusters[j]] right_weight = self._naive_risk(right_returns, right_cov, rm=rm, rf=rf) if rm == "vol": right_risk_ = rk.Sharpe_Risk( right_weight, cov=right_cov, returns=right_returns, rm="MV", rf=rf, alpha=self.alpha, ) else: right_risk_ = rk.Sharpe_Risk( right_weight, cov=right_cov, returns=right_returns, rm=rm, rf=rf, alpha=self.alpha, ) if rm == "MV": right_risk_ = np.power(right_risk_, 2) right_risk += right_risk_ w_1 = 1 - left_risk / (left_risk + right_risk) weight[left] *= w_1 # weight 1 weight[right] *= 1 - w_1 # weight 2 # Get constituents of k clusters clustered_assets = pd.Series(hr.cut_tree(Z, n_clusters=self.k).flatten(), index=self.cov.index) # Multiply within-cluster weight with inter-cluster weight for i in range(self.k): cluster = clustered_assets.loc[clustered_assets == i] cluster_cov = self.cov.loc[cluster.index, cluster.index] cluster_returns = self.returns.loc[:, cluster.index] if model == "HERC": cluster_weights = pd.Series( self._naive_risk(cluster_returns, cluster_cov, rm=rm, rf=rf).flatten(), index=cluster_cov.index, ) elif model == "HERC2": cluster_weights = pd.Series( self._naive_risk(cluster_returns, cluster_cov, rm="equal", rf=rf).flatten(), index=cluster_cov.index, ) weight.loc[cluster_weights.index] *= cluster_weights return weight
def plot_drawdown(nav, w, alpha=0.05, height=8, width=10, ax=None): r""" Create a chart with the evolution of portfolio prices and drawdown. Parameters ---------- nav : DataFrame Cumulative assets returns. w : DataFrame, optional A portfolio specified by the user to compare with the efficient frontier. The default is None. alpha : float, optional Significante level of DaR and CDaR. The default is 0.05. height : float, optional Height of the image in inches. The default is 8. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis of size (2,1), optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib axis. Returns the Axes object with the plot for further tweaking. Example ------- :: nav=port.nav ax = plf.plot_drawdown(nav=nav, w=w1, alpha=0.05, height=8, width=10, ax=None) .. image:: images/Drawdown.png """ if not isinstance(nav, pd.DataFrame): raise ValueError("nav must be a DataFrame") if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if w.shape[1] > 1 and w.shape[0] == 0: w = w.T elif w.shape[1] > 1 and w.shape[0] > 0: raise ValueError("w must be a DataFrame") if nav.shape[1] != w.shape[0]: a1 = str(nav.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if ax is None: fig = plt.gcf() ax = fig.subplots(nrows=2, ncols=1) ax = ax.flatten() fig.set_figwidth(width) fig.set_figheight(height) index = nav.index.tolist() a = np.array(nav, ndmin=2) a = np.insert(a, 0, 0, axis=0) a = np.diff(a, axis=0) a = np.array(a, ndmin=2) @ np.array(w, ndmin=2) prices = 1 + np.insert(a, 0, 0, axis=0) prices = np.cumprod(prices, axis=0) prices = np.ravel(prices).tolist() prices2 = 1 + np.array(np.cumsum(a, axis=0)) prices2 = np.ravel(prices2).tolist() del prices[0] DD = [] peak = -99999 for i in range(0, len(prices)): if prices2[i] > peak: peak = prices2[i] DD.append((peak - prices2[i])) DD = -np.array(DD) titles = [ "Historical Compounded Cumulative Returns", "Historical Uncompounded Drawdown", ] data = [prices, DD] color1 = ["b", "orange"] risk = [ -rk.MDD_Abs(a), -rk.ADD_Abs(a), -rk.DaR_Abs(a, alpha), -rk.CDaR_Abs(a, alpha), -rk.UCI_Abs(a), ] label = [ "Maximum Drawdown: " + "{0:.2%}".format(risk[0]), "Average Drawdown: " + "{0:.2%}".format(risk[1]), "{0:.2%}".format( (1 - alpha)) + " Confidence DaR: " + "{0:.2%}".format(risk[2]), "{0:.2%}".format( (1 - alpha)) + " Confidence CDaR: " + "{0:.2%}".format(risk[3]), "Ulcer Index: " + "{0:.2%}".format(risk[4]), ] color2 = ["r", "b", "limegreen", "dodgerblue", "fuchsia"] j = 0 ymin = np.min(DD) * 1.5 for i in ax: i.clear() i.plot_date(index, data[j], "-", color=color1[j]) if j == 1: i.fill_between(index, 0, data[j], facecolor=color1[j], alpha=0.3) for k in range(0, len(risk)): i.axhline(y=risk[k], color=color2[k], linestyle="-", label=label[k]) i.set_ylim(ymin, 0) i.legend(loc="lower right") # , fontsize = 'x-small') i.set_title(titles[j]) i.xaxis.set_major_locator( mdates.AutoDateLocator(tz=None, minticks=5, maxticks=10)) i.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m")) i.set_yticklabels(["{:3.2%}".format(x) for x in i.get_yticks()]) i.grid(linestyle=":") j = j + 1 fig = plt.gcf() fig.tight_layout() return ax
def _recursive_bisection(self, sort_order, rm="MV", rf=0): weight = pd.Series(1, index=sort_order) # set initial weights to 1 items = [sort_order] while len(items) > 0: # loop while weights is under 100% items = [ i[j:k] for i in items for j, k in ( (0, len(i) // 2), (len(i) // 2, len(i)), ) # get cluster indi if len(i) > 1 ] # allocate weight to left and right cluster for i in range(0, len(items), 2): left_cluster = items[i] right_cluster = items[i + 1] # Left cluster left_cov = self.cov.iloc[left_cluster, left_cluster] left_returns = self.returns.iloc[:, left_cluster] left_weight = self._naive_risk(left_returns, left_cov, rm=rm, rf=rf) if rm == "vol": left_risk = rk.Sharpe_Risk( left_weight, cov=left_cov, returns=left_returns, rm="MV", rf=rf, alpha=self.alpha, ) else: left_risk = rk.Sharpe_Risk( left_weight, cov=left_cov, returns=left_returns, rm=rm, rf=rf, alpha=self.alpha, ) if rm == "MV": left_risk = np.power(left_risk, 2) # Right cluster right_cov = self.cov.iloc[right_cluster, right_cluster] right_returns = self.returns.iloc[:, right_cluster] right_weight = self._naive_risk(right_returns, right_cov, rm=rm, rf=rf) if rm == "vol": right_risk = rk.Sharpe_Risk( right_weight, cov=right_cov, returns=right_returns, rm="MV", rf=rf, alpha=self.alpha, ) else: right_risk = rk.Sharpe_Risk( right_weight, cov=right_cov, returns=right_returns, rm=rm, rf=rf, alpha=self.alpha, ) if rm == "MV": right_risk = np.power(right_risk, 2) # Allocate weight to clusters alpha = 1 - left_risk / (left_risk + right_risk) weight[left_cluster] *= alpha # weight 1 weight[right_cluster] *= 1 - alpha # weight 2 weight.index = self.asset_order return weight
def plot_risk_con( w, cov=None, returns=None, rm="MV", rf=0, alpha=0.05, color="tab:blue", height=6, width=10, ax=None, ): r""" Create a chart with the risk contribution per asset of the portfolio. Parameters ---------- w : DataFrame of shape (n_assets, 1) Portfolio weights. cov : DataFrame of shape (n_features, n_features) Covariance matrix, where n_features is the number of features. returns : DataFrame 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 to estimate risk contribution. 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. - 'EVaR': Conditional Value at Risk. - 'WR': Worst Realization (Minimax) - 'MDD': Maximum Drawdown of uncompounded returns (Calmar Ratio). - 'ADD': Average Drawdown of uncompounded returns. - 'DaR': Drawdown at Risk of uncompounded returns. - 'CDaR': Conditional Drawdown at Risk of uncompounded returns. - 'UCI': Ulcer Index of uncompounded returns. rf : float, optional Risk free rate or minimum aceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR and CDaR. The default is 0.05. color : str, optional Color used to plot each asset risk contribution. The default is 'tab:blue'. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib axis. Returns the Axes object with the plot for further tweaking. Example ------- :: ax = plf.plot_risk_con(w=w2, cov=cov, returns=returns, rm='MSV', rf=0, alpha=0.05, color="tab:blue", height=6, width=10, ax=None) .. image:: images/Risk_Con.png """ if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) item = rmeasures.index(rm) title = "Risk (" + rm_names[item] + ") Contribution per Asset" ax.set_title(title) X = w.index.tolist() RC = rk.Risk_Contribution(w, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha) ax.bar(X, RC, alpha=0.7, color=color, edgecolor="black") ax.set_xlim(-0.5, len(X) - 0.5) ax.set_yticks(ax.get_yticks()) ax.set_yticklabels(["{:3.5%}".format(x) for x in ax.get_yticks()]) ax.grid(linestyle=":") fig = plt.gcf() fig.tight_layout() return ax
def plot_hist(returns, w, alpha=0.05, bins=50, height=6, width=10, ax=None): r""" Create a histogram of portfolio returns with the risk measures. Parameters ---------- returns : DataFrame Assets returns. w : DataFrame of shape (n_assets, 1) Portfolio weights. alpha : float, optional Significante level of VaR, CVaR and EVaR. The default is 0.05. bins : float, optional Number of bins of the histogram. The default is 50. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib axis. Returns the Axes object with the plot for further tweaking. Example ------- :: ax = plf.plot_hist(returns=Y, w=w1, alpha=0.05, bins=50, height=6, width=10, ax=None) .. image:: images/Histogram.png """ if not isinstance(returns, pd.DataFrame): raise ValueError("returns must be a DataFrame") if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if w.shape[1] > 1 and w.shape[0] == 0: w = w.T elif w.shape[1] > 1 and w.shape[0] > 0: raise ValueError("w must be a DataFrame") if returns.shape[1] != w.shape[0]: a1 = str(returns.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) a = np.array(returns, ndmin=2) @ np.array(w, ndmin=2) ax.set_title("Portfolio Returns Histogram") n, bins1, patches = ax.hist(a, bins, density=1, edgecolor="skyblue", color="skyblue", alpha=0.5) mu = np.mean(a) sigma = np.std(a, axis=0, ddof=1).item() risk = [ mu, mu - sigma, mu - rk.MAD(a), -rk.VaR_Hist(a, alpha), -rk.CVaR_Hist(a, alpha), -rk.EVaR_Hist(a, alpha)[0], -rk.WR(a), ] label = [ "Mean: " + "{0:.2%}".format(risk[0]), "Mean - Std. Dev.(" + "{0:.2%}".format(-risk[1] + mu) + "): " + "{0:.2%}".format(risk[1]), "Mean - MAD(" + "{0:.2%}".format(-risk[2] + mu) + "): " + "{0:.2%}".format(risk[2]), "{0:.2%}".format( (1 - alpha)) + " Confidence VaR: " + "{0:.2%}".format(risk[3]), "{0:.2%}".format( (1 - alpha)) + " Confidence CVaR: " + "{0:.2%}".format(risk[4]), "{0:.2%}".format( (1 - alpha)) + " Confidence EVaR: " + "{0:.2%}".format(risk[5]), "Worst Realization: " + "{0:.2%}".format(risk[6]), ] color = [ "b", "r", "fuchsia", "darkorange", "limegreen", "dodgerblue", "darkgrey" ] for i, j, k in zip(risk, label, color): ax.axvline(x=i, color=k, linestyle="-", label=j) # add a 'best fit' line y = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-0.5 * (1 / sigma * (bins1 - mu))**2) ax.plot( bins1, y, "--", color="orange", label="Normal: $\mu=" + "{0:.2%}".format(mu) + "$%, $\sigma=" + "{0:.2%}".format(sigma) + "$%", ) factor = (np.max(a) - np.min(a)) / bins ax.xaxis.set_major_locator(plt.AutoLocator()) ax.set_xticklabels(["{:3.2%}".format(x) for x in ax.get_xticks()]) ax.set_yticklabels(["{:3.2%}".format(x * factor) for x in ax.get_yticks()]) ax.legend(loc="upper right") # , fontsize = 'x-small') ax.grid(linestyle=":") ax.set_ylabel("Probability Density") fig = plt.gcf() fig.tight_layout() return ax
def plot_table(returns, w, MAR=0, alpha=0.05, height=9, width=12, t_factor=252, ax=None): r""" Create a table with information about risk measures and risk adjusted return ratios. Parameters ---------- returns : DataFrame Assets returns. w : DataFrame Portfolio weights. MAR: float, optional Minimum acceptable return. alpha: float, optional Significance level for VaR, CVaR, EVaR, DaR and CDaR. height : float, optional Height of the image in inches. The default is 9. width : float, optional Width of the image in inches. The default is 12. t_factor : float, optional Factor used to annualize expected return and expected risks for risk measures based on returns (not drawdowns). The default is 252. .. math:: \begin{align} \text{Annualized Return} & = \text{Return} \, \times \, \text{t_factor} \\ \text{Annualized Risk} & = \text{Risk} \, \times \, \sqrt{\text{t_factor}} \end{align} ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib axis Returns the Axes object with the plot for further tweaking. Example ------- :: ax = plf.plot_table(returns=Y, w=w1, MAR=0, alpha=0.05, ax=None) .. image:: images/Port_Table.png """ if not isinstance(returns, pd.DataFrame): raise ValueError("returns must be a DataFrame") if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if returns.shape[1] != w.shape[0]: a1 = str(returns.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) mu = returns.mean() cov = returns.cov() days = (returns.index[-1] - returns.index[0]).days + 1 X = returns @ w X = X.to_numpy().ravel() rowLabels = [ "Profitability and Other Inputs", "Mean Return (1)", "Compound Annual Growth Rate (CAGR)", "Minimum Acceptable Return (MAR) (1)", "Significance Level", "", "Risk Measures based on Returns", "Standard Deviation (2)", "Mean Absolute Deviation (MAD) (2)", "Semi Standard Deviation (2)", "First Lower Partial Moment (FLPM) (2)", "Second Lower Partial Moment (SLPM) (2)", "Value at Risk (VaR) (2)", "Conditional Value at Risk (CVaR) (2)", "Entropic Value at Risk (EVaR) (2)", "Worst Realization (2)", "Skewness", "Kurtosis", "", "Risk Measures based on Drawdowns (3)", "Max Drawdown (MDD)", "Average Drawdown (ADD)", "Drawdown at Risk (DaR)", "Conditional Drawdown at Risk (CDaR)", "Ulcer Index", "(1) Annualized, multiplied by " + str(t_factor), "(2) Annualized, multiplied by √" + str(t_factor), "(3) Based on uncompounded cumulated returns", ] indicators = [ "", (mu @ w).to_numpy().item() * t_factor, np.power(np.prod(1 + X), 360 / days) - 1, MAR, alpha, "", "", np.sqrt(w.T @ cov @ w).to_numpy().item() * t_factor**0.5, rk.MAD(X) * t_factor**0.5, rk.SemiDeviation(X) * t_factor**0.5, rk.LPM(X, MAR=MAR, p=1) * t_factor**0.5, rk.LPM(X, MAR=MAR, p=2) * t_factor**0.5, rk.VaR_Hist(X, alpha=alpha) * t_factor**0.5, rk.CVaR_Hist(X, alpha=alpha) * t_factor**0.5, rk.EVaR_Hist(X, alpha=alpha)[0] * t_factor**0.5, rk.WR(X) * t_factor**0.5, st.skew(X, bias=False), st.kurtosis(X, bias=False), "", "", rk.MDD_Abs(X), rk.ADD_Abs(X), rk.DaR_Abs(X), rk.CDaR_Abs(X, alpha=alpha), rk.UCI_Abs(X), "", "", "", ] ratios = [] for i in range(len(indicators)): if i < 6 or indicators[i] == "" or rowLabels[i] in [ "Skewness", "Kurtosis" ]: ratios.append("") else: ratio = (indicators[1] - MAR) / indicators[i] ratios.append(ratio) for i in range(len(indicators)): if indicators[i] != "": if rowLabels[i] in ["Skewness", "Kurtosis"]: indicators[i] = "{:.5f}".format(indicators[i]) else: indicators[i] = "{:.4%}".format(indicators[i]) if ratios[i] != "": ratios[i] = "{:.6f}".format(ratios[i]) data = pd.DataFrame({ "A": rowLabels, "B": indicators, "C": ratios }).to_numpy() ax.set_axis_off() ax.axis("tight") ax.axis("off") colLabels = ["", "Values", "(Return - MAR)/Risk"] colWidths = [0.45, 0.275, 0.275] rowHeight = 0.07 table = ax.table( cellText=data, colLabels=colLabels, colWidths=colWidths, cellLoc="center", loc="upper left", bbox=[-0.03, 0, 1, 1], ) table.auto_set_font_size(False) cellDict = table.get_celld() k = 1 rowHeight = 1 / len(rowLabels) ncols = len(colLabels) nrows = len(rowLabels) for i in range(0, ncols): cellDict[(0, i)].set_text_props(weight="bold", color="white", size="x-large") cellDict[(0, i)].set_facecolor("darkblue") cellDict[(0, i)].set_edgecolor("white") cellDict[(0, i)].set_height(rowHeight) for j in range(1, nrows + 1): cellDict[(j, 0)].set_text_props(weight="bold", color="black", size="x-large", ha="left") cellDict[(j, i)].set_text_props(color="black", size="x-large") cellDict[(j, 0)].set_edgecolor("white") cellDict[(j, i)].set_edgecolor("white") if k % 2 != 0: cellDict[(j, 0)].set_facecolor("whitesmoke") cellDict[(j, i)].set_facecolor("whitesmoke") if j in [6, 19]: cellDict[(j, 0)].set_facecolor("white") cellDict[(j, i)].set_facecolor("white") if j in [1, 7, 20]: cellDict[(j, 0)].set_text_props(color="white") cellDict[(j, 0)].set_facecolor("orange") cellDict[(j, i)].set_facecolor("orange") k = 1 k += 1 cellDict[(j, i)].set_height(rowHeight) for i in range(0, ncols): for j in range(nrows - 2, nrows + 1): cellDict[(j, i)].set_text_props(weight="normal", color="black", size="large") cellDict[(j, i)].set_facecolor("white") fig = plt.gcf() fig.tight_layout() return ax
def plot_frontier( w_frontier, mu, cov=None, returns=None, rm="MV", rf=0, alpha=0.05, cmap="viridis", w=None, label="Portfolio", marker="*", s=16, c="r", height=6, width=10, t_factor=252, ax=None, ): r""" Creates a plot of the efficient frontier for a risk measure specified by the user. Parameters ---------- w_frontier : DataFrame Portfolio weights of some points in the efficient frontier. mu : DataFrame of shape (1, n_assets) Vector of expected returns, where n_assets is the number of assets. cov : DataFrame of shape (n_features, n_features) Covariance matrix, where n_features is the number of features. returns : DataFrame 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 The risk measure used to estimate the frontier. 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. - 'EVaR': Conditional Value at Risk. - 'WR': Worst Realization (Minimax) - 'MDD': Maximum Drawdown of uncompounded returns (Calmar Ratio). - 'ADD': Average Drawdown of uncompounded returns. - 'DaR': Drawdown at Risk of uncompounded returns. - 'CDaR': Conditional Drawdown at Risk of uncompounded returns. - 'UCI': Ulcer Index of uncompounded returns. rf : float, optional Risk free rate or minimum aceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR, EVaR, DaR and CDaR. The default is 0.05. cmap : cmap, optional Colorscale, represente the risk adjusted return ratio. The default is 'viridis'. w : DataFrame of shape (n_assets, 1), optional A portfolio specified by the user. The default is None. label : str, optional Name of portfolio that appear on plot legend. The default is 'Portfolio'. marker : str, optional Marker of w. The default is "*". s : float, optional Size of marker. The default is 16. c : str, optional Color of marker. The default is 'r'. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. t_factor : float, optional Factor used to annualize expected return and expected risks for risk measures based on returns (not drawdowns). The default is 252. .. math:: \begin{align} \text{Annualized Return} & = \text{Return} \, \times \, \text{t_factor} \\ \text{Annualized Risk} & = \text{Risk} \, \times \, \sqrt{\text{t_factor}} \end{align} ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib Axes Returns the Axes object with the plot for further tweaking. Example ------- :: label = 'Max Risk Adjusted Return Portfolio' mu = port.mu cov = port.cov returns = port.returns ax = plf.plot_frontier(w_frontier=ws, mu=mu, cov=cov, returns=returns, rm=rm, rf=0, alpha=0.05, cmap='viridis', w=w1, label=label, marker='*', s=16, c='r', height=6, width=10, t_factor=252, ax=None) .. image:: images/MSV_Frontier.png """ if not isinstance(w_frontier, pd.DataFrame): raise ValueError("w_frontier must be a DataFrame") if not isinstance(mu, pd.DataFrame): raise ValueError("mu must be a DataFrame") if not isinstance(cov, pd.DataFrame): raise ValueError("cov must be a DataFrame") if not isinstance(returns, pd.DataFrame): raise ValueError("returns must be a DataFrame") if returns.shape[1] != w_frontier.shape[0]: a1 = str(returns.shape) a2 = str(w_frontier.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if w is not None: if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if w.shape[1] > 1 and w.shape[0] == 0: w = w.T elif w.shape[1] > 1 and w.shape[0] > 0: raise ValueError("w must be a column DataFrame") if returns.shape[1] != w.shape[0]: a1 = str(returns.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) mu_ = np.array(mu, ndmin=2) ax.set_ylabel("Expected Return") item = rmeasures.index(rm) x_label = rm_names[item] + " (" + rm + ")" ax.set_xlabel("Expected Risk - " + x_label) title = "Efficient Frontier Mean - " + x_label ax.set_title(title) X1 = [] Y1 = [] Z1 = [] for i in range(w_frontier.shape[1]): weights = np.array(w_frontier.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk(weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha) ret = mu_ @ weights ret = ret.item() * t_factor if rm not in ["MDD", "ADD", "CDaR", "UCI"]: risk = risk * t_factor**0.5 ratio = (ret - rf) / risk X1.append(risk) Y1.append(ret) Z1.append(ratio) ax1 = ax.scatter(X1, Y1, c=Z1, cmap=cmap) if w is not None: X2 = [] Y2 = [] for i in range(w.shape[1]): weights = np.array(w.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk(weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha) ret = mu_ @ weights ret = ret.item() * t_factor if rm not in ["MDD", "ADD", "CDaR", "UCI"]: risk = risk * t_factor**0.5 ratio = (ret - rf) / risk X2.append(risk) Y2.append(ret) ax.scatter(X2, Y2, marker=marker, s=s**2, c=c, label=label) ax.legend(loc="upper left") xmin = np.min(X1) - np.abs(np.max(X1) - np.min(X1)) * 0.1 xmax = np.max(X1) + np.abs(np.max(X1) - np.min(X1)) * 0.1 ymin = np.min(Y1) - np.abs(np.max(Y1) - np.min(Y1)) * 0.1 ymax = np.max(Y1) + np.abs(np.max(Y1) - np.min(Y1)) * 0.1 ax.set_ylim(ymin, ymax) ax.set_xlim(xmin, xmax) ax.xaxis.set_major_locator(plt.AutoLocator()) ax.set_yticklabels(["{:.4%}".format(x) for x in ax.get_yticks()]) ax.set_xticklabels(["{:.4%}".format(x) for x in ax.get_xticks()]) ax.tick_params(axis="y", direction="in") ax.tick_params(axis="x", direction="in") ax.grid(linestyle=":") colorbar = ax.figure.colorbar(ax1) colorbar.set_label("Risk Adjusted Return Ratio") fig = plt.gcf() fig.tight_layout() return ax
def efficient_frontier(self, model="Classic", rm="MV", points=20, rf=0, hist=True): r""" Method that calculates several portfolios in the efficient frontier of the selected risk measure, available with current assets and constraints. Parameters ---------- model : str, optional Methodology used to estimate input parameters. The default is 'Classic'. rm : str, optional Risk measure used by the optimization model. The default is 'MV'. points : scalar, optional Number of point calculated from the efficient frontier. The default is 50. rf : scalar, optional Risk free rate. The default is 0. 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 ------- frontier : DataFrame A dataframe that containts the weights of the portfolios. Notes ----- It's recommendable that don't use this method when there are too many assets (more than 100) and you are using a scenario based risk measure (all except standard deviation). It's preferable to use frontier_limits method (faster) to know the range of expected return and expected risk. """ 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_2) if hist == False: sigma = np.matrix(self.cov_bl_fm_2) 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) alpha1 = self.alpha limits = self.frontier_limits(model="Classic", rm=rm, rf=rf, hist=hist) w_min = np.matrix(limits.iloc[:, 0]).T w_max = np.matrix(limits.iloc[:, 1]).T ret_min = (mu * w_min).item() ret_max = (mu * w_max).item() if rm == "MV": risk_min = np.sqrt(w_min.T * sigma * w_min).item() risk_max = np.sqrt(w_max.T * sigma * w_max).item() elif rm == "MAD": risk_min = rk.MAD(returns * w_min) risk_max = rk.MAD(returns * w_max) elif rm == "MSV": risk_min = rk.SemiDeviation(returns * w_min) risk_max = rk.SemiDeviation(returns * w_max) elif rm == "CVaR": risk_min = rk.CVaR_Hist(returns * w_min, alpha1) risk_max = rk.CVaR_Hist(returns * w_max, alpha1) elif rm == "WR": risk_min = rk.WR(returns * w_min) risk_max = rk.WR(returns * w_max) elif rm == "FLPM": risk_min = rk.LPM(returns * w_min, rf, 1) risk_max = rk.LPM(returns * w_max, rf, 1) elif rm == "SLPM": risk_min = rk.LPM(returns * w_min, rf, 2) risk_max = rk.LPM(returns * w_max, rf, 2) elif rm == "MDD": risk_min = rk.MaxAbsDD(returns * w_min) risk_max = rk.MaxAbsDD(returns * w_max) elif rm == "ADD": risk_min = rk.AvgAbsDD(returns * w_min) risk_max = rk.AvgAbsDD(returns * w_max) elif rm == "CDaR": risk_min = rk.ConAbsDD(returns * w_min, alpha1) risk_max = rk.ConAbsDD(returns * w_max, alpha1) mus = np.linspace(ret_min, ret_max + (ret_max - ret_min) / (points), points + 1) risks = np.linspace( risk_min, risk_max + (risk_max - risk_min) / (points), points + 1 ) risk_lims = [ "upperdev", "uppermad", "uppersdev", "upperCVaR", "upperwr", "upperflpm", "upperslpm", "uppermdd", "upperadd", "upperCDaR", ] risk_names = [ "MV", "MAD", "MSV", "CVaR", "WR", "FLPM", "SLPM", "MDD", "ADD", "CDaR", ] item = risk_names.index(rm) frontier = [] n = 0 for i in range(len(risks)): try: if n == 0: w = self.optimization( model=model, rm=rm, obj="MinRisk", rf=rf, l=0, hist=hist ) else: setattr(self, risk_lims[item], risks[i]) w = self.optimization( model=model, rm=rm, obj="MaxRet", rf=rf, l=0, hist=hist ) n += 1 frontier.append(w) except: pass setattr(self, risk_lims[item], None) frontier = pd.concat(frontier, axis=1) frontier.columns = list(range(len(risks))) return frontier
def plot_risk_con( w, cov=None, returns=None, rm="MV", rf=0, alpha=0.01, color="tab:blue", height=6, width=10, ax=None, ): r""" Create a chart with the risk contribution per asset of the portfolio. Parameters ---------- w : DataFrame Weights of a portfolio. cov : DataFrame of shape (n_features, n_features) Covariance matrix, where n_features is the number of features. returns : DataFrame 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 to estimate risk contribution. The default is 'MV'. rf : float, optional Risk free rate or minimum aceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR and CDaR. The default is 0.01. color : str, optional Color used to plot each asset risk contribution. The default is 'tab:blue'. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib axis. Returns the Axes object with the plot for further tweaking. Example ------- :: ax = plf.plot_risk_con(w=w2, cov=cov, returns=returns, rm='MSV', rf=0, alpha=0.01, cmap="tab20", height=6, width=10, ax=None) .. image:: images/Risk_Con.png """ if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) item = rmeasures.index(rm) title = "Risk (" + rm_names[item] + ") Contribution per Asset" ax.set_title(title) X = w.index.tolist() RC = rk.Risk_Contribution(w, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha) ax.bar(X, RC, alpha=0.7, color=color, edgecolor="black") ax.set_xlim(-0.5, len(X) - 0.5) ax.set_yticks(ax.get_yticks()) ax.set_yticklabels(["{:3.5%}".format(x) for x in ax.get_yticks()]) ax.grid(linestyle=":") fig = plt.gcf() fig.tight_layout() return ax
def plot_frontier( w_frontier, mu, cov=None, returns=None, rm="MV", rf=0, alpha=0.01, cmap="viridis", w=None, label="Portfolio", marker="*", s=16, c="r", height=6, width=10, ax=None, ): """ Creates a plot of the efficient frontier for a risk measure specified by the user. Parameters ---------- w_frontier : DataFrame Portfolio weights of some points in the efficient frontier. mu : DataFrame of shape (1, n_assets) Vector of expected returns, where n_assets is the number of assets. cov : DataFrame of shape (n_features, n_features) Covariance matrix, where n_features is the number of features. returns : DataFrame 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 to create the frontier. The default is 'MV'. rf : float, optional Risk free rate or minimum aceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR and CDaR. The default is 0.01. cmap : cmap, optional Colorscale, represente the risk adjusted return ratio. The default is 'viridis'. w : DataFrame, optional A portfolio specified by the user. The default is None. label : str, optional Name of portfolio that appear on plot legend. The default is 'Portfolio'. marker : str, optional Marker of w_. The default is '*'. s : float, optional Size of marker. The default is 16. c : str, optional Color of marker. The default is 'r'. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib Axes Returns the Axes object with the plot for further tweaking. Example ------- :: label = 'Max Risk Adjusted Return Portfolio' mu = port.mu cov = port.cov returns = port.returns ax = plf.plot_frontier(w_frontier=ws, mu=mu, cov=cov, returns=returns, rm=rm, rf=0, alpha=0.01, cmap='viridis', w=w1, label='Portfolio', marker='*', s=16, c='r', height=6, width=10, ax=None) .. image:: images/MSV_Frontier.png """ if not isinstance(w_frontier, pd.DataFrame): raise ValueError("w_frontier must be a DataFrame") if not isinstance(mu, pd.DataFrame): raise ValueError("mu must be a DataFrame") if not isinstance(cov, pd.DataFrame): raise ValueError("cov must be a DataFrame") if not isinstance(returns, pd.DataFrame): raise ValueError("returns must be a DataFrame") if returns.shape[1] != w_frontier.shape[0]: a1 = str(returns.shape) a2 = str(w_frontier.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if w is not None: if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if w.shape[1] > 1 and w.shape[0] == 0: w = w.T elif w.shape[1] > 1 and w.shape[0] > 0: raise ValueError("w must be a column DataFrame") if returns.shape[1] != w.shape[0]: a1 = str(returns.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if ax is None: ax = plt.gca() fig = plt.gcf() fig.set_figwidth(width) fig.set_figheight(height) mu_ = np.array(mu, ndmin=2) ax.set_ylabel("Expected Return") item = rmeasures.index(rm) x_label = rm_names[item] + " (" + rm + ")" ax.set_xlabel("Expected Risk - " + x_label) title = "Efficient Frontier Mean - " + x_label ax.set_title(title) X1 = [] Y1 = [] Z1 = [] for i in range(w_frontier.shape[1]): weights = np.array(w_frontier.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk( weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha ) ret = mu_ @ weights ret = ret.item() ratio = (ret - rf) / risk X1.append(risk) Y1.append(ret) Z1.append(ratio) ax1 = ax.scatter(X1, Y1, c=Z1, cmap=cmap) if w is not None: X2 = [] Y2 = [] for i in range(w.shape[1]): weights = np.array(w.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk( weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha ) ret = mu_ @ weights ret = ret.item() ratio = (ret - rf) / risk X2.append(risk) Y2.append(ret) ax.scatter(X2, Y2, marker=marker, s=s ** 2, c=c, label=label) ax.legend(loc="upper left") xmin = np.min(X1) - np.abs(np.max(X1) - np.min(X1)) * 0.1 xmax = np.max(X1) + np.abs(np.max(X1) - np.min(X1)) * 0.1 ymin = np.min(Y1) - np.abs(np.max(Y1) - np.min(Y1)) * 0.1 ymax = np.max(Y1) + np.abs(np.max(Y1) - np.min(Y1)) * 0.1 ax.set_ylim(ymin, ymax) ax.set_xlim(xmin, xmax) ax.set_yticklabels(["{:.4%}".format(x) for x in ax.get_yticks()]) ax.set_xticklabels(["{:.4%}".format(x) for x in ax.get_xticks()]) ax.tick_params(axis="y", direction="in") ax.tick_params(axis="x", direction="in") ax.grid(linestyle=":") colorbar = ax.figure.colorbar(ax1) colorbar.set_label("Risk Adjusted Return Ratio") fig = plt.gcf() fig.tight_layout() return ax
def excel_report(returns, w, rf=0, alpha=0.05, t_factor=252, name="report"): r""" Create an Excel report (with formulas) with useful information to analyze risk and profitability of investment portfolios. Parameters ---------- returns : DataFrame Assets returns. w : DataFrame of size (n_assets, n_portfolios) Portfolio weights. rf : float, optional Risk free rate or minimum aceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR, EVaR, DaR and CDaR. The default is 0.05. t_factor : float, optional Factor used to annualize expected return and expected risks for risk measures based on returns (not drawdowns). The default is 252. .. math:: \begin{align} \text{Annualized Return} & = \text{Return} \, \times \, \text{t_factor} \\ \text{Annualized Risk} & = \text{Risk} \, \times \, \sqrt{\text{t_factor}} \end{align} name : str, optional Name or name with path where the Excel report will be saved. If no path is provided the report will be saved in the same path of current file. Raises ------ ValueError When the report cannot be built. Example ------- :: rp.excel_report(returns, w, MAR=0, alpha=0.05, name='report', files=None) .. image:: images/Excel.png """ n1 = w.shape[0] n2 = returns.shape[0] portfolios = w.columns.tolist() dates = returns.index.tolist() year = str(datetime.datetime.now().year) days = (returns.index[-1] - returns.index[0]).days + 1 # Create a Pandas Excel writer using XlsxWriter as the engine. writer = pd.ExcelWriter(name + ".xlsx", engine="xlsxwriter") # Convert the dataframe to an XlsxWriter Excel object. w.to_excel(writer, sheet_name="Resume", startrow=35, startcol=0) returns.to_excel(writer, sheet_name="Returns", index_label=["Date"]) # Get the xlsxwriter objects from the dataframe writer object. workbook = writer.book worksheet1 = writer.sheets["Resume"] worksheet2 = writer.sheets["Returns"] worksheet3 = workbook.add_worksheet("Portfolios") worksheet4 = workbook.add_worksheet("Absdev") worksheet5 = workbook.add_worksheet("CumRet") worksheet6 = workbook.add_worksheet("Drawdown") worksheet7 = workbook.add_worksheet("devBelowTarget") worksheet8 = workbook.add_worksheet("devBelowMean") worksheet1.hide_gridlines(2) worksheet2.hide_gridlines(2) worksheet3.hide_gridlines(2) worksheet4.hide_gridlines(2) worksheet5.hide_gridlines(2) worksheet6.hide_gridlines(2) worksheet7.hide_gridlines(2) worksheet8.hide_gridlines(2) # Cell Formats cell_format1 = workbook.add_format({"bold": True, "border": True}) cell_format2 = workbook.add_format({"bold": True, "font_size": 28, "right": True}) cell_format3 = workbook.add_format({"num_format": "0.0000%"}) cell_format4 = workbook.add_format({"num_format": "0.0000%", "border": True}) cell_format5 = workbook.add_format({"num_format": "yyyy-mm-dd", "bold": True}) cell_format6 = workbook.add_format({"num_format": "0.0000", "border": True}) cell_format7 = workbook.add_format( {"num_format": "yyyy-mm-dd", "bold": True, "border": True} ) cell_format8 = workbook.add_format({"num_format": "0,000", "border": True}) cols = xl_col_to_name(1) + ":" + xl_col_to_name(n2) worksheet1.set_column(cols, 11, cell_format3) worksheet2.set_column(cols, 9, cell_format3) worksheet2.write(0, 0, "Date", cell_format1) worksheet3.write(0, 0, "Date", cell_format1) worksheet4.write(0, 0, "Date", cell_format1) worksheet5.write(0, 0, "Date", cell_format1) worksheet6.write(0, 0, "Date", cell_format1) worksheet7.write(0, 0, "Date", cell_format1) worksheet8.write(0, 0, "Date", cell_format1) worksheet1.set_column("A:A", 35) worksheet2.set_column("A:A", 10, cell_format5) worksheet3.set_column("A:A", 10, cell_format5) worksheet4.set_column("A:A", 10, cell_format5) worksheet5.set_column("A:A", 10, cell_format5) worksheet6.set_column("A:A", 10, cell_format5) worksheet7.set_column("A:A", 10, cell_format5) worksheet8.set_column("A:A", 10, cell_format5) for i in range(0, n2): r = xl_rowcol_to_cell(i + 1, 0) formula = "=Returns!" + r + "" worksheet2.write(i + 1, 0, dates[i], cell_format7) worksheet3.write_formula(i + 1, 0, formula, cell_format7) worksheet4.write_formula(i + 1, 0, formula, cell_format7) worksheet5.write_formula(i + 1, 0, formula, cell_format7) worksheet6.write_formula(i + 1, 0, formula, cell_format7) worksheet7.write_formula(i + 1, 0, formula, cell_format7) worksheet8.write_formula(i + 1, 0, formula, cell_format7) labels_1 = [ "", "", "", "", "Profitability and Other Inputs", "Total Days in DataBase", "Mean Return (1)", "Compound Annual Growth Rate (CAGR)", "Minimum Acceptable Return (MAR) (1)", "Alpha", "", "Risk Measures based on Returns", "Standard Deviation (2)", "Mean Absolute Deviation (MAD) (2)", "Semi Standard Deviation (2)", "First Lower Partial Moment (FLPM) (2)", "Second Lower Partial Moment (SLPM) (2)", "Value at Risk (VaR) (2)", "Conditional Value at Risk (CVaR) (2)", "Entropic Value at Risk (EVaR) (2)", "Worst Realization (2)", "Skewness", "Kurtosis", "", "Risk Measures based on Drawdowns (3)", "Max Drawdown (MDD)", "Average Drawdown (ADD)", "Drawdown at Risk (DaR)", "Conditional Drawdown at Risk (CDaR)", "Ulcer Index (ULC)", ] for i in range(0, len(labels_1)): if labels_1[i] != "": worksheet1.write(i, 0, labels_1[i], cell_format1) for i in range(0, len(portfolios)): a = "Portfolio " + str(i + 1) worksheet1.write(3, 1 + i, a, cell_format1) worksheet1.write(35, 1 + i, a, cell_format1) worksheet3.write(0, 1 + i, a, cell_format1) worksheet4.write(0, 1 + i, a, cell_format1) worksheet5.write(0, 1 + i, a, cell_format1) worksheet6.write(0, 1 + i, a, cell_format1) worksheet7.write(0, 1 + i, a, cell_format1) worksheet8.write(0, 1 + i, a, cell_format1) for j in range(0, len(portfolios)): r_0 = xl_rowcol_to_cell(8, 1 + j) # MAR cell r_1 = xl_range_abs(36, 1 + j, 35 + n1, 1 + j) r_2 = xl_range_abs(1, 1 + j, n2, 1 + j) for i in range(0, n2): r_3 = xl_range(i + 1, 1, i + 1, n1) r_4 = xl_rowcol_to_cell(i + 1, 1 + j) r_5 = xl_range_abs(1, 1 + j, i + 1, 1 + j) formula1 = "{=MMULT(" + "Returns!" + r_3 + ",Resume!" + r_1 + ")}" formula2 = "=ABS(Portfolios!" + r_4 + "-AVERAGE(Portfolios!" + r_2 + "))" formula3 = "=SUM(Portfolios!" + r_5 + ")" formula4 = "=MAX(CumRet!" + r_5 + ")-CumRet!" + r_4 formula5 = ( "=MAX(Resume!" + r_0 + "/ " + str(t_factor) + "-Portfolios!" + r_4 + ", 0)" ) formula6 = "=MAX(AVERAGE(Portfolios!" + r_2 + ")-Portfolios!" + r_4 + ", 0)" worksheet3.write_formula(i + 1, 1 + j, formula1, cell_format3) worksheet4.write_formula(i + 1, 1 + j, formula2, cell_format3) worksheet5.write_formula(i + 1, 1 + j, formula3, cell_format3) worksheet6.write_formula(i + 1, 1 + j, formula4, cell_format3) worksheet7.write_formula(i + 1, 1 + j, formula5, cell_format3) worksheet8.write_formula(i + 1, 1 + j, formula6, cell_format3) r_6 = xl_rowcol_to_cell(9, 1 + j) # Alpha cell r_7 = xl_rowcol_to_cell(17, 1 + j) # Value at Risk cell AVG = "=AVERAGE(Portfolios!" + r_2 + ") * " + str(t_factor) + "" CUM = "{=PRODUCT(1 + Portfolios!" + r_2 + ")^(360/" + str(days) + ")-1}" STDEV = "=STDEV(Portfolios!" + r_2 + ") * SQRT(" + str(t_factor) + ")" MAD = "=AVERAGE(Absdev!" + r_2 + ") * SQRT(" + str(t_factor) + ")" ALPHA = "=" + str(alpha) VaR = ( "=-SMALL(Portfolios!" + r_2 + ",ROUNDUP(COUNT(Portfolios!" + r_2 + ")*" + r_6 + ",0)) * SQRT(" + str(t_factor) + ")" ) CVaR = ( "=-((SUMIF(Portfolios!" + r_2 + ',"<="&(-' + r_7 + "/SQRT(" + str(t_factor) + ")),Portfolios!" + r_2 + ")" ) CVaR += ( "-ROUNDUP(COUNT(Portfolios!" + r_2 + ")*" + r_6 + ",0)*(-" + r_7 + "/SQRT(" + str(t_factor) + ")))/(COUNT(Portfolios!" + r_2 + ")*" + r_6 + ")-" + r_7 + "/SQRT(" + str(t_factor) + ")) * SQRT(" + str(t_factor) + ")" ) EVaR = ( "=" + str(rk.EVaR_Hist(returns @ w, alpha=alpha)[0]) + " * SQRT(" + str(t_factor) + ")" ) WR = "=-MIN(Portfolios!" + r_2 + ") * SQRT(" + str(t_factor) + ")" MDD = "=MAX(Drawdown!" + r_2 + ")" ADD = "=AVERAGE(Drawdown!" + r_2 + ")" DaR = ( "=+LARGE(Drawdown!" + r_2 + ",ROUNDUP(COUNT(Drawdown!" + r_2 + ")*" + r_6 + ",0))" ) CDaR = ( "=((SUMIF(Drawdown!" + r_2 + ',">="&' + DaR[2:] + ",Drawdown!" + r_2 + ")" ) CDaR += ( "-ROUNDUP(COUNT(Drawdown!" + r_2 + ")*" + r_6 + ",0)*" + DaR[2:] + ")/(COUNT(Drawdown!" + r_2 + ")*" + r_6 + ")+" + DaR[2:] + ")" ) ULC = "=SQRT(SUMSQ(Drawdown!" + r_2 + ")/COUNT(Drawdown!" + r_2 + "))" MAR = "=" + str(rf) FLPM = "=AVERAGE(devBelowTarget!" + r_2 + ") * SQRT(" + str(t_factor) + ")" SLPM = ( "=SQRT(SUMSQ(devBelowTarget!" + r_2 + ")/(COUNT(devBelowTarget!" + r_2 + ") - 1))" + " * SQRT(" + str(t_factor) + ")" ) SDEV = ( "=SQRT(SUMSQ(devBelowMean!" + r_2 + ")/(COUNT(devBelowMean!" + r_2 + ") - 1))" + " * SQRT(" + str(t_factor) + ")" ) SKEW = "=SKEW(Portfolios!" + r_2 + ")" KURT = "=KURT(Portfolios!" + r_2 + ")" labels_2 = [ "", "", "", "", "", str(days), AVG, CUM, MAR, ALPHA, "", "", STDEV, MAD, SDEV, FLPM, SLPM, VaR, CVaR, EVaR, WR, SKEW, KURT, "", "", MDD, ADD, DaR, CDaR, ULC, ] for i in range(0, len(labels_2)): if labels_1[i] in ["Skewness", "Kurtosis"]: worksheet1.write_formula(i, 1 + j, labels_2[i], cell_format6) elif labels_1[i] in ["Total Days in DataBase"]: worksheet1.write_formula(i, 1 + j, labels_2[i], cell_format8) elif labels_2[i] != "": worksheet1.write_formula(i, 1 + j, labels_2[i], cell_format4) merge_format = workbook.add_format({"align": "Left", "valign": "vjustify"}) merge_format.set_text_wrap() worksheet1.set_row(1, 215) worksheet1.merge_range("A2:K2", __LICENSE__.replace("2021", year), merge_format) worksheet1.write(30, 0, "(1) Annualized, multiplied by " + str(t_factor)) worksheet1.write(31, 0, "(2) Annualized, multiplied by √" + str(t_factor)) worksheet1.write(32, 0, "(3) Based on uncompounded cumulated returns") worksheet1.write(0, 0, "Riskfolio-Lib Report", cell_format2) writer.save() workbook.close()
def plot_frontier( w_frontier, mu, cov=None, returns=None, rm="MV", rf=0, alpha=0.05, cmap="viridis", w=None, label="Portfolio", marker="*", s=16, c="r", height=6, width=10, ax=None, ): """ Creates a plot of the efficient frontier for a risk measure specified by the user. Parameters ---------- w_frontier : DataFrame Portfolio weights of some points in the efficient frontier. mu : DataFrame of shape (1, n_assets) Vector of expected returns, where n_assets is the number of assets. cov : DataFrame of shape (n_features, n_features) Covariance matrix, where n_features is the number of features. returns : DataFrame 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 The risk measure used to estimate the frontier. 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. - 'EVaR': Conditional Value at Risk. - 'WR': Worst Realization (Minimax) - 'MDD': Maximum Drawdown of uncompounded returns (Calmar Ratio). - 'ADD': Average Drawdown of uncompounded returns. - 'DaR': Drawdown at Risk of uncompounded returns. - 'CDaR': Conditional Drawdown at Risk of uncompounded returns. - 'UCI': Ulcer Index of uncompounded returns. rf : float, optional Risk free rate or minimum acceptable return. The default is 0. alpha : float, optional Significante level of VaR, CVaR, EVaR, DaR and CDaR. The default is 0.05. cmap : cmap, optional Colorscale, represente the risk adjusted return ratio. The default is 'viridis'. w : DataFrame of shape (n_assets, 1), optional A portfolio specified by the user. The default is None. label : str, optional Name of portfolio that appear on plot legend. The default is 'Portfolio'. marker : str, optional Marker of w_. The default is '*'. s : float, optional Size of marker. The default is 16. c : str, optional Color of marker. The default is 'r'. height : float, optional Height of the image in inches. The default is 6. width : float, optional Width of the image in inches. The default is 10. ax : matplotlib axis, optional If provided, plot on this axis. The default is None. Raises ------ ValueError When the value cannot be calculated. Returns ------- ax : matplotlib Axes Returns the Axes object with the plot for further tweaking. Example ------- :: label = 'Max Risk Adjusted Return Portfolio' mu = port.mu cov = port.cov returns = port.returns ax = plf.plot_frontier(w_frontier=ws, mu=mu, cov=cov, returns=returns, rm=rm, rf=0, alpha=0.05, cmap='viridis', w=w1, label='Portfolio', marker='*', s=16, c='r', height=6, width=10, ax=None) .. image:: images/MSV_Frontier.png """ if not isinstance(w_frontier, pd.DataFrame): raise ValueError("w_frontier must be a DataFrame") if not isinstance(mu, pd.DataFrame): raise ValueError("mu must be a DataFrame") if not isinstance(cov, pd.DataFrame): raise ValueError("cov must be a DataFrame") if not isinstance(returns, pd.DataFrame): raise ValueError("returns must be a DataFrame") if returns.shape[1] != w_frontier.shape[0]: a1 = str(returns.shape) a2 = str(w_frontier.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") if w is not None: if not isinstance(w, pd.DataFrame): raise ValueError("w must be a DataFrame") if w.shape[1] > 1 and w.shape[0] == 0: w = w.T elif w.shape[1] > 1 and w.shape[0] > 0: raise ValueError("w must be a column DataFrame") if returns.shape[1] != w.shape[0]: a1 = str(returns.shape) a2 = str(w.shape) raise ValueError("shapes " + a1 + " and " + a2 + " not aligned") mu_ = np.array(mu, ndmin=2) item = rmeasures.index(rm) x_label = rm_names[item] + " (" + rm + ")" # title = "Efficient Frontier Mean - " + x_label pretty_container_bgcolor = '#f9f9f9' fig = go.Figure( layout=go.Layout( # title=title, plot_bgcolor=pretty_container_bgcolor, hovermode='x', hoverdistance=100, spikedistance=1000, xaxis=dict( title=rm_names[item] + " (" + rm + ")", linecolor='#9a9a9a', showgrid=False, showspikes=True, spikethickness=3, spikedash='dot', spikecolor='#FF0000', spikemode='across' ), yaxis=dict( title="Expected Return", linecolor='#9a9a9a', showgrid=False ) ) ) X1 = [] Y1 = [] Z1 = [] for i in range(w_frontier.shape[1]): weights = np.array(w_frontier.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk( weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha ) ret = mu_ @ weights ret = ret.item() ratio = (ret - rf) / risk X1.append(risk) Y1.append(ret) Z1.append(ratio) fig.add_trace( go.Scatter( x=X1, y=Y1 ) ) # ax1 = ax.scatter(X1, Y1, c=Z1, cmap=cmap) if w is not None: X2 = [] Y2 = [] for i in range(w.shape[1]): weights = np.array(w.iloc[:, i], ndmin=2).T risk = rk.Sharpe_Risk( weights, cov=cov, returns=returns, rm=rm, rf=rf, alpha=alpha ) ret = mu_ @ weights ret = ret.item() ratio = (ret - rf) / risk X2.append(risk) Y2.append(ret) fig.add_trace( go.Scatter( x=X2, y=Y2 ) ) fig.update_layout( margin=dict(l=100, r=100, t=100, b=100) ) # # xmin = np.min(X1) - np.abs(np.max(X1) - np.min(X1)) * 0.1 # xmax = np.max(X1) + np.abs(np.max(X1) - np.min(X1)) * 0.1 # ymin = np.min(Y1) - np.abs(np.max(Y1) - np.min(Y1)) * 0.1 # ymax = np.max(Y1) + np.abs(np.max(Y1) - np.min(Y1)) * 0.1 # # ax.set_ylim(ymin, ymax) # ax.set_xlim(xmin, xmax) # # ax.set_yticklabels(["{:.4%}".format(x) for x in ax.get_yticks()]) # ax.set_xticklabels(["{:.4%}".format(x) for x in ax.get_xticks()]) # # ax.tick_params(axis="y", direction="in") # ax.tick_params(axis="x", direction="in") # # ax.grid(linestyle=":") # # colorbar = ax.figure.colorbar(ax1) # colorbar.set_label("Risk Adjusted Return Ratio") # # fig = plt.gcf() # fig.tight_layout() return fig