Allow Verbose as an Option to cvxpy.Problem#solve

This commit is contained in:
Pat Newell
2020-07-29 16:49:16 -04:00
parent 386d72da52
commit 38a61643e2
3 changed files with 75 additions and 15 deletions

View File

@@ -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,

View File

@@ -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):
"""

View File

@@ -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)