Make Verbose Option a Hidden API

This commit is contained in:
Pat Newell
2020-08-01 06:59:02 -04:00
parent 38a61643e2
commit 19320d229b
15 changed files with 76 additions and 72 deletions

View File

@@ -132,7 +132,8 @@ raw_weights = ef.max_sharpe()
cleaned_weights = ef.clean_weights()
ef.save_weights_to_file("weights.csv") # saves to file
print(cleaned_weights)
ef.portfolio_performance(verbose=True)
ef.verbose = True
ef.portfolio_performance()
```
This outputs the following weights:

View File

@@ -930,7 +930,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{
@@ -1255,7 +1256,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{
@@ -1417,7 +1419,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{
@@ -1491,7 +1494,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{

View File

@@ -556,7 +556,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{
@@ -685,7 +686,8 @@
}
],
"source": [
"ef.portfolio_performance(verbose=True);"
"ef.verbose = True\n",
"ef.portfolio_performance();"
]
},
{

View File

@@ -32,7 +32,8 @@
"returns = risk_models.returns_from_prices(df)\n",
"hrp = HRPOpt(returns)\n",
"weights = hrp.optimize()\n",
"hrp.portfolio_performance(verbose=True);"
"hrp.verbose = True\n",
"hrp.portfolio_performance();"
]
},
{

View File

@@ -121,14 +121,14 @@ Basic Usage
from pypfopt import base_optimizer
base_optimizer.portfolio_performance(
weights, expected_returns, cov_matrix, verbose=True, risk_free_rate=0.02
weights, expected_returns, cov_matrix, 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 <https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver>`_.
choose the solver, simply assign ``ef.solver = "ECOS"`` prior to calling the actual optimisation
method. You can choose from any of the `supported solvers <https://www.cvxpy.org/tutorial/advanced/index.html#choosing-a-solver>`_.
Adding objectives and constraints
=================================

View File

@@ -178,7 +178,8 @@ 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.portfolio_performance(verbose=True)
ef.verbose = True
ef.portfolio_performance()
.. code-block:: text

View File

@@ -131,7 +131,8 @@ that's fine too::
# Optimise for maximal Sharpe ratio
ef = EfficientFrontier(mu, S)
weights = ef.max_sharpe()
ef.portfolio_performance(verbose=True)
ef.verbose = True
ef.portfolio_performance()
This outputs the following:

View File

