diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index bd4566e..1ea69b3 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -201,22 +201,25 @@ class BaseConvexOptimizer(BaseOptimizer): self._constraints.append(self._w >= self._lower_bounds) self._constraints.append(self._w <= self._upper_bounds) - def _solve_cvxpy_opt_problem(self): + def _solve_cvxpy_opt_problem(self, verbose=False): """ Helper method to solve the cvxpy problem and check output, once objectives and constraints have been defined + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional :raises exceptions.OptimizationError: if problem is not solvable by cvxpy """ try: opt = cp.Problem(cp.Minimize(self._objective), self._constraints) if self.solver is not None: - opt.solve(solver=self.solver, verbose=True) + opt.solve(solver=self.solver, verbose=verbose) else: - opt.solve() + opt.solve(verbose=verbose) except (TypeError, cp.DCPError): raise exceptions.OptimizationError + if opt.status != "optimal": raise exceptions.OptimizationError self.weights = self._w.value.round(16) + 0.0 # +0.0 removes signed zero @@ -296,7 +299,7 @@ class BaseConvexOptimizer(BaseOptimizer): is_sector = [sector_mapper[t] == sector for t in self.tickers] self._constraints.append(cp.sum(self._w[is_sector]) >= sector_lower[sector]) - def convex_objective(self, custom_objective, weights_sum_to_one=True, **kwargs): + def convex_objective(self, custom_objective, weights_sum_to_one=True, verbose=False, **kwargs): """ Optimise a custom convex objective function. Constraints should be added with ``ef.add_constraint()``. Optimiser arguments must be passed as keyword-args. Example:: @@ -313,6 +316,8 @@ class BaseConvexOptimizer(BaseOptimizer): :type custom_objective: function with signature (cp.Variable, `**kwargs`) -> cp.Expression :param weights_sum_to_one: whether to add the default objective, defaults to True :type weights_sum_to_one: bool, optional + :param verbose: whether performance should be printed, defaults to False + :type verbose: bool, optional :raises OptimizationError: if the objective is nonconvex or constraints nonlinear. :return: asset weights for the efficient risk portfolio :rtype: OrderedDict @@ -326,7 +331,7 @@ class BaseConvexOptimizer(BaseOptimizer): if weights_sum_to_one: self._constraints.append(cp.sum(self._w) == 1) - return self._solve_cvxpy_opt_problem() + return self._solve_cvxpy_opt_problem(verbose=verbose) def nonconvex_objective( self, diff --git a/pypfopt/efficient_frontier.py b/pypfopt/efficient_frontier.py index a943d38..97f2f68 100644 --- a/pypfopt/efficient_frontier.py +++ b/pypfopt/efficient_frontier.py @@ -134,7 +134,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): del self._constraints[0] del self._constraints[0] - def min_volatility(self): + def min_volatility(self, verbose=False): """ Minimise volatility. @@ -149,9 +149,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): self._constraints.append(cp.sum(self._w) == 1) - return self._solve_cvxpy_opt_problem() + return self._solve_cvxpy_opt_problem(verbose=verbose) - def max_sharpe(self, risk_free_rate=0.02): + def max_sharpe(self, risk_free_rate=0.02, verbose=False): """ Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, as it is the portfolio for which the capital market line is tangent to the efficient frontier. @@ -209,12 +209,12 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): k >= 0, ] + new_constraints - self._solve_cvxpy_opt_problem() + self._solve_cvxpy_opt_problem(verbose=verbose) # Inverse-transform self.weights = (self._w.value / k.value).round(16) + 0.0 return self._make_output_weights() - def max_quadratic_utility(self, risk_aversion=1, market_neutral=False): + def max_quadratic_utility(self, risk_aversion=1, market_neutral=False, verbose=False): r""" Maximise the given quadratic utility, i.e: @@ -246,9 +246,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): else: self._constraints.append(cp.sum(self._w) == 1) - return self._solve_cvxpy_opt_problem() + return self._solve_cvxpy_opt_problem(verbose=verbose) - def efficient_risk(self, target_volatility, market_neutral=False): + def efficient_risk(self, target_volatility, market_neutral=False, verbose=False): """ Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal). @@ -285,9 +285,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): else: self._constraints.append(cp.sum(self._w) == 1) - return self._solve_cvxpy_opt_problem() + return self._solve_cvxpy_opt_problem(verbose=verbose) - def efficient_return(self, target_return, market_neutral=False): + def efficient_return(self, target_return, market_neutral=False, verbose=False): """ Calculate the 'Markowitz portfolio', minimising volatility for a given target return. @@ -331,7 +331,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer): else: self._constraints.append(cp.sum(self._w) == 1) - return self._solve_cvxpy_opt_problem() + return self._solve_cvxpy_opt_problem(verbose=verbose) def portfolio_performance(self, verbose=False, risk_free_rate=0.02): """ diff --git a/tests/test_base_optimizer.py b/tests/test_base_optimizer.py index 7be110e..085daf6 100644 --- a/tests/test_base_optimizer.py +++ b/tests/test_base_optimizer.py @@ -1,7 +1,10 @@ import json import os +import cvxpy import numpy as np import pytest +from random import random +from mock import patch, Mock from pypfopt import EfficientFrontier from pypfopt import exceptions from tests.utilities_for_tests import get_data, setup_efficient_frontier @@ -203,3 +206,55 @@ def test_save_weights_to_file(): os.remove("tests/test.txt") 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() + + with patch("cvxpy.Problem.solve") as mock: + with pytest.raises(exceptions.OptimizationError): + # we're not testing the behavior of ef.min_volatility, just that it + # passes the verbose kwarg on to Problem#solve. + # mocking Problem#solve causes EfficientFrontier#min_volatility to + # raise an error, but it is safe to ignore it + optimize_for_method(ef, *args, verbose=verbose) + + # mock.assert_called_with(verbose=verbose) doesn't work here because + # sometimes the mock is called with more kwargs. all we want to know is + # whether the value of verbose is passed on to Problem#solve + _name, _args, kwargs = mock.mock_calls[0] + assert kwargs['verbose'] == verbose + +def test_min_volatility_verbose_option(): + assert_verbose_option(EfficientFrontier.min_volatility) + +def test_min_volatility_verbose_option_with_solver(): + assert_verbose_option(EfficientFrontier.min_volatility, solver=cvxpy.settings.ECOS) + +def test_max_sharpe_verbose_option(): + assert_verbose_option(EfficientFrontier.max_sharpe) + +def test_max_sharpe_verbose_option_with_solver(): + assert_verbose_option(EfficientFrontier.min_volatility, solver=cvxpy.settings.ECOS) + +def test_max_quadratic_utility_verbose_option(): + assert_verbose_option(EfficientFrontier.max_quadratic_utility) + +def test_max_quadratic_utility_verbose_option_with_solver(): + assert_verbose_option(EfficientFrontier.min_volatility, solver=cvxpy.settings.ECOS) + +def test_efficient_risk_verbose_option(): + assert_verbose_option(EfficientFrontier.efficient_risk, 0.1) + +def test_efficient_risk_verbose_option_with_solver(): + assert_verbose_option(EfficientFrontier.min_volatility, solver=cvxpy.settings.ECOS) + +def test_efficient_return_verbose_option(): + assert_verbose_option(EfficientFrontier.efficient_return, 0.01) + +def test_efficient_return_verbose_option_with_solver(): + assert_verbose_option(EfficientFrontier.min_volatility, solver=cvxpy.settings.ECOS)