feat: exclude NEUTRAL setups from the activation gate (default on)
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 56s
Deploy / deploy (push) Successful in 34s

A NEUTRAL ("No Clear Setup") recommendation means the engine found no clear
directional trade, yet such setups could still qualify and even be crowned the
top pick purely on momentum rank (e.g. an extended momentum leader with a far,
5%-probability target). A NEUTRAL signal isn't actionable, so it shouldn't
qualify.

New `exclude_neutral` activation flag (default on): setup_qualifies drops setups
whose recommended_action is NEUTRAL. It lives in the shared gate, so it flows
through the dashboard's qualified/top-pick selection, the track record's
qualified stats, and the backtest (which computes recommended_action and gates on
meets_core). Toggleable in Admin → Settings → Activation; the frontend mirror and
activationSummary ("directional") match.

Re-run the backtest after enabling to confirm it holds/improves expectancy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:19:07 +02:00
parent 20a1c143f3
commit 6511a1020b
8 changed files with 60 additions and 0 deletions
+7
View File
@@ -30,6 +30,7 @@ class TestActivationConfig:
"min_confidence": 55.0,
"require_high_conviction": False,
"exclude_conflicts": False,
"exclude_neutral": True,
}
async def test_update_and_read_back(self, session: AsyncSession):
@@ -62,6 +63,12 @@ class TestActivationConfig:
assert config["require_high_conviction"] is True
assert config["exclude_conflicts"] is True
async def test_exclude_neutral_round_trip(self, session: AsyncSession):
# On by default; can be turned off.
assert (await get_activation_config(session))["exclude_neutral"] is True
await update_activation_config(session, {"exclude_neutral": False})
assert (await get_activation_config(session))["exclude_neutral"] is False
async def test_rejects_negative_rr(self, session: AsyncSession):
with pytest.raises(ValidationError):
await update_activation_config(session, {"min_rr": -1.0})
+18
View File
@@ -111,6 +111,24 @@ class TestStrictTighteners:
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_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
class TestBestTargetProbability:
def test_returns_max(self):
s = _setup(targets=[{"probability": 40.0}, {"probability": 72.0}, {"probability": 55.0}])