diff --git a/pypfopt/efficient_frontier/efficient_frontier.py b/pypfopt/efficient_frontier/efficient_frontier.py index 04e9d627..7cc3f432 100644 --- a/pypfopt/efficient_frontier/efficient_frontier.py +++ b/pypfopt/efficient_frontier/efficient_frontier.py @@ -386,14 +386,14 @@ def efficient_risk(self, target_volatility, market_neutral=False): if not isinstance(target_volatility, (float, int)) or target_volatility < 0: raise ValueError("target_volatility should be a positive float") - global_min_volatility = np.sqrt(1 / np.sum(np.linalg.pinv(self.cov_matrix))) - - if target_volatility < global_min_volatility: - raise ValueError( - "The minimum volatility is {:.3f}. Please use a higher target_volatility".format( - global_min_volatility + if not market_neutral: + global_min_volatility = np.sqrt(1 / np.sum(np.linalg.pinv(self.cov_matrix))) + if target_volatility < global_min_volatility: + raise ValueError( + "The minimum volatility is {:.3f}. Please use a higher target_volatility".format( + global_min_volatility + ) ) - ) update_existing_parameter = self.is_parameter_defined("target_variance") if update_existing_parameter: diff --git a/tests/test_market_neutral_risk_regression.py b/tests/test_market_neutral_risk_regression.py new file mode 100644 index 00000000..7ce72dd4 --- /dev/null +++ b/tests/test_market_neutral_risk_regression.py @@ -0,0 +1,37 @@ +import numpy as np +import pytest + +from pypfopt import EfficientFrontier, exceptions + + +@pytest.mark.parametrize("target", [0.01, 0.05, 0.1]) +def test_market_neutral_risk_below_fully_invested_minimum(target): + # With diagonal 0.04 covariance and weights (a, -a), risk=sqrt(0.08)*a. + ef = EfficientFrontier([0.1, 0.0], np.eye(2) * 0.04, weight_bounds=(-1, 1)) + weights = np.array(list(ef.efficient_risk(target, market_neutral=True).values())) + a = target / np.sqrt(0.08) + np.testing.assert_allclose(weights, [a, -a], atol=2e-5) + + +def test_market_neutral_risk_updates_parameter(): + ef = EfficientFrontier([0.1, 0.0], np.eye(2) * 0.04, weight_bounds=(-1, 1)) + for target in (0.05, 0.02): + weights = np.array( + list(ef.efficient_risk(target, market_neutral=True).values()) + ) + np.testing.assert_allclose( + weights, [target / np.sqrt(0.08), -target / np.sqrt(0.08)], atol=2e-5 + ) + + +def test_fully_invested_low_risk_is_still_rejected(): + ef = EfficientFrontier([0.1, 0.0], np.eye(2) * 0.04) + with pytest.raises(ValueError, match="minimum volatility"): + ef.efficient_risk(0.05) + + +def test_market_neutral_infeasible_extra_constraint_is_not_accepted(): + ef = EfficientFrontier([0.1, 0.0], np.eye(2) * 0.04, weight_bounds=(-1, 1)) + ef.add_constraint(lambda w: w[0] >= 0.5) + with pytest.raises(exceptions.OptimizationError): + ef.efficient_risk(0.05, market_neutral=True)