Files
signal-platform/tests/unit/test_qualification.py
T
dennisthiessenandClaude Opus 4.8 294d935030 Add blue-sky projected targets and played-out setup UX
Fixes stale below-price setups showing as current recommendations. Three
distinct causes share the symptom (get_trade_setups returns the latest stored
setup per direction and never expires it):

- Genuine blue-sky (no overhead S/R): scanner + TargetGenerator now project a
  measured-move target (entry +/- 3*ATR, ~2:1 R:R), flagged projected with a
  low sr_strength probability haircut. Overhead check keys on level tag OR price
  so it never projects through a straddling resistance cluster.
- Projected targets clear a stricter activation bar (long-only, momentum >= 90,
  confidence >= min+10), independent of the general momentum gate. Mirrored in
  frontend qualification.ts.
- Played-out UX (fixes the reported TTWO case, which is R:R-starved under a
  resistance cluster, not blue-sky): when price is at/past target or through the
  stop, RecommendationPanel shows a "No current setup" state and softens the
  stale ticker-level header/reasoning, instead of a stale actionable card.

No migration: the projected flag rides in existing targets_json. 504 backend
unit tests pass; frontend typechecks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:30:28 +02:00

225 lines
8.9 KiB
Python

"""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