"""Unit tests for scenario robustness.py — RRS computation logic.""" import pytest from libs.backtest.domain import MetricsBundle from libs.backtest.scenarios.robustness import ( RegimeRobustnessReport, ScenarioResult, compute_rrs, ) def _make_result( name: str, sharpe: float = 1.0, total_return: float = 10.0, max_dd: float = 10.0, win_rate: float = 0.55, profit_factor: float = 1.5, trade_count: int = 20, ) -> ScenarioResult: """Helper: construct a minimal ScenarioResult.""" m = MetricsBundle( trade_count=trade_count, win_rate=win_rate, avg_win_pct=2.0, avg_loss_pct=-1.0, profit_factor=profit_factor, total_return_pct=total_return, max_drawdown_pct=max_dd, sharpe_ratio=sharpe, ) return ScenarioResult( scenario_name=name, metrics=m, trade_count=trade_count, sharpe_ratio=sharpe, total_return_pct=total_return, max_drawdown_pct=max_dd, win_rate=win_rate, profit_factor=profit_factor, ) @pytest.mark.unit class TestComputeRRS: def test_empty_results_returns_zeros(self): rrs, si, br, dr, rt, st = compute_rrs({}) assert rrs == 0.0 assert si == 0.0 assert br == 0.0 def test_signal_integrity_perfect_when_no_signal_sharpe_zero(self): results = { "no_signal": _make_result("no_signal", sharpe=0.0), "strong_signal": _make_result("strong_signal", sharpe=1.5), } _, si, _, _, _, _ = compute_rrs(results) assert si == 100.0 def test_signal_integrity_perfect_when_no_signal_sharpe_negative(self): results = { "no_signal": _make_result("no_signal", sharpe=-0.5), "strong_signal": _make_result("strong_signal", sharpe=1.5), } _, si, _, _, _, _ = compute_rrs(results) assert si == 100.0 def test_signal_integrity_zero_when_no_signal_equals_strong_signal(self): results = { "no_signal": _make_result("no_signal", sharpe=1.5), "strong_signal": _make_result("strong_signal", sharpe=1.5), } _, si, _, _, _, _ = compute_rrs(results) assert si == pytest.approx(0.0, abs=1.0) def test_signal_integrity_neutral_when_no_signal_scenario_missing(self): results = {"steady_bull": _make_result("steady_bull", sharpe=1.0)} _, si, _, _, _, _ = compute_rrs(results) assert si == 50.0 def test_breadth_all_positive(self): results = { "a": _make_result("a", sharpe=0.5), "b": _make_result("b", sharpe=1.0), "c": _make_result("c", sharpe=0.1), } _, _, br, _, _, _ = compute_rrs(results) assert br == pytest.approx(100.0) def test_breadth_all_negative(self): results = { "a": _make_result("a", sharpe=-0.5), "b": _make_result("b", sharpe=-1.0), } _, _, br, _, _, _ = compute_rrs(results) assert br == pytest.approx(0.0) def test_breadth_half_positive(self): results = { "a": _make_result("a", sharpe=1.0), "b": _make_result("b", sharpe=-1.0), } _, _, br, _, _, _ = compute_rrs(results) assert br == pytest.approx(50.0) def test_drawdown_resilience_zero_dd_gives_100(self): results = {"a": _make_result("a", max_dd=0.0)} _, _, _, dr, _, _ = compute_rrs(results) assert dr == pytest.approx(100.0) def test_drawdown_resilience_50pct_dd_gives_0(self): results = {"a": _make_result("a", max_dd=50.0)} _, _, _, dr, _, _ = compute_rrs(results) assert dr == pytest.approx(0.0) def test_drawdown_resilience_clamped_not_negative(self): results = {"a": _make_result("a", max_dd=80.0)} _, _, _, dr, _, _ = compute_rrs(results) assert dr >= 0.0 def test_regime_transition_neutral_when_missing(self): results = {"steady_bull": _make_result("steady_bull", sharpe=1.0)} _, _, _, _, rt, _ = compute_rrs(results) assert rt == 50.0 def test_regime_transition_good_when_above_median(self): results = { "a": _make_result("a", sharpe=0.5), "b": _make_result("b", sharpe=0.5), "regime_switch": _make_result("regime_switch", sharpe=2.0), } _, _, _, _, rt, _ = compute_rrs(results) # regime_switch > median → should be > 50 assert rt > 50.0 def test_stability_perfect_when_all_same_sharpe(self): results = { "a": _make_result("a", sharpe=1.0), "b": _make_result("b", sharpe=1.0), "c": _make_result("c", sharpe=1.0), } _, _, _, _, _, st = compute_rrs(results) assert st == pytest.approx(100.0) def test_stability_lower_when_high_variance(self): low_var = { "a": _make_result("a", sharpe=1.0), "b": _make_result("b", sharpe=1.1), "c": _make_result("c", sharpe=0.9), } high_var = { "a": _make_result("a", sharpe=3.0), "b": _make_result("b", sharpe=-0.5), "c": _make_result("c", sharpe=0.1), } _, _, _, _, _, st_low = compute_rrs(low_var) _, _, _, _, _, st_high = compute_rrs(high_var) assert st_low > st_high def test_rrs_weighted_sum(self): """RRS = 0.25*si + 0.25*br + 0.20*dr + 0.15*rt + 0.15*st.""" results = { "no_signal": _make_result("no_signal", sharpe=-0.1), "strong_signal": _make_result("strong_signal", sharpe=1.5), "steady_bull": _make_result("steady_bull", sharpe=1.2), "steady_bear": _make_result("steady_bear", sharpe=0.3), "regime_switch": _make_result("regime_switch", sharpe=0.8), } rrs, si, br, dr, rt, st = compute_rrs(results) expected = 0.25 * si + 0.25 * br + 0.20 * dr + 0.15 * rt + 0.15 * st assert rrs == pytest.approx(expected, rel=1e-6) def test_rrs_bounded_0_to_100(self): results = { "no_signal": _make_result("no_signal", sharpe=-0.5), "strong_signal": _make_result("strong_signal", sharpe=2.0), "a": _make_result("a", sharpe=1.0), "b": _make_result("b", sharpe=1.5), } rrs, _, _, _, _, _ = compute_rrs(results) assert 0.0 <= rrs <= 100.0 @pytest.mark.unit class TestVerdicts: def _make_report(self, rrs: float) -> RegimeRobustnessReport: from libs.backtest.scenarios.robustness import _verdict results = {"a": _make_result("a", sharpe=1.0)} return RegimeRobustnessReport( experiment_name="test", scenario_results=results, signal_integrity=50.0, breadth=50.0, drawdown_resilience=50.0, regime_transition=50.0, stability=50.0, rrs=rrs, verdict=_verdict(rrs), ) def test_robust_verdict(self): r = self._make_report(75.0) assert r.verdict == "ROBUST" def test_fragile_verdict_lower_bound(self): r = self._make_report(40.0) assert r.verdict == "FRAGILE" def test_fragile_verdict_upper_bound(self): r = self._make_report(69.9) assert r.verdict == "FRAGILE" def test_overfit_verdict(self): r = self._make_report(39.9) assert r.verdict == "OVERFIT" def test_boundary_70_is_robust(self): r = self._make_report(70.0) assert r.verdict == "ROBUST"