robertmartin8 / PyPortfolioOpt

Financial portfolio optimisation in python, including classical efficient frontier, Black-Litterman, Hierarchical Risk Parity
https://pyportfolioopt.readthedocs.io/
MIT License
4.51k stars 956 forks source link

Plot entire Efficient Frontier #304

Closed joostbos123 closed 3 years ago

joostbos123 commented 3 years ago

Hi!

I'm trying to use the package to plot the efficient frontier for a set of assets. Just to make the plot a bit nicer, I would like to plot the entire efficient frontier including the lower part (where the returns are lower then the retrun of the minimum risk portfolio). In the example I only see a plot of the upper part of the frontier. Is there a way to get this entire frontier using the package?

Kind regards, Joost

phschiele commented 3 years ago

Hi @joostbos123,

great question! Indeed, you should be able to plot the inefficient part of the frontier in most cases. If you are in the completely unconstrained case (i.e. not even a short-sale constraint), you can use the closed-form solution by Merton (1972) discussed in #300.

If you are in a constrained case, consider the following: You want to minimize the return for a given volatility. Minimizing the portfolio returns is equivalent to maximizing the negative returns (in fact, the return maximization is even implemented as minimization of negative expected returns).

Hacking a bit into plot_efficient_frontier() to return the list of sigmas and mus, you can do something like:

# with constraints
ef = setup_efficient_frontier()
ef.add_constraint(lambda x: x <= 0.15)
ef.add_constraint(lambda x: x[0] == 0.05)
_, sigmas1, mus1 = plotting.plot_efficient_frontier(ef)

mean_return, sample_cov_matrix = setup_efficient_frontier(data_only=True)
negative_returns = - mean_return
inefficient_frontier = EfficientFrontier(negative_returns, sample_cov_matrix)
inefficient_frontier.add_constraint(lambda x: x <= 0.15)
inefficient_frontier.add_constraint(lambda x: x[0] == 0.05)
_, sigmas2, negative_mus2 = plotting.plot_efficient_frontier(inefficient_frontier)
mus2 = [-mu for mu in negative_mus2]

plt.figure()
plt.plot(list(reversed(sigmas1))+sigmas2, list(reversed(mus1))+mus2)
plt.show()

This yields the following plot, I'll leave it to you to make the formatting pretty :)

image

Notice that in the constrained case, the frontier is not reflection symmetric, which can be seen when "flipping up" the inefficient part of the frontier:

plt.figure()
flipped_up_mus = [(max(mus2) - mu) + max(mus2) for mu in mus2]
plt.plot(list(reversed(sigmas1))+sigmas2, list(reversed(mus1))+flipped_up_mus)
plt.show()

image

This approach should work for cases where you do not have further concave objectives in the return maximization objective.

Hope this helps!

Edit: @robertmartin8 The icon of PyPortfolioOpt even shows the inefficient part of the frontier! Perhaps one could add it as a (non-default) "vanity" option to the plotting API :sparkles: But since it does not serve any purpose from the portfolio optimization perspective itself, that's probably debatable.

joostbos123 commented 3 years ago

Thanks a lot for your quick reply and the suggested workaround the get the entire plot in the constrained case!

However, when I use this method on a set of assets of which some assets have negative expected returns (mu) then I recieve the error _"targetreturn should be a positive float" when trying to plot the lower part. This is caused by negative values of the target_return input argument of efficient_return function in effcient_frontier.py.

Do you have any suggestions to overcome this problem as well?

Kind regards, Joost

phschiele commented 3 years ago

@joostbos123 Yes, you simply need to comment out this assertion.

robertmartin8 commented 3 years ago

This is a nice workaround, but unless there is a very simple implementation, I think there are other features that need to be prioritised