mirror of
https://github.com/robertmartin8/PyPortfolioOpt.git
synced 2022-11-27 18:02:41 +03:00
Allow Verbose as an Option to cvxpy.Problem#solve
This commit is contained in:
@@ -201,22 +201,25 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
self._constraints.append(self._w >= self._lower_bounds)
|
self._constraints.append(self._w >= self._lower_bounds)
|
||||||
self._constraints.append(self._w <= self._upper_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,
|
Helper method to solve the cvxpy problem and check output,
|
||||||
once objectives and constraints have been defined
|
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
|
:raises exceptions.OptimizationError: if problem is not solvable by cvxpy
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
|
||||||
|
|
||||||
if self.solver is not None:
|
if self.solver is not None:
|
||||||
opt.solve(solver=self.solver, verbose=True)
|
opt.solve(solver=self.solver, verbose=verbose)
|
||||||
else:
|
else:
|
||||||
opt.solve()
|
opt.solve(verbose=verbose)
|
||||||
except (TypeError, cp.DCPError):
|
except (TypeError, cp.DCPError):
|
||||||
raise exceptions.OptimizationError
|
raise exceptions.OptimizationError
|
||||||
|
|
||||||
if opt.status != "optimal":
|
if opt.status != "optimal":
|
||||||
raise exceptions.OptimizationError
|
raise exceptions.OptimizationError
|
||||||
self.weights = self._w.value.round(16) + 0.0 # +0.0 removes signed zero
|
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]
|
is_sector = [sector_mapper[t] == sector for t in self.tickers]
|
||||||
self._constraints.append(cp.sum(self._w[is_sector]) >= sector_lower[sector])
|
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
|
Optimise a custom convex objective function. Constraints should be added with
|
||||||
``ef.add_constraint()``. Optimiser arguments must be passed as keyword-args. Example::
|
``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
|
: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
|
:param weights_sum_to_one: whether to add the default objective, defaults to True
|
||||||
:type weights_sum_to_one: bool, optional
|
: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.
|
:raises OptimizationError: if the objective is nonconvex or constraints nonlinear.
|
||||||
:return: asset weights for the efficient risk portfolio
|
:return: asset weights for the efficient risk portfolio
|
||||||
:rtype: OrderedDict
|
:rtype: OrderedDict
|
||||||
@@ -326,7 +331,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
|||||||
if weights_sum_to_one:
|
if weights_sum_to_one:
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
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(
|
def nonconvex_objective(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
del self._constraints[0]
|
del self._constraints[0]
|
||||||
del self._constraints[0]
|
del self._constraints[0]
|
||||||
|
|
||||||
def min_volatility(self):
|
def min_volatility(self, verbose=False):
|
||||||
"""
|
"""
|
||||||
Minimise volatility.
|
Minimise volatility.
|
||||||
|
|
||||||
@@ -149,9 +149,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
|
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
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,
|
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.
|
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,
|
k >= 0,
|
||||||
] + new_constraints
|
] + new_constraints
|
||||||
|
|
||||||
self._solve_cvxpy_opt_problem()
|
self._solve_cvxpy_opt_problem(verbose=verbose)
|
||||||
# Inverse-transform
|
# Inverse-transform
|
||||||
self.weights = (self._w.value / k.value).round(16) + 0.0
|
self.weights = (self._w.value / k.value).round(16) + 0.0
|
||||||
return self._make_output_weights()
|
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"""
|
r"""
|
||||||
Maximise the given quadratic utility, i.e:
|
Maximise the given quadratic utility, i.e:
|
||||||
|
|
||||||
@@ -246,9 +246,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
else:
|
else:
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
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
|
Maximise return for a target risk. The resulting portfolio will have a volatility
|
||||||
less than the target (but not guaranteed to be equal).
|
less than the target (but not guaranteed to be equal).
|
||||||
@@ -285,9 +285,9 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
else:
|
else:
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
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.
|
Calculate the 'Markowitz portfolio', minimising volatility for a given target return.
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
|||||||
else:
|
else:
|
||||||
self._constraints.append(cp.sum(self._w) == 1)
|
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):
|
def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import cvxpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
from random import random
|
||||||
|
from mock import patch, Mock
|
||||||
from pypfopt import EfficientFrontier
|
from pypfopt import EfficientFrontier
|
||||||
from pypfopt import exceptions
|
from pypfopt import exceptions
|
||||||
from tests.utilities_for_tests import get_data, setup_efficient_frontier
|
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.txt")
|
||||||
os.remove("tests/test.json")
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user