Files
PyPortfolioOpt/pypfopt/plotting.py
2021-09-26 22:37:18 +02:00

287 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
The ``plotting`` module houses all the functions to generate various plots.
Currently implemented:
- ``plot_covariance`` - plot a correlation matrix
- ``plot_dendrogram`` - plot the hierarchical clusters in a portfolio
- ``plot_efficient_frontier`` plot the efficient frontier from an EfficientFrontier or CLA object
- ``plot_weights`` - bar chart of weights
"""
import copy
import numpy as np
from . import risk_models, exceptions
from . import EfficientFrontier, CLA
import scipy.cluster.hierarchy as sch
import warnings
try:
import matplotlib.pyplot as plt
plt.style.use("seaborn-deep")
except (ModuleNotFoundError, ImportError): # pragma: no cover
raise ImportError("Please install matplotlib via pip or poetry")
def _plot_io(**kwargs):
"""
Helper method to optionally save the figure to file.
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param dpi: dpi of figure to save or plot, defaults to 300
:type dpi: int (between 50-500)
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
"""
filename = kwargs.get("filename", None)
showfig = kwargs.get("showfig", False)
dpi = kwargs.get("dpi", 300)
plt.tight_layout()
if filename:
plt.savefig(fname=filename, dpi=dpi)
if showfig: # pragma: no cover
plt.show()
def plot_covariance(cov_matrix, plot_correlation=False, show_tickers=True, **kwargs):
"""
Generate a basic plot of the covariance (or correlation) matrix, given a
covariance matrix.
:param cov_matrix: covariance matrix
:type cov_matrix: pd.DataFrame or np.ndarray
:param plot_correlation: whether to plot the correlation matrix instead, defaults to False.
:type plot_correlation: bool, optional
:param show_tickers: whether to use tickers as labels (not recommended for large portfolios),
defaults to True
:type show_tickers: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
if plot_correlation:
matrix = risk_models.cov_to_corr(cov_matrix)
else:
matrix = cov_matrix
fig, ax = plt.subplots()
cax = ax.imshow(matrix)
fig.colorbar(cax)
if show_tickers:
ax.set_xticks(np.arange(0, matrix.shape[0], 1))
ax.set_xticklabels(matrix.index)
ax.set_yticks(np.arange(0, matrix.shape[0], 1))
ax.set_yticklabels(matrix.index)
plt.xticks(rotation=90)
_plot_io(**kwargs)
return ax
def plot_dendrogram(hrp, ax=None, show_tickers=True, **kwargs):
"""
Plot the clusters in the form of a dendrogram.
:param hrp: HRPpt object that has already been optimized.
:type hrp: object
:param show_tickers: whether to use tickers as labels (not recommended for large portfolios),
defaults to True
:type show_tickers: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
ax = ax or plt.gca()
if hrp.clusters is None:
warnings.warn(
"hrp param has not been optimized. Attempting optimization.",
RuntimeWarning,
)
hrp.optimize()
if show_tickers:
sch.dendrogram(hrp.clusters, labels=hrp.tickers, ax=ax, orientation="top")
ax.tick_params(axis="x", rotation=90)
plt.tight_layout()
else:
sch.dendrogram(hrp.clusters, no_labels=True, ax=ax)
_plot_io(**kwargs)
return ax
def _plot_cla(cla, points, ax, show_assets):
"""
Helper function to plot the efficient frontier from a CLA object
"""
if cla.weights is None:
cla.max_sharpe()
optimal_ret, optimal_risk, _ = cla.portfolio_performance()
if cla.frontier_values is None:
cla.efficient_frontier(points=points)
mus, sigmas, _ = cla.frontier_values
ax.plot(sigmas, mus, label="Efficient frontier")
ax.scatter(optimal_risk, optimal_ret, marker="x", s=100, color="r", label="optimal")
if show_assets:
ax.scatter(
np.sqrt(np.diag(cla.cov_matrix)),
cla.expected_returns,
s=30,
color="k",
label="assets",
)
return ax
def _ef_default_returns_range(ef, points):
"""
Helper function to generate a range of returns from the GMV returns to
the maximum (constrained) returns
"""
ef_minvol = copy.deepcopy(ef)
ef_maxret = copy.deepcopy(ef)
ef_minvol.min_volatility()
min_ret = ef_minvol.portfolio_performance()[0]
max_ret = ef_maxret._max_return()
return np.linspace(min_ret, max_ret - 0.0001, points)
def _plot_ef(ef, ef_param, ef_param_range, ax, show_assets):
"""
Helper function to plot the efficient frontier from an EfficientFrontier object
"""
mus, sigmas = [], []
# Create a portfolio for each value of ef_param_range
for param_value in ef_param_range:
try:
if ef_param == "utility":
ef.max_quadratic_utility(param_value)
elif ef_param == "risk":
ef.efficient_risk(param_value)
elif ef_param == "return":
ef.efficient_return(param_value)
else:
raise NotImplementedError(
"ef_param should be one of {'utility', 'risk', 'return'}"
)
except exceptions.OptimizationError:
continue
except ValueError:
warnings.warn(
"Could not construct portfolio for paramter value {:.3f}".format(
param_value
)
)
ret, sigma, _ = ef.portfolio_performance()
mus.append(ret)
sigmas.append(sigma)
ax.plot(sigmas, mus, label="Efficient frontier")
if show_assets:
ax.scatter(
np.sqrt(np.diag(ef.cov_matrix)),
ef.expected_returns,
s=30,
color="k",
label="assets",
)
return ax
def plot_efficient_frontier(
opt,
ef_param="return",
ef_param_range=None,
points=100,
ax=None,
show_assets=True,
**kwargs
):
"""
Plot the efficient frontier based on either a CLA or EfficientFrontier object.
:param opt: an instantiated optimizer object BEFORE optimising an objective
:type opt: EfficientFrontier or CLA
:param ef_param: [EfficientFrontier] whether to use a range over utility, risk, or return.
Defaults to "return".
:type ef_param: str, one of {"utility", "risk", "return"}.
:param ef_param_range: the range of parameter values for ef_param.
If None, automatically compute a range from min->max return.
:type ef_param_range: np.array or list (recommended to use np.arange or np.linspace)
:param points: number of points to plot, defaults to 100. This is overridden if
an `ef_param_range` is provided explicitly.
:type points: int, optional
:param show_assets: whether we should plot the asset risks/returns also, defaults to True
:type show_assets: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to False
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
ax = ax or plt.gca()
if isinstance(opt, CLA):
ax = _plot_cla(opt, points, ax=ax, show_assets=show_assets)
elif isinstance(opt, EfficientFrontier):
if ef_param_range is None:
ef_param_range = _ef_default_returns_range(opt, points)
ax = _plot_ef(opt, ef_param, ef_param_range, ax=ax, show_assets=show_assets)
else:
raise NotImplementedError("Please pass EfficientFrontier or CLA object")
ax.legend()
ax.set_xlabel("Volatility")
ax.set_ylabel("Return")
_plot_io(**kwargs)
return ax
def plot_weights(weights, ax=None, **kwargs):
"""
Plot the portfolio weights as a horizontal bar chart
:param weights: the weights outputted by any PyPortfolioOpt optimizer
:type weights: {ticker: weight} dict
:param ax: ax to plot to, optional
:type ax: matplotlib.axes
:return: matplotlib axis
:rtype: matplotlib.axes
"""
ax = ax or plt.gca()
desc = sorted(weights.items(), key=lambda x: x[1], reverse=True)
labels = [i[0] for i in desc]
vals = [i[1] for i in desc]
y_pos = np.arange(len(labels))
ax.barh(y_pos, vals)
ax.set_xlabel("Weight")
ax.set_yticks(y_pos)
ax.set_yticklabels(labels)
ax.invert_yaxis()
_plot_io(**kwargs)
return ax