6511a1020b
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>
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
"""Unit tests for activation threshold configuration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.exceptions import ValidationError
|
|
from app.services.admin_service import (
|
|
get_activation_config,
|
|
update_activation_config,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def session() -> AsyncSession:
|
|
"""DB session compatible with services that commit."""
|
|
from tests.conftest import _test_session_factory
|
|
|
|
async with _test_session_factory() as session:
|
|
yield session
|
|
|
|
|
|
class TestActivationConfig:
|
|
async def test_defaults_when_unset(self, session: AsyncSession):
|
|
config = await get_activation_config(session)
|
|
assert config == {
|
|
"min_momentum_percentile": 80.0,
|
|
"min_rr": 1.2,
|
|
"min_confidence": 55.0,
|
|
"require_high_conviction": False,
|
|
"exclude_conflicts": False,
|
|
"exclude_neutral": True,
|
|
}
|
|
|
|
async def test_update_and_read_back(self, session: AsyncSession):
|
|
updated = await update_activation_config(
|
|
session, {"min_momentum_percentile": 70.0, "min_confidence": 60.0}
|
|
)
|
|
assert updated["min_momentum_percentile"] == 70.0
|
|
assert updated["min_confidence"] == 60.0
|
|
|
|
config = await get_activation_config(session)
|
|
assert config["min_momentum_percentile"] == 70.0
|
|
assert config["min_confidence"] == 60.0
|
|
|
|
async def test_partial_update_keeps_other_value(self, session: AsyncSession):
|
|
await update_activation_config(session, {"min_confidence": 80.0})
|
|
config = await get_activation_config(session)
|
|
assert config["min_rr"] == 1.2 # default untouched
|
|
assert config["min_confidence"] == 80.0
|
|
|
|
async def test_rejects_out_of_range_momentum_percentile(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_activation_config(session, {"min_momentum_percentile": 150.0})
|
|
|
|
async def test_conviction_flags_round_trip(self, session: AsyncSession):
|
|
await update_activation_config(
|
|
session,
|
|
{"require_high_conviction": True, "exclude_conflicts": True},
|
|
)
|
|
config = await get_activation_config(session)
|
|
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})
|
|
|
|
async def test_rejects_out_of_range_confidence(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_activation_config(session, {"min_confidence": 120.0})
|