Tighten qualified signal gate
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
@@ -127,6 +128,15 @@ def _make_setup(
|
||||
detected: datetime | None = None,
|
||||
**kwargs,
|
||||
) -> TradeSetup:
|
||||
targets_json = kwargs.pop(
|
||||
"targets_json",
|
||||
json.dumps([{
|
||||
"price": target,
|
||||
"rr_ratio": rr,
|
||||
"probability": 50.0,
|
||||
"is_primary": True,
|
||||
}]),
|
||||
)
|
||||
return TradeSetup(
|
||||
ticker_id=ticker.id,
|
||||
direction=direction,
|
||||
@@ -136,6 +146,7 @@ def _make_setup(
|
||||
rr_ratio=rr,
|
||||
composite_score=50.0,
|
||||
detected_at=detected or datetime(2026, 1, 2, 21, 0, tzinfo=timezone.utc),
|
||||
targets_json=targets_json,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,15 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.qualification import best_target_probability, setup_qualifies
|
||||
from app.services.qualification import (
|
||||
best_target_probability,
|
||||
primary_target_probability,
|
||||
setup_qualifies,
|
||||
)
|
||||
|
||||
# Default gate: floors only; the momentum selection is off (0). Conviction /
|
||||
# conflict / target-probability are optional tighteners, off here.
|
||||
# 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,
|
||||
@@ -68,6 +73,15 @@ class TestFloors:
|
||||
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):
|
||||
@@ -76,15 +90,17 @@ class TestMomentumGate:
|
||||
def test_below_threshold_fails(self):
|
||||
assert setup_qualifies(_setup(momentum_percentile=50.0), MOMENTUM_GATE) is False
|
||||
|
||||
def test_missing_percentile_defers_to_floors(self):
|
||||
# No percentile attached (e.g. production not yet wired) → the momentum
|
||||
# gate is skipped and the setup still clears on the floors.
|
||||
assert setup_qualifies(_setup(), MOMENTUM_GATE) is True
|
||||
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
|
||||
@@ -146,3 +162,14 @@ class TestBestTargetProbability:
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user