"""Unit tests for the activation qualification predicate (momentum-based gate).""" from __future__ import annotations from types import SimpleNamespace from app.services.qualification import ( best_target_probability, primary_target_is_projected, primary_target_probability, setup_qualifies, ) # Default gate: floors only; the momentum selection is off (0). Conviction / # conflict are optional tighteners, off here. Target probability is always # required because qualified means the headline target is evidence-backed. DEFAULT_GATE = { "min_momentum_percentile": 0.0, "min_rr": 1.2, "min_confidence": 55.0, "require_high_conviction": False, "exclude_conflicts": False, } # Gate with the cross-sectional momentum selection on (top quintile). MOMENTUM_GATE = {**DEFAULT_GATE, "min_momentum_percentile": 80.0} # Strict gate: every optional tightener turned on. STRICT_GATE = { "min_momentum_percentile": 0.0, "min_rr": 2.0, "min_confidence": 70.0, "require_high_conviction": True, "exclude_conflicts": True, } def _setup(**kwargs): base = dict( direction="long", rr_ratio=3.0, confidence_score=80.0, recommended_action="LONG_HIGH", risk_level="Low", targets=[{"probability": 50.0, "is_primary": True}], ) base.update(kwargs) return SimpleNamespace(**base) class TestFloors: def test_passes_floors(self): assert setup_qualifies(_setup(), DEFAULT_GATE) is True def test_low_rr_floor_fails(self): assert setup_qualifies(_setup(rr_ratio=1.0), DEFAULT_GATE) is False def test_low_confidence_fails(self): assert setup_qualifies(_setup(confidence_score=40.0), DEFAULT_GATE) is False def test_conviction_and_conflict_ignored_by_default(self): s = _setup(recommended_action="LONG_MODERATE", risk_level="Medium") assert setup_qualifies(s, DEFAULT_GATE) is True def test_over_progressed_setup_fails_on_live_rr(self): s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=117.0) assert setup_qualifies(s, DEFAULT_GATE) is False def test_fresh_setup_passes_live_rr(self): s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=101.0) assert setup_qualifies(s, DEFAULT_GATE) is True def test_past_stop_fails_live_rr(self): s = _setup(direction="long", target=120.0, stop_loss=95.0, current_price=94.0) assert setup_qualifies(s, DEFAULT_GATE) is False def test_missing_target_probability_fails(self): assert setup_qualifies(_setup(targets=[]), DEFAULT_GATE) is False def test_non_primary_target_probability_still_passes(self): assert setup_qualifies( _setup(targets=[{"probability": 42.0}]), DEFAULT_GATE, ) is True class TestMomentumGate: def test_top_momentum_passes(self): assert setup_qualifies(_setup(momentum_percentile=92.0), MOMENTUM_GATE) is True def test_below_threshold_fails(self): assert setup_qualifies(_setup(momentum_percentile=50.0), MOMENTUM_GATE) is False def test_missing_percentile_fails_when_gate_active(self): # No residual rank means the production momentum edge was not measured. assert setup_qualifies(_setup(), MOMENTUM_GATE) is False def test_threshold_zero_disables_gate(self): # min_momentum_percentile 0 → a low-momentum name still passes. assert setup_qualifies(_setup(momentum_percentile=10.0), DEFAULT_GATE) is True def test_threshold_zero_allows_missing_percentile(self): assert setup_qualifies(_setup(), DEFAULT_GATE) is True def test_missing_key_defaults_off(self): legacy = {k: v for k, v in DEFAULT_GATE.items() if k != "min_momentum_percentile"} assert setup_qualifies(_setup(momentum_percentile=10.0), legacy) is True def test_short_excluded_when_gate_active(self): # The momentum edge is long-only — a short never qualifies while the gate # is on, even on a top-momentum name. assert setup_qualifies(_setup(direction="short", momentum_percentile=95.0), MOMENTUM_GATE) is False def test_short_allowed_when_gate_off(self): # With the momentum gate disabled, shorts pass on the floors as before. assert setup_qualifies(_setup(direction="short", momentum_percentile=10.0), DEFAULT_GATE) is True class TestStrictTighteners: def test_clean_high_conviction_passes(self): assert setup_qualifies(_setup(targets=[{"probability": 65.0, "is_primary": True}]), STRICT_GATE) is True def test_moderate_action_fails(self): s = _setup(recommended_action="LONG_MODERATE", targets=[{"probability": 65.0, "is_primary": True}]) assert setup_qualifies(s, STRICT_GATE) is False def test_non_low_risk_fails(self): s = _setup(risk_level="Medium", targets=[{"probability": 65.0, "is_primary": True}]) assert setup_qualifies(s, STRICT_GATE) is False NEUTRAL_GATE = {**DEFAULT_GATE, "exclude_neutral": True} class TestExcludeNeutral: def test_neutral_excluded_when_on(self): assert setup_qualifies(_setup(recommended_action="NEUTRAL"), NEUTRAL_GATE) is False def test_missing_action_treated_as_neutral(self): assert setup_qualifies(_setup(recommended_action=None), NEUTRAL_GATE) is False def test_directional_passes_when_on(self): assert setup_qualifies(_setup(recommended_action="LONG_MODERATE"), NEUTRAL_GATE) is True def test_opposing_short_action_fails_for_long_setup(self): assert setup_qualifies(_setup(direction="long", recommended_action="SHORT_MODERATE"), NEUTRAL_GATE) is False def test_matching_short_action_still_fails_long_only_momentum_gate(self): assert setup_qualifies( _setup(direction="short", recommended_action="SHORT_MODERATE", momentum_percentile=95.0), {**NEUTRAL_GATE, "min_momentum_percentile": 80.0}, ) is False def test_neutral_allowed_when_off(self): # Flag absent from the config → NEUTRAL still qualifies (backward compatible). assert setup_qualifies(_setup(recommended_action="NEUTRAL"), DEFAULT_GATE) is True def _projected_setup(**kwargs): """A setup whose primary (headline) target is a blue-sky projection.""" base = dict( direction="long", momentum_percentile=92.0, confidence_score=80.0, targets=[{"probability": 30.0, "is_primary": True, "projected": True}], ) base.update(kwargs) return _setup(**base) class TestProjectedTargetGate: """Projected targets clear a stricter bar, independent of the momentum gate.""" def test_projected_passes_stricter_bar(self): # DEFAULT_GATE has the momentum selection OFF, yet the projected block # still requires strong momentum + higher confidence — and this one clears. assert setup_qualifies(_projected_setup(), DEFAULT_GATE) is True def test_projected_fails_below_momentum_floor(self): assert setup_qualifies(_projected_setup(momentum_percentile=85.0), DEFAULT_GATE) is False def test_projected_fails_missing_momentum(self): assert setup_qualifies(_projected_setup(momentum_percentile=None), DEFAULT_GATE) is False def test_projected_fails_below_raised_confidence_floor(self): # min_confidence 55 + 10 margin = 65; a 60% confidence projected setup fails # even though it would clear the plain 55 floor. assert setup_qualifies(_projected_setup(confidence_score=60.0), DEFAULT_GATE) is False def test_projected_short_never_qualifies(self): s = _projected_setup(direction="short", recommended_action="SHORT_HIGH") assert setup_qualifies(s, DEFAULT_GATE) is False def test_sr_anchored_setup_unaffected_by_projected_bar(self): # A normal (non-projected) setup with modest momentum still passes DEFAULT. assert setup_qualifies(_setup(momentum_percentile=10.0), DEFAULT_GATE) is True def test_primary_target_is_projected_helper(self): assert primary_target_is_projected(_projected_setup()) is True assert primary_target_is_projected(_setup()) is False def test_projected_flag_falls_back_when_no_primary(self): s = _setup(targets=[{"probability": 30.0, "projected": True}]) assert primary_target_is_projected(s) is True class TestBestTargetProbability: def test_returns_max(self): s = _setup(targets=[{"probability": 40.0}, {"probability": 72.0}, {"probability": 55.0}]) assert best_target_probability(s) == 72.0 def test_empty_is_zero(self): assert best_target_probability(_setup(targets=[])) == 0.0 def test_primary_probability_prefers_starred_target(self): s = _setup(targets=[ {"probability": 70.0}, {"probability": 45.0, "is_primary": True}, ]) assert primary_target_probability(s) == 45.0 def test_primary_probability_falls_back_to_best(self): s = _setup(targets=[{"probability": 40.0}, {"probability": 72.0}]) assert primary_target_probability(s) == 72.0