diff --git a/README.md b/README.md index 3278e52..c31e381 100755 --- a/README.md +++ b/README.md @@ -132,8 +132,7 @@ raw_weights = ef.max_sharpe() cleaned_weights = ef.clean_weights() ef.save_weights_to_file("weights.csv") # saves to file print(cleaned_weights) -ef.verbose = True -ef.portfolio_performance() +ef.portfolio_performance(verbose=True) ``` This outputs the following weights: diff --git a/cookbook/2-Mean-Variance-Optimisation.ipynb b/cookbook/2-Mean-Variance-Optimisation.ipynb index c7d386c..9d5e4db 100644 --- a/cookbook/2-Mean-Variance-Optimisation.ipynb +++ b/cookbook/2-Mean-Variance-Optimisation.ipynb @@ -930,8 +930,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -1256,8 +1255,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -1419,8 +1417,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -1494,8 +1491,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -1566,4 +1562,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb b/cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb index 5d2e5b9..361f57c 100644 --- a/cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb +++ b/cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb @@ -556,8 +556,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -686,8 +685,7 @@ } ], "source": [ - "ef.verbose = True\n", - "ef.portfolio_performance();" + "ef.portfolio_performance(verbose=True);" ] }, { @@ -1167,4 +1165,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/cookbook/HierarchicalRiskParity.ipynb b/cookbook/HierarchicalRiskParity.ipynb index ad554eb..9115cd1 100644 --- a/cookbook/HierarchicalRiskParity.ipynb +++ b/cookbook/HierarchicalRiskParity.ipynb @@ -32,8 +32,7 @@ "returns = risk_models.returns_from_prices(df)\n", "hrp = HRPOpt(returns)\n", "weights = hrp.optimize()\n", - "hrp.verbose = True\n", - "hrp.portfolio_performance();" + "hrp.portfolio_performance(verbose=True);" ] }, { @@ -86,4 +85,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/docs/EfficientFrontier.rst b/docs/EfficientFrontier.rst index b941cb2..9c8f9a0 100644 --- a/docs/EfficientFrontier.rst +++ b/docs/EfficientFrontier.rst @@ -121,14 +121,14 @@ Basic Usage from pypfopt import base_optimizer base_optimizer.portfolio_performance( - weights, expected_returns, cov_matrix, risk_free_rate=0.02 + 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, simply assign ``ef.solver = "ECOS"`` prior to calling the actual optimisation - method. You can choose from any of the `supported solvers `_. + choose the solver, simply pass the optional ``solver = "ECOS"`` kwarg to the constructor. + You can choose from any of the `supported solvers `_. Adding objectives and constraints ================================= diff --git a/docs/UserGuide.rst b/docs/UserGuide.rst index a026805..fb556c3 100644 --- a/docs/UserGuide.rst +++ b/docs/UserGuide.rst @@ -117,7 +117,7 @@ portfolio) – the set of all these optimal portfolios is referred to as the .. image:: ../media/efficient_frontier.png :align: center - :alt: risk-return characteristics of possible portfolios + :alt: risk-return characteristics of possible portfolios Each dot on this diagram represents a different possible portfolio, with darker blue corresponding to 'better' portfolios (in terms of the Sharpe Ratio). The dotted @@ -178,8 +178,7 @@ This prints:: If we want to know the expected performance of the portfolio with optimal weights ``w``, we can use the :py:meth:`portfolio_performance` method:: - ef.verbose = True - ef.portfolio_performance() + ef.portfolio_performance(verbose=True) .. code-block:: text diff --git a/docs/index.rst b/docs/index.rst index 15cf961..27938c0 100755 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,8 +65,8 @@ The alternative is to clone/download the project, then in the project directory python setup.py install -Thanks to Thomas Schmelzer, PyPortfolioOpt now supports Docker (requires -**make**, **docker**, **docker-compose**). Build your first container with +Thanks to Thomas Schmelzer, PyPortfolioOpt now supports Docker (requires +**make**, **docker**, **docker-compose**). Build your first container with ``make build``; run tests with ``make test``. For more information, please read `this guide `_. @@ -131,8 +131,7 @@ that's fine too:: # Optimise for maximal Sharpe ratio ef = EfficientFrontier(mu, S) weights = ef.max_sharpe() - ef.verbose = True - ef.portfolio_performance() + ef.portfolio_performance(verbose=True) This outputs the following: @@ -160,7 +159,7 @@ Contents .. toctree:: :caption: Other information - + Roadmap Contributing About diff --git a/examples.py b/examples.py index da22fd6..e8cfb93 100644 --- a/examples.py +++ b/examples.py @@ -28,8 +28,7 @@ def deviation_risk_parity(w, cov_matrix): ef = EfficientFrontier(mu, S) weights = ef.nonconvex_objective(deviation_risk_parity, ef.cov_matrix) -ef.verbose = True -ef.portfolio_performance() +ef.portfolio_performance(verbose=True) """ Expected annual return: 22.9% @@ -83,8 +82,7 @@ rets = bl.bl_returns() ef = EfficientFrontier(rets, S) ef.max_sharpe() print(ef.clean_weights()) -ef.verbose = True -ef.portfolio_performance() +ef.portfolio_performance(verbose=True) """ {'GOOG': 0.2015, @@ -117,8 +115,7 @@ Sharpe Ratio: 0.46 # Hierarchical risk parity hrp = HRPOpt(returns) weights = hrp.optimize() -hrp.verbose = True -hrp.portfolio_performance() +hrp.portfolio_performance(verbose=True) print(weights) plotting.plot_dendrogram(hrp) # to plot dendrogram @@ -153,8 +150,7 @@ Sharpe Ratio: 0.66 # Crticial Line Algorithm cla = CLA(mu, S) print(cla.max_sharpe()) -cla.verbose = True -cla.portfolio_performance() +cla.portfolio_performance(verbose=True) plotting.plot_efficient_frontier(cla) # to plot """ diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index dafffbc..a343439 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -47,7 +47,6 @@ class BaseOptimizer: self.tickers = tickers # Outputs self.weights = None - self.verbose = False def _make_output_weights(self, weights=None): """ @@ -142,12 +141,16 @@ class BaseConvexOptimizer(BaseOptimizer): - ``save_weights_to_file()`` saves the weights to csv, json, or txt. """ - def __init__(self, n_assets, tickers=None, weight_bounds=(0, 1)): + def __init__(self, n_assets, tickers=None, weight_bounds=(0, 1), solver=None, verbose=False): """ :param weight_bounds: minimum and maximum weight of each asset OR single min/max pair if all identical, defaults to (0, 1). Must be changed to (-1, 1) for portfolios with shorting. :type weight_bounds: tuple OR tuple list, optional + :param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()` + :type solver: str, optional (see cvxpy.Problem#_solve for default. spoiler: it's ECOS) + :param verbose: whether performance and debugging info should be printed, defaults to False + :type verbose: bool, optional """ super().__init__(n_assets, tickers) @@ -160,7 +163,8 @@ class BaseConvexOptimizer(BaseOptimizer): self._upper_bounds = None self._map_bounds_to_constraints(weight_bounds) - self.solver = None + self._solver = solver + self._verbose = verbose def _map_bounds_to_constraints(self, test_bounds): """ @@ -212,10 +216,10 @@ class BaseConvexOptimizer(BaseOptimizer): try: opt = cp.Problem(cp.Minimize(self._objective), self._constraints) - if self.solver is not None: - opt.solve(solver=self.solver, verbose=self.verbose) + if self._solver is not None: + opt.solve(solver=self._solver, verbose=self._verbose) else: - opt.solve(verbose=self.verbose) + opt.solve(verbose=self._verbose) except (TypeError, cp.DCPError): raise exceptions.OptimizationError diff --git a/pypfopt/black_litterman.py b/pypfopt/black_litterman.py index 9bb0594..99e43f2 100644 --- a/pypfopt/black_litterman.py +++ b/pypfopt/black_litterman.py @@ -451,7 +451,7 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer): """ return self.bl_weights(risk_aversion) - def portfolio_performance(self, risk_free_rate=0.02): + def portfolio_performance(self, risk_free_rate=0.02, verbose=False): """ After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. @@ -461,6 +461,8 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer): The period of the risk-free rate should correspond to the frequency of expected returns. :type risk_free_rate: float, optional + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional :raises ValueError: if weights have not been calcualted yet :return: expected return, volatility, Sharpe ratio. :rtype: (float, float, float) @@ -471,6 +473,6 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer): self.weights, self.posterior_rets, self.posterior_cov, - self.verbose, + verbose, risk_free_rate, ) diff --git a/pypfopt/cla.py b/pypfopt/cla.py index 952d604..6246b7c 100644 --- a/pypfopt/cla.py +++ b/pypfopt/cla.py @@ -445,13 +445,15 @@ class CLA(base_optimizer.BaseOptimizer): # Overrides parent method since set_weights does nothing. raise NotImplementedError("set_weights does nothing for CLA") - def portfolio_performance(self, risk_free_rate=0.02): + def portfolio_performance(self, risk_free_rate=0.02, verbose=False): """ After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02 :type risk_free_rate: float, optional + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional :raises ValueError: if weights have not been calculated yet :return: expected return, volatility, Sharpe ratio. :rtype: (float, float, float) @@ -460,6 +462,6 @@ class CLA(base_optimizer.BaseOptimizer): self.weights, self.expected_returns, self.cov_matrix, - self.verbose, + verbose, risk_free_rate, ) diff --git a/pypfopt/efficient_frontier.py b/pypfopt/efficient_frontier.py index d8f5068..fc82a63 100644 --- a/pypfopt/efficient_frontier.py +++ b/pypfopt/efficient_frontier.py @@ -53,7 +53,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): - ``save_weights_to_file()`` saves the weights to csv, json, or txt. """ - def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1), gamma=0): + def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1), gamma=0, solver=None, verbose=False): """ :param expected_returns: expected returns for each asset. Can be None if optimising for volatility only (but not recommended). @@ -68,6 +68,10 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): :param gamma: L2 regularisation parameter, defaults to 0. Increase if you want more non-negligible weights :type gamma: float, optional + :param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()` + :type solver: str + :param verbose: whether performance and debugging info should be printed, defaults to False + :type verbose: bool, optional :raises TypeError: if ``expected_returns`` is not a series, list or array :raises TypeError: if ``cov_matrix`` is not a dataframe or array """ @@ -89,7 +93,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): if cov_matrix.shape != (len(expected_returns), len(expected_returns)): raise ValueError("Covariance matrix does not match expected returns") - super().__init__(len(tickers), tickers, weight_bounds) + super().__init__(len(tickers), tickers, weight_bounds, solver=solver, verbose=verbose) @staticmethod def _validate_expected_returns(expected_returns): @@ -333,7 +337,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): return self._solve_cvxpy_opt_problem() - def portfolio_performance(self, risk_free_rate=0.02): + def portfolio_performance(self, risk_free_rate=0.02, verbose=False): """ After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. @@ -342,6 +346,8 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): The period of the risk-free rate should correspond to the frequency of expected returns. :type risk_free_rate: float, optional + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional :raises ValueError: if weights have not been calcualted yet :return: expected return, volatility, Sharpe ratio. :rtype: (float, float, float) @@ -350,6 +356,6 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): self.weights, self.expected_returns, self.cov_matrix, - self.verbose, + verbose, risk_free_rate, ) diff --git a/pypfopt/hierarchical_portfolio.py b/pypfopt/hierarchical_portfolio.py index 0031da1..2355b9e 100644 --- a/pypfopt/hierarchical_portfolio.py +++ b/pypfopt/hierarchical_portfolio.py @@ -167,7 +167,7 @@ class HRPOpt(base_optimizer.BaseOptimizer): self.set_weights(weights) return weights - def portfolio_performance(self, risk_free_rate=0.02, frequency=252): + def portfolio_performance(self, risk_free_rate=0.02, frequency=252, verbose=False): """ After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio @@ -180,7 +180,9 @@ class HRPOpt(base_optimizer.BaseOptimizer): :param frequency: number of time periods in a year, defaults to 252 (the number of trading days in a year) :type frequency: int, optional - :raises ValueError: if weights have not been calcualted yet + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional + :raises ValueError: if weights have not been calculated yet :return: expected return, volatility, Sharpe ratio. :rtype: (float, float, float) """ @@ -192,5 +194,5 @@ class HRPOpt(base_optimizer.BaseOptimizer): mu = self.returns.mean() * frequency return base_optimizer.portfolio_performance( - self.weights, mu, cov, self.verbose, risk_free_rate + self.weights, mu, cov, verbose, risk_free_rate ) diff --git a/tests/test_base_optimizer.py b/tests/test_base_optimizer.py index eee8fa6..6453142 100644 --- a/tests/test_base_optimizer.py +++ b/tests/test_base_optimizer.py @@ -208,13 +208,11 @@ def test_save_weights_to_file(): os.remove("tests/test.json") def assert_verbose_option(optimize_for_method, *args, solver=None): - ef = setup_efficient_frontier() - ef.solver = solver - # using a random number for `verbose` simply to test that what is received # by the method is passed on to Problem#solve verbose=random() - ef.verbose = verbose + + ef = setup_efficient_frontier(solver=solver, verbose=verbose) with patch("cvxpy.Problem.solve") as mock: with pytest.raises(exceptions.OptimizationError): diff --git a/tests/test_discrete_allocation.py b/tests/test_discrete_allocation.py index 155717e..60214b6 100644 --- a/tests/test_discrete_allocation.py +++ b/tests/test_discrete_allocation.py @@ -208,7 +208,7 @@ def test_lp_allocation_rmse_error(): latest_prices = get_latest_prices(df) da = DiscreteAllocation(w, latest_prices) da.lp_portfolio() - np.testing.assert_almost_equal(da._allocation_rmse_error(), 0.017070218149194846) + np.testing.assert_almost_equal(da._allocation_rmse_error(verbose=False), 0.017070218149194846) def test_lp_portfolio_allocation_short(): @@ -310,18 +310,18 @@ def test_rmse_decreases_with_value(): da1 = DiscreteAllocation(w, latest_prices, total_portfolio_value=10000) da1.greedy_portfolio() - rmse1 = da1._allocation_rmse_error() + rmse1 = da1._allocation_rmse_error(verbose=False) da2 = DiscreteAllocation(w, latest_prices, total_portfolio_value=100000) da2.greedy_portfolio() - rmse2 = da2._allocation_rmse_error() + rmse2 = da2._allocation_rmse_error(verbose=False) assert rmse2 < rmse1 da3 = DiscreteAllocation(w, latest_prices, total_portfolio_value=10000) da3.lp_portfolio() - rmse3 = da3._allocation_rmse_error() + rmse3 = da3._allocation_rmse_error(verbose=False) da4 = DiscreteAllocation(w, latest_prices, total_portfolio_value=30000) da4.lp_portfolio() - rmse4 = da4._allocation_rmse_error() + rmse4 = da4._allocation_rmse_error(verbose=False) assert rmse4 < rmse3 diff --git a/tests/test_efficient_frontier.py b/tests/test_efficient_frontier.py index 35c3145..a5fda23 100644 --- a/tests/test_efficient_frontier.py +++ b/tests/test_efficient_frontier.py @@ -66,8 +66,7 @@ def test_min_volatility(): def test_min_volatility_different_solver(): - ef = setup_efficient_frontier() - ef.solver = "ECOS" + ef = setup_efficient_frontier(solver="ECOS") w = ef.min_volatility() assert isinstance(w, dict) assert set(w.keys()) == set(ef.tickers) @@ -76,13 +75,11 @@ def test_min_volatility_different_solver(): 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" + ef = setup_efficient_frontier(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" + ef = setup_efficient_frontier(solver="SCS") w = ef.min_volatility() np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-3) diff --git a/tests/utilities_for_tests.py b/tests/utilities_for_tests.py index 042048b..87069fc 100644 --- a/tests/utilities_for_tests.py +++ b/tests/utilities_for_tests.py @@ -45,13 +45,13 @@ def get_market_caps(): return mcaps -def setup_efficient_frontier(data_only=False): +def setup_efficient_frontier(data_only=False, solver=None, verbose=False): df = get_data() mean_return = expected_returns.mean_historical_return(df) sample_cov_matrix = risk_models.sample_cov(df) if data_only: return mean_return, sample_cov_matrix - return EfficientFrontier(mean_return, sample_cov_matrix) + return EfficientFrontier(mean_return, sample_cov_matrix, solver=solver, verbose=verbose) def setup_cla(data_only=False):