Add multi-factor conviction gate to activation
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 26s

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:
2026-06-13 11:50:42 +02:00
parent 6da65b8d8f
commit d53ed972d1
25 changed files with 924 additions and 110 deletions
+10 -13
View File
@@ -23,6 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.ohlcv import OHLCVRecord
from app.models.trade_setup import TradeSetup
from app.services.qualification import setup_qualifies
logger = logging.getLogger(__name__)
@@ -180,8 +181,7 @@ def _confidence_bucket(score: float | None) -> str | None:
async def get_performance_stats(
db: AsyncSession,
min_rr: float | None = None,
min_confidence: float | None = None,
config: dict | None = None,
) -> dict:
"""Aggregate outcome statistics over all evaluated trade setups.
@@ -189,9 +189,10 @@ async def get_performance_stats(
loss = -1R, expired = 0R). A positive avg_r means the signals have
been profitable on a risk-adjusted basis.
min_rr / min_confidence filter the overall, direction and action
breakdowns. The confidence breakdown deliberately stays unfiltered:
it is the instrument for validating the thresholds themselves.
When ``config`` (an activation-gate dict) is supplied, the overall,
direction and action breakdowns cover only qualified setups. The
confidence breakdown deliberately stays unfiltered: it is the
instrument for validating the gate itself.
"""
result = await db.execute(
select(TradeSetup).where(TradeSetup.actual_outcome.is_not(None))
@@ -203,14 +204,10 @@ async def get_performance_stats(
)
pending_count = len(pending_result.scalars().all())
def qualifies(setup: TradeSetup) -> bool:
if min_rr is not None and setup.rr_ratio < min_rr:
return False
if min_confidence is not None and (setup.confidence_score or 0.0) < min_confidence:
return False
return True
qualified = [s for s in evaluated if qualifies(s)]
if config is not None:
qualified = [s for s in evaluated if setup_qualifies(s, config)]
else:
qualified = evaluated
by_direction: dict[str, list[TradeSetup]] = {}
by_action: dict[str, list[TradeSetup]] = {}