Promote Solver & Verbose to Constructor Options; internals are now private

This commit is contained in:
Pat Newell
2020-08-07 15:50:18 -04:00
parent 19320d229b
commit 53b048ae77
17 changed files with 69 additions and 72 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,14 +121,14 @@ Basic Usage
from pypfopt import base_optimizer
base_optimizer.portfolio_performance(
weights, expected_returns, cov_matrix, risk_free_rate=0.02
weights, expected_returns, cov_matrix, verbose=True, risk_free_rate=0.02
)
.. note::
PyPortfolioOpt defers to cvxpy's default choice of solver. If you would like to explicitly
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>`_.
choose the solver, simply pass the optional ``solver = "ECOS"`` kwarg to the constructor.
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,8 +178,7 @@ 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.verbose = True
ef.portfolio_performance()
ef.portfolio_performance(verbose=True)
.. code-block:: text

View File

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

View File

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

View File

@@ -47,7 +47,6 @@ class BaseOptimizer:
self.tickers = tickers
# Outputs
self.weights = None
self.verbose = False
def _make_output_weights(self, weights=None):
"""
@@ -142,12 +141,16 @@ class BaseConvexOptimizer(BaseOptimizer):
- ``save_weights_to_file()`` saves the weights to csv, json, or txt.
"""
def __init__(self, n_assets, tickers=None, weight_bounds=(0, 1)):
def __init__(self, n_assets, tickers=None, weight_bounds=(0, 1), solver=None, verbose=False):
"""
:param weight_bounds: minimum and maximum weight of each asset OR single min/max pair
if all identical, defaults to (0, 1). Must be changed to (-1, 1)
for portfolios with shorting.
:type weight_bounds: tuple OR tuple list, optional
:param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()`
:type solver: str, optional (see cvxpy.Problem#_solve for default. spoiler: it's ECOS)
:param verbose: whether performance and debugging info should be printed, defaults to False
:type verbose: bool, optional
"""
super().__init__(n_assets, tickers)
@@ -160,7 +163,8 @@ class BaseConvexOptimizer(BaseOptimizer):
self._upper_bounds = None
self._map_bounds_to_constraints(weight_bounds)
self.solver = None
self._solver = solver
self._verbose = verbose
def _map_bounds_to_constraints(self, test_bounds):
"""
@@ -212,10 +216,10 @@ class BaseConvexOptimizer(BaseOptimizer):
try:
opt = cp.Problem(cp.Minimize(self._objective), self._constraints)
if self.solver is not None:
opt.solve(solver=self.solver, verbose=self.verbose)
if self._solver is not None:
opt.solve(solver=self._solver, verbose=self._verbose)
else:
opt.solve(verbose=self.verbose)
opt.solve(verbose=self._verbose)
except (TypeError, cp.DCPError):
raise exceptions.OptimizationError

View File

@@ -451,7 +451,7 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
"""
return self.bl_weights(risk_aversion)
def portfolio_performance(self, risk_free_rate=0.02):
def portfolio_performance(self, risk_free_rate=0.02, verbose=False):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
@@ -461,6 +461,8 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:raises ValueError: if weights have not been calcualted yet
:return: expected return, volatility, Sharpe ratio.
:rtype: (float, float, float)
@@ -471,6 +473,6 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
self.weights,
self.posterior_rets,
self.posterior_cov,
self.verbose,
verbose,
risk_free_rate,
)

View File

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

View File

@@ -53,7 +53,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
- ``save_weights_to_file()`` saves the weights to csv, json, or txt.
"""
def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1), gamma=0):
def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1), gamma=0, solver=None, verbose=False):
"""
:param expected_returns: expected returns for each asset. Can be None if
optimising for volatility only (but not recommended).
@@ -68,6 +68,10 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
:param gamma: L2 regularisation parameter, defaults to 0. Increase if you want more
non-negligible weights
:type gamma: float, optional
:param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()`
:type solver: str
:param verbose: whether performance and debugging info should be printed, defaults to False
:type verbose: bool, optional
:raises TypeError: if ``expected_returns`` is not a series, list or array
:raises TypeError: if ``cov_matrix`` is not a dataframe or array
"""
@@ -89,7 +93,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
if cov_matrix.shape != (len(expected_returns), len(expected_returns)):
raise ValueError("Covariance matrix does not match expected returns")
super().__init__(len(tickers), tickers, weight_bounds)
super().__init__(len(tickers), tickers, weight_bounds, solver=solver, verbose=verbose)
@staticmethod
def _validate_expected_returns(expected_returns):
@@ -333,7 +337,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
return self._solve_cvxpy_opt_problem()
def portfolio_performance(self, risk_free_rate=0.02):
def portfolio_performance(self, risk_free_rate=0.02, verbose=False):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
@@ -342,6 +346,8 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:raises ValueError: if weights have not been calcualted yet
:return: expected return, volatility, Sharpe ratio.
:rtype: (float, float, float)
@@ -350,6 +356,6 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
self.weights,
self.expected_returns,
self.cov_matrix,
self.verbose,
verbose,
risk_free_rate,
)

