d53ed972d1
Make "qualified" mean an edge candidate, not just R:R + confidence. The gate now also requires (all admin-configurable, defaults on): - high conviction: recommended_action LONG_HIGH / SHORT_HIGH only - clean read: risk_level Low (no contradicting signals) - probable primary target: best target probability >= min (default 60) - Shared predicate: app/services/qualification.py + frontend/src/lib/qualification.ts (mirrored) - Activation config extended (min_target_probability, require_high_conviction, exclude_conflicts) with bool-aware get/update + validation - /trades/performance switched to ?qualified_only=true, applying the full gate server-side; confidence breakdown stays unfiltered - Dashboard "Qualified", Signals "Qualified only" toggle, and Track Record all use the one gate; Admin gains the new controls Sentiment provider runtime config (prior change) included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""Unit tests for the activation qualification predicate."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from app.services.qualification import best_target_probability, setup_qualifies
|
|
|
|
FULL_GATE = {
|
|
"min_rr": 2.0,
|
|
"min_confidence": 70.0,
|
|
"min_target_probability": 60.0,
|
|
"require_high_conviction": True,
|
|
"exclude_conflicts": True,
|
|
}
|
|
|
|
|
|
def _setup(**kwargs):
|
|
base = dict(
|
|
rr_ratio=3.0,
|
|
confidence_score=80.0,
|
|
recommended_action="LONG_HIGH",
|
|
risk_level="Low",
|
|
targets=[{"probability": 65.0}],
|
|
)
|
|
base.update(kwargs)
|
|
return SimpleNamespace(**base)
|
|
|
|
|
|
class TestSetupQualifies:
|
|
def test_clean_high_conviction_setup_passes(self):
|
|
assert setup_qualifies(_setup(), FULL_GATE) is True
|
|
|
|
def test_low_rr_fails(self):
|
|
assert setup_qualifies(_setup(rr_ratio=1.5), FULL_GATE) is False
|
|
|
|
def test_low_confidence_fails(self):
|
|
assert setup_qualifies(_setup(confidence_score=60.0), FULL_GATE) is False
|
|
|
|
def test_moderate_action_fails_when_high_conviction_required(self):
|
|
assert setup_qualifies(_setup(recommended_action="LONG_MODERATE"), FULL_GATE) is False
|
|
|
|
def test_neutral_action_fails(self):
|
|
assert setup_qualifies(_setup(recommended_action="NEUTRAL"), FULL_GATE) is False
|
|
|
|
def test_short_high_passes(self):
|
|
assert setup_qualifies(_setup(recommended_action="SHORT_HIGH"), FULL_GATE) is True
|
|
|
|
def test_non_low_risk_fails_when_excluding_conflicts(self):
|
|
assert setup_qualifies(_setup(risk_level="Medium"), FULL_GATE) is False
|
|
assert setup_qualifies(_setup(risk_level="High"), FULL_GATE) is False
|
|
|
|
def test_low_target_probability_fails(self):
|
|
assert setup_qualifies(_setup(targets=[{"probability": 40.0}]), FULL_GATE) is False
|
|
|
|
def test_no_targets_fails_when_probability_required(self):
|
|
assert setup_qualifies(_setup(targets=[]), FULL_GATE) is False
|
|
|
|
def test_conviction_filters_can_be_disabled(self):
|
|
relaxed = {
|
|
"min_rr": 2.0,
|
|
"min_confidence": 70.0,
|
|
"min_target_probability": 0.0,
|
|
"require_high_conviction": False,
|
|
"exclude_conflicts": False,
|
|
}
|
|
# Moderate action, medium risk, no targets — still passes on rr+confidence alone
|
|
s = _setup(recommended_action="LONG_MODERATE", risk_level="Medium", targets=[])
|
|
assert setup_qualifies(s, relaxed) is True
|
|
|
|
def test_missing_confidence_treated_as_zero(self):
|
|
assert setup_qualifies(_setup(confidence_score=None), FULL_GATE) is False
|
|
|
|
|
|
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
|