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