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();"
]
},
{
@@ -1562,4 +1566,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
}
}

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();"
]
},
{
@@ -1165,4 +1167,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
}
}

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();"
]
},
{
@@ -85,4 +86,4 @@
},
"nbformat": 4,
"nbformat_minor": 2
}
}

View File

@@ -26,7 +26,7 @@ the main difficulty is inputting our specific problem into a solver.
PyPortfolioOpt aims to do the hard work for you, allowing for one-liners like ``ef.min_volatility()``
to generate a portfolio that minimises the volatility, while at the same time allowing for more
complex problems to be built up from modular units. This is all possible thanks to
complex problems to be built up from modular units. This is all possible thanks to
`cvxpy <https://www.cvxpy.org/>`_, the *fantastic* python-embedded modelling
language for convex optimisation upon which PyPortfolioOpt's efficient frontier functionality lies.
@@ -47,7 +47,7 @@ the optimisation objective, and the optimisation constraints. For example, the c
optimisation problem is to **minimise risk** subject to a **return constraint** (i.e the portfolio
must return more than a certain amount). From an implementation perspective, however, there is
not much difference between an objective and a constraint. Consider a similar problem, which is to
**maximize return** subject to a **risk constraint** -- now, the role of risk and return have swapped.
**maximize return** subject to a **risk constraint** -- now, the role of risk and return have swapped.
To that end, PyPortfolioOpt defines an :py:mod:`objective_functions` module that contains objective functions
(which can also act as constraints, as we have just seen). The actual optimisation occurs in the :py:class:`efficient_frontier.EfficientFrontier` class.
@@ -61,7 +61,7 @@ For example, adding a regularisation objective (explained below) to a minimum vo
ef.min_volatility() # find the portfolio that minimises volatility and L2_reg
.. tip::
If you would like to plot the efficient frontier, take a look at the :ref:`plotting` module.
Basic Usage
@@ -90,7 +90,7 @@ Basic Usage
.. caution::
Because ``max_sharpe()`` makes a variable substitution, additional objectives may
not work as intended.
not work as intended.
.. automethod:: max_quadratic_utility
@@ -116,19 +116,19 @@ Basic Usage
.. tip::
If you would like to use the ``portfolio_performance`` function independently of any
optimiser (e.g for debugging purposes), you can use::
optimiser (e.g for debugging purposes), you can use::
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::
.. 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
=================================
@@ -213,7 +213,7 @@ different API. For examples, check out this `cookbook recipe <https://github.com
.. class:: pypfopt.base_optimizer.BaseConvexOptimizer
.. automethod:: convex_objective
.. automethod:: nonconvex_objective

View File

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

@@ -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 <https://docker-curriculum.com/#introduction>`_.
@@ -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:
@@ -159,7 +160,7 @@ Contents
.. toctree::
:caption: Other information
Roadmap
Contributing
About

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