Skip to content

Commit

Permalink
minor (unrelated) fix to worstcaserisk, tests pass and full coverage …
Browse files Browse the repository at this point in the history
…of new code
  • Loading branch information
enzbus committed Jan 9, 2024
1 parent e84b08d commit db8304f
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion cvxportfolio/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions cvxportfolio/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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
"""
Expand Down
20 changes: 16 additions & 4 deletions cvxportfolio/risks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
22 changes: 20 additions & 2 deletions cvxportfolio/tests/test_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,16 +433,34 @@ 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(
'2023-01-01'), pd.Timestamp('2023-04-20'))

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(
Expand Down

0 comments on commit db8304f

Please sign in to comment.