robertmartin8 / PyPortfolioOpt

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

efficient_risk raises "LinAlgError: Singular matrix" if you include a risk-free asset #456

Closed agj60 closed 2 years ago

agj60 commented 2 years ago

Description If you include a risk-free asset with zero volatility, the set of maximum sharpe ratio portfolios lie in a straight line that represents a linear combination between the risk-free asset and the tangent portfolio of the efficient frontier that does not include the risk-free asset, the so called "securities market line". That works perfectly well if you calculate efficient_return portfolios: if you aim at half the volatility of the tangent portfolio, you get a portfolio with half the risk-free asset, half the components of the tangent portfolio. But if you try to calculate efficient_risk portfolios, you get a "LinAlgError: Singular matrix"

Workaround A workaround is to test the diagonal of the covariance matrix and use a very small variance instead of zero, say 1e-32 (or change the zero volatility to 1e-16), in order to avoid the singularity.

Code sample import numpy as np from pypfopt.efficient_frontier import EfficientFrontier

ret = np.array([[.0, .06, .10]]).T # Returns vol = np.array([[.0,.08,.15]]).T # Volatilities -- workaround change .0 to 1e-16 rho = np.array([[1.,.0,.0],[.0,1.,.0],[.0,.0,1.]]) # Correlations cov = np.diagflat(vol)@rho@np.diagflat(vol)

ef = EfficientFrontier(ret, cov) ef.efficient_risk(0.0726/2) # 0.0726 is the return of the tangent portfolio # you should get weights of [0.50, 0.34, 0.16]

Versions UBUNTU 18.04.5 LTS (Bionic Beaver) Python 3.7.13 PyPortfolioOpt 1.5.2 on Google Colab

agj60 commented 2 years ago

Not mentioned: you have to impose a restriction of x[0] == 0 to get the correct tangent portfolio if you include a risk-free asset in the set: ef = EfficientFrontier(ret, cov) ef.add_constraint(lambda x: x[0]==0) ef.max_sharpe(ret[0],[0]) # you should get weights of [0., .68, .32] Otherwise you are going to get some random allocation to the risk-free asset, probably due to precision issues with the infinitely many solution over the security market line with a a constant maximum sharpe ratio.

phschiele commented 2 years ago

@agj60 Can you try upgrading to version 1.5.3 ? Your issue should be fixed by https://github.com/robertmartin8/PyPortfolioOpt/pull/453/commits/e88bb1564571c2a27cc29168c6fc2602442d64aa , which was included in the 1.5.3 release.

agj60 commented 2 years ago

Using a pseudoinverse will definitely do the trick! 1.5.3 is available for python >=3.8 only, but thanks for pointing it out!