View File

@@ -167,7 +167,7 @@ class HRPOpt(base_optimizer.BaseOptimizer):
self.set_weights(weights)
return weights
def portfolio_performance(self, risk_free_rate=0.02, frequency=252):
def portfolio_performance(self, risk_free_rate=0.02, frequency=252, verbose=False):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio
@@ -180,7 +180,9 @@ class HRPOpt(base_optimizer.BaseOptimizer):
:param frequency: number of time periods in a year, defaults to 252 (the number
of trading days in a year)
:type frequency: int, optional
:raises ValueError: if weights have not been calcualted yet
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:raises ValueError: if weights have not been calculated yet
:return: expected return, volatility, Sharpe ratio.
:rtype: (float, float, float)
"""
@@ -192,5 +194,5 @@ class HRPOpt(base_optimizer.BaseOptimizer):
mu = self.returns.mean() * frequency
return base_optimizer.portfolio_performance(
self.weights, mu, cov, self.verbose, risk_free_rate
self.weights, mu, cov, verbose, risk_free_rate
)

View File

@@ -208,13 +208,11 @@ def test_save_weights_to_file():
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()
ef.verbose = verbose
ef = setup_efficient_frontier(solver=solver, verbose=verbose)
with patch("cvxpy.Problem.solve") as mock:
with pytest.raises(exceptions.OptimizationError):

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

View File

@@ -66,8 +66,7 @@ def test_min_volatility():
def test_min_volatility_different_solver():
ef = setup_efficient_frontier()
ef.solver = "ECOS"
ef = setup_efficient_frontier(solver="ECOS")
w = ef.min_volatility()
assert isinstance(w, dict)
assert set(w.keys()) == set(ef.tickers)
@@ -76,13 +75,11 @@ def test_min_volatility_different_solver():
test_performance = (0.179312, 0.159151, 1.001015)
np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-5)
ef = setup_efficient_frontier()
ef.solver = "OSQP"
ef = setup_efficient_frontier(solver="OSQP")
w = ef.min_volatility()
np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-5)
ef = setup_efficient_frontier()
ef.solver = "SCS"
ef = setup_efficient_frontier(solver="SCS")
w = ef.min_volatility()
np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-3)

View File

@@ -45,13 +45,13 @@ def get_market_caps():
return mcaps
def setup_efficient_frontier(data_only=False):
def setup_efficient_frontier(data_only=False, solver=None, verbose=False):
df = get_data()
mean_return = expected_returns.mean_historical_return(df)
sample_cov_matrix = risk_models.sample_cov(df)
if data_only:
return mean_return, sample_cov_matrix
return EfficientFrontier(mean_return, sample_cov_matrix)
return EfficientFrontier(mean_return, sample_cov_matrix, solver=solver, verbose=verbose)
def setup_cla(data_only=False):