@@ -28,7 +28,8 @@ def deviation_risk_parity(w, cov_matrix):
ef = EfficientFrontier(mu, S)
weights = ef.nonconvex_objective(deviation_risk_parity, ef.cov_matrix)
ef.portfolio_performance(verbose=True)
ef.verbose = True
ef.portfolio_performance()
"""
Expected annual return: 22.9%
@@ -82,7 +83,8 @@ rets = bl.bl_returns()
ef = EfficientFrontier(rets, S)
ef.max_sharpe()
print(ef.clean_weights())
ef.portfolio_performance(verbose=True)
ef.verbose = True
ef.portfolio_performance()
"""
{'GOOG': 0.2015,
@@ -115,7 +117,8 @@ Sharpe Ratio: 0.46
# Hierarchical risk parity
hrp = HRPOpt(returns)
weights = hrp.optimize()
hrp.portfolio_performance(verbose=True)
hrp.verbose = True
hrp.portfolio_performance()
print(weights)
plotting.plot_dendrogram(hrp) # to plot dendrogram
@@ -150,7 +153,8 @@ Sharpe Ratio: 0.66
# Crticial Line Algorithm
cla = CLA(mu, S)
print(cla.max_sharpe())
cla.portfolio_performance(verbose=True)
cla.verbose = True
cla.portfolio_performance()
plotting.plot_efficient_frontier(cla) # to plot
"""

View File

@@ -47,6 +47,7 @@ class BaseOptimizer:
self.tickers = tickers
# Outputs
self.weights = None
self.verbose = False
def _make_output_weights(self, weights=None):
"""
@@ -201,22 +202,20 @@ 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, verbose=False):
def _solve_cvxpy_opt_problem(self):
"""
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=verbose)
opt.solve(solver=self.solver, verbose=self.verbose)
else:
opt.solve(verbose=verbose)
opt.solve(verbose=self.verbose)
except (TypeError, cp.DCPError):
raise exceptions.OptimizationError
@@ -299,7 +298,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, verbose=False, **kwargs):
def convex_objective(self, custom_objective, weights_sum_to_one=True, **kwargs):
"""
Optimise a custom convex objective function. Constraints should be added with
``ef.add_constraint()``. Optimiser arguments must be passed as keyword-args. Example::
@@ -316,8 +315,6 @@ 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
@@ -331,7 +328,7 @@ class BaseConvexOptimizer(BaseOptimizer):
if weights_sum_to_one:
self._constraints.append(cp.sum(self._w) == 1)
return self._solve_cvxpy_opt_problem(verbose=verbose)
return self._solve_cvxpy_opt_problem()
def nonconvex_objective(
self,

View File

@@ -451,14 +451,12 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
"""
return self.bl_weights(risk_aversion)
def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, risk_free_rate=0.02):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
This method uses the BL posterior returns and covariance matrix.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
The period of the risk-free rate should correspond to the
frequency of expected returns.
@@ -473,6 +471,6 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
self.weights,
self.posterior_rets,
self.posterior_cov,
verbose,
self.verbose,
risk_free_rate,
)

View File

@@ -445,13 +445,11 @@ 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, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, risk_free_rate=0.02):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02
:type risk_free_rate: float, optional
:raises ValueError: if weights have not been calculated yet
@@ -462,6 +460,6 @@ class CLA(base_optimizer.BaseOptimizer):
self.weights,
self.expected_returns,
self.cov_matrix,
verbose,
self.verbose,
risk_free_rate,
)

View File

@@ -134,7 +134,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
del self._constraints[0]
del self._constraints[0]
def min_volatility(self, verbose=False):
def min_volatility(self):
"""
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(verbose=verbose)
return self._solve_cvxpy_opt_problem()
def max_sharpe(self, risk_free_rate=0.02, verbose=False):
def max_sharpe(self, risk_free_rate=0.02):
"""
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(verbose=verbose)
self._solve_cvxpy_opt_problem()
# 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, verbose=False):
def max_quadratic_utility(self, risk_aversion=1, market_neutral=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(verbose=verbose)
return self._solve_cvxpy_opt_problem()
def efficient_risk(self, target_volatility, market_neutral=False, verbose=False):
def efficient_risk(self, target_volatility, market_neutral=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(verbose=verbose)
return self._solve_cvxpy_opt_problem()
def efficient_return(self, target_return, market_neutral=False, verbose=False):
def efficient_return(self, target_return, market_neutral=False):
"""
Calculate the 'Markowitz portfolio', minimising volatility for a given target return.
@@ -331,15 +331,13 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
else:
self._constraints.append(cp.sum(self._w) == 1)
return self._solve_cvxpy_opt_problem(verbose=verbose)
return self._solve_cvxpy_opt_problem()
def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
def portfolio_performance(self, risk_free_rate=0.02):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
The period of the risk-free rate should correspond to the
frequency of expected returns.
@@ -352,6 +350,6 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
self.weights,
self.expected_returns,
self.cov_matrix,
verbose,
self.verbose,
risk_free_rate,
)

View File

@@ -167,14 +167,12 @@ class HRPOpt(base_optimizer.BaseOptimizer):
self.set_weights(weights)
return weights
def portfolio_performance(self, verbose=False, risk_free_rate=0.02, frequency=252):
def portfolio_performance(self, risk_free_rate=0.02, frequency=252):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio
assuming returns are daily
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
The period of the risk-free rate should correspond to the
frequency of expected returns.
@@ -194,5 +192,5 @@ class HRPOpt(base_optimizer.BaseOptimizer):
mu = self.returns.mean() * frequency
return base_optimizer.portfolio_performance(
self.weights, mu, cov, verbose, risk_free_rate
self.weights, mu, cov, self.verbose, risk_free_rate
)

View File

@@ -214,6 +214,7 @@ def assert_verbose_option(optimize_for_method, *args, solver=None):
# 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
with patch("cvxpy.Problem.solve") as mock:
with pytest.raises(exceptions.OptimizationError):
@@ -221,7 +222,7 @@ def assert_verbose_option(optimize_for_method, *args, solver=None):
# 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)
optimize_for_method(ef, *args)
# 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

View File

@@ -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(verbose=False), 0.017070218149194846)
np.testing.assert_almost_equal(da._allocation_rmse_error(), 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(verbose=False)
rmse1 = da1._allocation_rmse_error()
da2 = DiscreteAllocation(w, latest_prices, total_portfolio_value=100000)
da2.greedy_portfolio()
rmse2 = da2._allocation_rmse_error(verbose=False)
rmse2 = da2._allocation_rmse_error()
assert rmse2 < rmse1
da3 = DiscreteAllocation(w, latest_prices, total_portfolio_value=10000)
da3.lp_portfolio()
rmse3 = da3._allocation_rmse_error(verbose=False)
rmse3 = da3._allocation_rmse_error()
da4 = DiscreteAllocation(w, latest_prices, total_portfolio_value=30000)
da4.lp_portfolio()
rmse4 = da4._allocation_rmse_error(verbose=False)
rmse4 = da4._allocation_rmse_error()
assert rmse4 < rmse3