diff --git a/docs/EfficientFrontier.rst b/docs/EfficientFrontier.rst index b76aa6d..8b01eeb 100644 --- a/docs/EfficientFrontier.rst +++ b/docs/EfficientFrontier.rst @@ -70,8 +70,6 @@ Basic Usage .. automodule:: pypfopt.efficient_frontier .. autoclass:: EfficientFrontier - :members: - :exclude-members: custom_objective .. automethod:: __init__ @@ -85,6 +83,7 @@ Basic Usage If you want to generate short-only portfolios, there is a quick hack. Multiply your expected returns by -1, then optimise a long-only portfolio. + .. automethod:: min_volatility .. automethod:: max_sharpe @@ -110,6 +109,8 @@ Basic Usage :py:meth:`efficient_return`, the optimiser will fail silently and return weird weights. *Caveat emptor* applies! + .. automethod:: efficient_return + .. automethod:: portfolio_performance .. tip:: @@ -123,6 +124,12 @@ Basic Usage weights, expected_returns, cov_matrix, verbose=True, risk_free_rate=0.02 ) +.. note:: + + PyPortfolioOpt defers to cvxpy's default choice of solver. If you would like to explicitly + choose the solver and see verbose output, simply assign ``ef.solver = "ECOS"`` prior to calling + the actual optimisation method. You can choose from any of the `supported solvers `_. + Adding objectives and constraints ================================= diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index 0a056a9..01a6e57 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -113,6 +113,7 @@ class BaseConvexOptimizer(BaseOptimizer): - ``n_assets`` - int - ``tickers`` - str list - ``weights`` - np.ndarray + - ``solver`` - str Public methods: @@ -144,6 +145,8 @@ class BaseConvexOptimizer(BaseOptimizer): self._upper_bounds = None self._map_bounds_to_constraints(weight_bounds) + self.solver = None + def _map_bounds_to_constraints(self, test_bounds): """ Process input bounds into a form acceptable by cvxpy and add to the constraints list. @@ -193,7 +196,11 @@ class BaseConvexOptimizer(BaseOptimizer): """ try: opt = cp.Problem(cp.Minimize(self._objective), self._constraints) - opt.solve() + + if self.solver is not None: + opt.solve(solver=self.solver, verbose=True) + else: + opt.solve() except (TypeError, cp.DCPError): raise exceptions.OptimizationError if opt.status != "optimal": diff --git a/pypfopt/efficient_frontier.py b/pypfopt/efficient_frontier.py index c454aa8..ebc66db 100644 --- a/pypfopt/efficient_frontier.py +++ b/pypfopt/efficient_frontier.py @@ -28,7 +28,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): - ``bounds`` - float tuple OR (float tuple) list - ``cov_matrix`` - np.ndarray - ``expected_returns`` - np.ndarray - + - ``solver`` - str - Output: ``weights`` - np.ndarray @@ -265,7 +265,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): :return: asset weights for the efficient risk portfolio :rtype: dict """ - if not isinstance(target_volatility, float) or target_volatility < 0: + if not isinstance(target_volatility, (float, int)) or target_volatility < 0: raise ValueError("target_volatility should be a positive float") self._objective = objective_functions.portfolio_return( diff --git a/tests/test_efficient_frontier.py b/tests/test_efficient_frontier.py index f1112de..9f4ebd3 100644 --- a/tests/test_efficient_frontier.py +++ b/tests/test_efficient_frontier.py @@ -65,6 +65,28 @@ def test_min_volatility(): ) +def test_min_volatility_different_solver(): + ef = setup_efficient_frontier() + ef.solver = "ECOS" + w = ef.min_volatility() + assert isinstance(w, dict) + assert set(w.keys()) == set(ef.tickers) + np.testing.assert_almost_equal(ef.weights.sum(), 1) + assert all([i >= 0 for i in w.values()]) + test_performance = (0.179312, 0.159151, 1.001015) + np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-5) + + ef = setup_efficient_frontier() + ef.solver = "OSQP" + w = ef.min_volatility() + np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-5) + + ef = setup_efficient_frontier() + ef.solver = "SCS" + w = ef.min_volatility() + np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-3) + + def test_min_volatility_no_rets(): # Should work with no rets, see issue #82 df = get_data()