Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into v1.5.6
  • Loading branch information
robertmartin8 committed Dec 1, 2024
2 parents 4e1ccc9 + 348d2a0 commit 7b51c91
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 10 deletions.
4 changes: 4 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ COPY requirements.txt .

RUN pip install --upgrade pip \
pip install yfinance && \
pip install poetry \
pip install ipython \
pip install jupyter \
pip install pytest \
pip install -r requirements.txt

COPY . .
Expand Down
2 changes: 1 addition & 1 deletion docs/Roadmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ PyPortfolioOpt is now a "mature" package – it is stable and I don't intend to
=====

- Major redesign of the backend, thanks to `Philipp Schiele <https://github.com/phschiele>`_
- Becuase we use ``cp.Parameter``, we can efficiently re-run optimisation problems with different constants (e.g risk targets)
- Because we use ``cp.Parameter``, we can efficiently re-run optimisation problems with different constants (e.g risk targets)
- This leads to a significant improvement in plotting performance as we no longer have to repeatedly re-instantiate ``EfficientFrontier``.
- Several misc bug fixes (thanks to `Eric Armbruster <https://github.com/armbruer>`_ and `Ayoub Ennassiri <https://github.com/samatix>`_)

Expand Down
27 changes: 24 additions & 3 deletions pypfopt/black_litterman.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,14 @@ def bl_returns(self):
if self._A is None:
self._A = (self.P @ self._tau_sigma_P) + self.omega
b = self.Q - self.P @ self.pi
post_rets = self.pi + self._tau_sigma_P @ np.linalg.solve(self._A, b)
try:
solution = np.linalg.solve(self._A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
post_rets = self.pi + self._tau_sigma_P @ solution
return pd.Series(post_rets.flatten(), index=self.tickers)

def bl_cov(self):
Expand All @@ -423,7 +430,14 @@ def bl_cov(self):
self._A = (self.P @ self._tau_sigma_P) + self.omega

b = self._tau_sigma_P.T
M = self.tau * self.cov_matrix - self._tau_sigma_P @ np.linalg.solve(self._A, b)
try:
M_solution = np.linalg.solve(self._A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
M_solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
M = self.tau * self.cov_matrix - self._tau_sigma_P @ M_solution
posterior_cov = self.cov_matrix + M
return pd.DataFrame(posterior_cov, index=self.tickers, columns=self.tickers)

Expand All @@ -449,7 +463,14 @@ def bl_weights(self, risk_aversion=None):
self.posterior_rets = self.bl_returns()
A = risk_aversion * self.cov_matrix
b = self.posterior_rets
raw_weights = np.linalg.solve(A, b)
try:
weight_solution = np.linalg.solve(A, b)
except np.linalg.LinAlgError as e:
if 'Singular matrix' in str(e):
weight_solution = np.linalg.lstsq(self._A, b, rcond=None)[0]
else:
raise e
raw_weights = weight_solution
self.weights = raw_weights / raw_weights.sum()
return self._make_output_weights()

Expand Down
2 changes: 1 addition & 1 deletion pypfopt/efficient_frontier/efficient_frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def max_sharpe(self, risk_free_rate=0.02):

# max_sharpe requires us to make a variable transformation.
# Here we treat w as the transformed variable.
self._objective = cp.quad_form(self._w, self.cov_matrix)
self._objective = cp.quad_form(self._w, self.cov_matrix, assume_PSD=True)
k = cp.Variable()

# Note: objectives are not scaled by k. Hence there are subtle differences
Expand Down
4 changes: 2 additions & 2 deletions pypfopt/expected_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def returns_from_prices(prices, log_returns=False):
:rtype: pd.DataFrame
"""
if log_returns:
returns = np.log(1 + prices.pct_change()).dropna(how="all")
returns = np.log(1 + prices.pct_change(fill_method=None)).dropna(how="all")
else:
returns = prices.pct_change().dropna(how="all")
returns = prices.pct_change(fill_method=None).dropna(how="all")
return returns


Expand Down
6 changes: 3 additions & 3 deletions pypfopt/objective_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def portfolio_variance(w, cov_matrix):
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
variance = cp.quad_form(w, cov_matrix)
variance = cp.quad_form(w, cov_matrix, assume_PSD=True)
return _objective_value(w, variance)


Expand Down Expand Up @@ -109,7 +109,7 @@ def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative=
:rtype: float
"""
mu = w @ expected_returns
sigma = cp.sqrt(cp.quad_form(w, cov_matrix))
sigma = cp.sqrt(cp.quad_form(w, cov_matrix, assume_PSD=True))
sign = -1 if negative else 1
sharpe = (mu - risk_free_rate) / sigma
return _objective_value(w, sign * sharpe)
Expand Down Expand Up @@ -156,7 +156,7 @@ def quadratic_utility(w, expected_returns, cov_matrix, risk_aversion, negative=T
"""
sign = -1 if negative else 1
mu = w @ expected_returns
variance = cp.quad_form(w, cov_matrix)
variance = cp.quad_form(w, cov_matrix, assume_PSD=True)

risk_aversion_par = cp.Parameter(
value=risk_aversion, name="risk_aversion", nonneg=True
Expand Down
96 changes: 96 additions & 0 deletions tests/resources/cov_matrix.csv

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions tests/test_efficient_frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
risk_models,
)
from tests.utilities_for_tests import (
get_cov_matrix,
get_data,
setup_efficient_frontier,
simple_ef_weights,
Expand Down Expand Up @@ -354,6 +355,14 @@ def test_min_volatility_nonlinear_constraint():
ef.min_volatility()


def test_min_volatility_large_cov_matrix():
# test that a large covariance matrix will not fail the PSD check in cvxpy
cov_matrix = get_cov_matrix()
mean_return = pd.Series([0.1] * len(cov_matrix), index=cov_matrix.index)
ef = EfficientFrontier(mean_return, cov_matrix, verbose=True)
ef.min_volatility()


def test_max_returns():
ef = setup_efficient_frontier()
#  In unconstrained case, should equal maximal asset return
Expand Down
4 changes: 4 additions & 0 deletions tests/utilities_for_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def get_market_caps():
return mcaps


def get_cov_matrix():
return pd.read_csv(resource("cov_matrix.csv"), index_col=0)


def setup_efficient_frontier(data_only=False, *args, **kwargs):
df = get_data()
mean_return = expected_returns.mean_historical_return(df)
Expand Down

0 comments on commit 7b51c91

Please sign in to comment.