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,94 @@
|
||||
"""Unit tests for runtime sentiment provider configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.exceptions import ProviderError, ValidationError
|
||||
from app.models.settings import SystemSetting
|
||||
from app.services import sentiment_provider_service as sps
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def session() -> AsyncSession:
|
||||
from tests.conftest import _test_session_factory
|
||||
|
||||
async with _test_session_factory() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_env_keys(monkeypatch):
|
||||
"""Default: no env keys, so DB-only behavior is deterministic."""
|
||||
monkeypatch.setattr(sps.settings, "openai_api_key", "")
|
||||
monkeypatch.setattr(sps.settings, "gemini_api_key", "")
|
||||
|
||||
|
||||
class TestGetConfig:
|
||||
async def test_defaults_no_key(self, session: AsyncSession):
|
||||
config = await sps.get_sentiment_config(session)
|
||||
assert config["provider"] == "openai"
|
||||
assert config["model"] == "gpt-4o-mini"
|
||||
assert config["api_key_configured"] is False
|
||||
assert config["api_key_source"] == "none"
|
||||
assert set(config["valid_providers"]) == {"openai", "gemini"}
|
||||
|
||||
async def test_never_returns_raw_key(self, session: AsyncSession):
|
||||
await sps.update_sentiment_config(session, api_key="sk-secret-123")
|
||||
config = await sps.get_sentiment_config(session)
|
||||
# No field should leak the key
|
||||
assert "sk-secret-123" not in str(config)
|
||||
assert config["api_key_configured"] is True
|
||||
assert config["api_key_source"] == "database"
|
||||
|
||||
async def test_env_fallback_reported(self, session: AsyncSession, monkeypatch):
|
||||
monkeypatch.setattr(sps.settings, "openai_api_key", "sk-from-env")
|
||||
config = await sps.get_sentiment_config(session)
|
||||
assert config["api_key_configured"] is True
|
||||
assert config["api_key_source"] == "environment"
|
||||
|
||||
|
||||
class TestUpdateConfig:
|
||||
async def test_update_provider_and_model(self, session: AsyncSession):
|
||||
result = await sps.update_sentiment_config(
|
||||
session, provider="gemini", model="gemini-2.0-flash"
|
||||
)
|
||||
assert result["provider"] == "gemini"
|
||||
assert result["model"] == "gemini-2.0-flash"
|
||||
|
||||
async def test_empty_key_does_not_overwrite(self, session: AsyncSession):
|
||||
await sps.update_sentiment_config(session, api_key="sk-original")
|
||||
# Subsequent save without a key must keep the original
|
||||
await sps.update_sentiment_config(session, provider="openai", api_key="")
|
||||
result = await session.execute(
|
||||
select(SystemSetting).where(SystemSetting.key == sps.KEY_API_KEY)
|
||||
)
|
||||
assert result.scalar_one().value == "sk-original"
|
||||
|
||||
async def test_rejects_invalid_provider(self, session: AsyncSession):
|
||||
with pytest.raises(ValidationError):
|
||||
await sps.update_sentiment_config(session, provider="anthropic")
|
||||
|
||||
|
||||
class TestBuildProvider:
|
||||
async def test_raises_without_key(self, session: AsyncSession):
|
||||
with pytest.raises(ProviderError):
|
||||
await sps.build_sentiment_provider(session)
|
||||
|
||||
async def test_builds_openai(self, session: AsyncSession):
|
||||
await sps.update_sentiment_config(session, provider="openai", api_key="sk-x")
|
||||
provider = await sps.build_sentiment_provider(session)
|
||||
assert type(provider).__name__ == "OpenAISentimentProvider"
|
||||
|
||||
async def test_builds_gemini(self, session: AsyncSession):
|
||||
await sps.update_sentiment_config(session, provider="gemini", api_key="g-x")
|
||||
provider = await sps.build_sentiment_provider(session)
|
||||
assert type(provider).__name__ == "GeminiSentimentProvider"
|
||||
|
||||
async def test_uses_env_key_when_db_empty(self, session: AsyncSession, monkeypatch):
|
||||
monkeypatch.setattr(sps.settings, "openai_api_key", "sk-env")
|
||||
await sps.update_sentiment_config(session, provider="openai")
|
||||
provider = await sps.build_sentiment_provider(session)
|
||||
assert type(provider).__name__ == "OpenAISentimentProvider"
|
||||
Reference in New Issue
Block a user