From db8304fa6dd2cc686bcf11bb77571e26e0f7e88d Mon Sep 17 00:00:00 2001 From: Enzo Busseti Date: Tue, 9 Jan 2024 12:49:01 +0400 Subject: [PATCH] minor (unrelated) fix to worstcaserisk, tests pass and full coverage of new code --- Makefile | 2 +- cvxportfolio/costs.py | 6 +++++- cvxportfolio/estimator.py | 4 ++++ cvxportfolio/risks.py | 20 ++++++++++++++++---- cvxportfolio/tests/test_simulator.py | 22 ++++++++++++++++++++-- 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 0fd25df87..427883a9a 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ test: ## run tests w/ cov report # $(BINDIR)/bandit $(PROJECT)/*.py $(TESTS)/*.py lint: ## run linter - $(BINDIR)/pylint $(PROJECT) $(EXTRA_SCRIPTS) $(EXAMPLES) + $(BINDIR)/pylint $(PROJECT) # $(EXTRA_SCRIPTS) $(EXAMPLES) $(BINDIR)/diff-quality --violations=pylint --config-file pyproject.toml docs: ## build docs diff --git a/cvxportfolio/costs.py b/cvxportfolio/costs.py index 0bee6ca77..d5cab8c2f 100644 --- a/cvxportfolio/costs.py +++ b/cvxportfolio/costs.py @@ -128,6 +128,8 @@ def __init__(self, costs, multipliers): "You can only sum cost instances to other cost instances.") self.costs = costs self.multipliers = multipliers + # this is changed by WorstCaseRisk before compiling to Cvxpy + self.DO_CONVEXITY_CHECK = True def __add__(self, other): """Add other (combined) cost to self.""" @@ -194,8 +196,10 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm): cost.compile_to_cvxpy(w_plus, z, w_plus_minus_w_bm) if not add.is_dcp(): raise ConvexSpecificationError(cost * multiplier) - if not add.is_concave(): + if self.DO_CONVEXITY_CHECK and (not add.is_concave()): raise ConvexityError(cost * multiplier) + if (not self.DO_CONVEXITY_CHECK) and add.is_concave(): + raise ConvexityError(-cost * multiplier) expression += add return expression diff --git a/cvxportfolio/estimator.py b/cvxportfolio/estimator.py index b25c10811..71fe45cb5 100644 --- a/cvxportfolio/estimator.py +++ b/cvxportfolio/estimator.py @@ -85,6 +85,8 @@ def finalize_estimator(self, **kwargs): We aren't currently using in the rest of the library but we plan to move the caching logic in it. + .. versionadded:: 1.1.0 + :param kwargs: Reserved for future expansion. :type kwargs: dict """ @@ -95,6 +97,8 @@ def finalize_estimator(self, **kwargs): def finalize_estimator_recursive(self, **kwargs): """Recursively finalize all estimators in a policy. + .. versionadded:: 1.1.0 + :param kwargs: Parameters sent down an estimator tree to finalize it. :type kwargs: dict """ diff --git a/cvxportfolio/risks.py b/cvxportfolio/risks.py index 2bd47fa02..8bde56a30 100644 --- a/cvxportfolio/risks.py +++ b/cvxportfolio/risks.py @@ -399,13 +399,17 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm): class WorstCaseRisk(Cost): """Select the most restrictive risk model for each value of the allocation. - vector. - Given a list of risk models, penalize the portfolio allocation by the one with highest risk value at the solution point. If uncertain about which risk model to use this procedure can be an easy solution. + :Example: + + >>> risk_model = cvx.WorstCaseRisk( + [cvx.FullCovariance(), + cvx.DiagonalCovariance() + 0.25 * cvx.RiskForecastError()]) + :param riskmodels: risk model instances on which to compute the worst-case risk. :type riskmodels: list @@ -446,8 +450,16 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm): :returns: Cvxpy expression representing the risk model. :rtype: cvxpy.expression """ - risks = [risk.compile_to_cvxpy(w_plus, z, w_plus_minus_w_bm) - for risk in self.riskmodels] + risks = [] + for risk in self.riskmodels: + # this is needed if user provides individual risk terms + # that are composed objects (CombinedCost) + # it will check concavity instead of convexity + risk.DO_CONVEXITY_CHECK = False + risks.append(risk.compile_to_cvxpy(w_plus, z, w_plus_minus_w_bm)) + # we also change it back in case the user is sharing the instance + risk.DO_CONVEXITY_CHECK = True + return cp.max(cp.hstack(risks)) def finalize_estimator_recursive(self, **kwargs): diff --git a/cvxportfolio/tests/test_simulator.py b/cvxportfolio/tests/test_simulator.py index e53cfba7c..c1ae7f3d8 100644 --- a/cvxportfolio/tests/test_simulator.py +++ b/cvxportfolio/tests/test_simulator.py @@ -433,9 +433,11 @@ def test_backtest(self): """Test simple back-test.""" pol = cvx.SinglePeriodOptimization( cvx.ReturnsForecast() - cvx.ReturnsForecastError() - - .5 * cvx.FullCovariance(), + - .5 * cvx.WorstCaseRisk( + [cvx.FullCovariance(), + cvx.DiagonalCovariance() + .25 * cvx.DiagonalCovariance()]), [cvx.LeverageLimit(1)], verbose=True, - solver=self.default_qp_solver) + solver=self.default_socp_solver) sim = cvx.MarketSimulator( market_data=self.market_data_4, base_location=self.datadir) result = sim.backtest(pol, pd.Timestamp( @@ -443,6 +445,22 @@ def test_backtest(self): print(result) + def test_wrong_worstcase(self): + """Test wrong worst-case convexity.""" + pol = cvx.SinglePeriodOptimization( + cvx.ReturnsForecast() - cvx.ReturnsForecastError() + - .5 * cvx.WorstCaseRisk( + [-cvx.FullCovariance(), + cvx.DiagonalCovariance() + .25 * cvx.DiagonalCovariance()]), + [cvx.LeverageLimit(1)], verbose=True, + solver=self.default_socp_solver) + sim = cvx.MarketSimulator( + market_data=self.market_data_4, base_location=self.datadir) + + with self.assertRaises(ConvexityError): + sim.backtest(pol, pd.Timestamp( + '2023-01-01'), pd.Timestamp('2023-04-20')) + def test_backtest_changing_universe(self): """Test back-test with changing universe.""" sim = cvx.MarketSimulator(