Add multi-factor conviction gate to activation
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>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user