Add activation thresholds: qualified-signal defaults and views
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 32s
Deploy / deploy (push) Successful in 24s

Admin-configurable thresholds (min R:R, default 2.0; min confidence,
default 70%) defining what counts as an actionable signal:

- Admin Settings: new Activation Thresholds panel
  (GET/PUT /admin/settings/activation)
- GET /trades/activation exposes values to all users with access
- Signals/Setups: filters initialize from activation values
- Track Record: "Qualified signals only" toggle (default on) via
  min_rr/min_confidence params on /trades/performance; the
  confidence breakdown always covers the full population so the
  thresholds can be validated against outcomes
- Dashboard: "Qualified" metric and qualified-first Top Setups
- Outcome evaluator unchanged: every setup is still evaluated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:16:04 +02:00
parent d139dd0390
commit 6da65b8d8f
20 changed files with 440 additions and 29 deletions
+50
View File
@@ -0,0 +1,50 @@
"""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}
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, "min_confidence": 60.0}
config = await get_activation_config(session)
assert config == {"min_rr": 1.5, "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_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})
+39
View File
@@ -270,3 +270,42 @@ class TestGetPerformanceStats:
assert stats["overall"]["losses"] == 1
assert stats["overall"]["hit_rate"] == 0.0
assert stats["overall"]["avg_r"] == -1.0
async def test_activation_filters_apply_to_overall_but_not_confidence(
self, db_session: AsyncSession
):
ticker = await _make_ticker(db_session)
# Qualified: high confidence, high R:R
db_session.add(_make_setup(
ticker, rr=3.0, confidence_score=80.0, actual_outcome=OUTCOME_TARGET_HIT,
))
# Unqualified: low confidence
db_session.add(_make_setup(
ticker, rr=3.0, confidence_score=40.0, actual_outcome=OUTCOME_STOP_HIT,
))
# Unqualified: low R:R
db_session.add(_make_setup(
ticker, rr=1.2, confidence_score=90.0, actual_outcome=OUTCOME_STOP_HIT,
))
await db_session.flush()
stats = await get_performance_stats(db_session, min_rr=2.0, min_confidence=70.0)
# Overall covers only the qualified setup
assert stats["overall"]["total"] == 1
assert stats["overall"]["wins"] == 1
assert stats["overall"]["hit_rate"] == 100.0
# Confidence breakdown still covers the full population
total_in_confidence = sum(
bucket["total"] for bucket in stats["by_confidence"].values()
)
assert total_in_confidence == 3
async def test_no_filters_returns_full_population(self, db_session: AsyncSession):
ticker = await _make_ticker(db_session)
db_session.add(_make_setup(ticker, rr=1.2, confidence_score=10.0, actual_outcome=OUTCOME_TARGET_HIT))
await db_session.flush()
stats = await get_performance_stats(db_session)
assert stats["overall"]["total"] == 1