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>
73 lines
2.7 KiB
Python
73 lines
2.7 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_rr": 2.0,
|
|
"min_confidence": 70.0,
|
|
"min_target_probability": 60.0,
|
|
"require_high_conviction": True,
|
|
"exclude_conflicts": True,
|
|
}
|
|
|
|
async def test_update_and_read_back(self, session: AsyncSession):
|
|
updated = await update_activation_config(
|
|
session, {"min_rr": 1.5, "min_confidence": 60.0}
|
|
)
|
|
assert updated["min_rr"] == 1.5
|
|
assert updated["min_confidence"] == 60.0
|
|
|
|
config = await get_activation_config(session)
|
|
assert config["min_rr"] == 1.5
|
|
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"] == 2.0 # default untouched
|
|
assert config["min_confidence"] == 80.0
|
|
|
|
async def test_conviction_flags_round_trip(self, session: AsyncSession):
|
|
await update_activation_config(
|
|
session,
|
|
{"require_high_conviction": False, "exclude_conflicts": False, "min_target_probability": 45.0},
|
|
)
|
|
config = await get_activation_config(session)
|
|
assert config["require_high_conviction"] is False
|
|
assert config["exclude_conflicts"] is False
|
|
assert config["min_target_probability"] == 45.0
|
|
|
|
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})
|
|
|
|
async def test_rejects_out_of_range_target_probability(self, session: AsyncSession):
|
|
with pytest.raises(ValidationError):
|
|
await update_activation_config(session, {"min_target_probability": 150.0})
|