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
co-authored by Claude Fable 5
parent 6da65b8d8f
commit d53ed972d1
25 changed files with 924 additions and 110 deletions
+33 -11
View File
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { useTrades } from '../../hooks/useTrades';
import { qualifiesSetup, activationSummary } from '../../lib/qualification';
import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../scanner/TradeTable';
import { SkeletonTable } from '../ui/Skeleton';
import { useToast } from '../ui/Toast';
@@ -99,18 +100,16 @@ export function SetupsPanel() {
const queryClient = useQueryClient();
const toast = useToast();
// null = user hasn't touched the filter; falls back to admin-configured
// activation thresholds once loaded
const [minRROverride, setMinRROverride] = useState<number | null>(null);
const [minConfidenceOverride, setMinConfidenceOverride] = useState<number | null>(null);
// "Qualified only" applies the admin activation gate; the manual filters
// below refine within whatever is shown.
const [qualifiedOnly, setQualifiedOnly] = useState(true);
const [minRR, setMinRR] = useState(0);
const [minConfidence, setMinConfidence] = useState(0);
const [directionFilter, setDirectionFilter] = useState<DirectionFilter>('both');
const [actionFilter, setActionFilter] = useState<ActionFilter>('all');
const [sortColumn, setSortColumn] = useState<SortColumn>('rr_ratio');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const minRR = minRROverride ?? activation.data?.min_rr ?? 0;
const minConfidence = minConfidenceOverride ?? activation.data?.min_confidence ?? 0;
const scanMutation = useMutation({
mutationFn: () => triggerJob('rr_scanner'),
onSuccess: () => {
@@ -133,12 +132,35 @@ export function SetupsPanel() {
const processed = useMemo(() => {
if (!trades) return [];
const filtered = filterTrades(trades, minRR, directionFilter, minConfidence, actionFilter);
let base = trades;
if (qualifiedOnly && activation.data) {
base = base.filter((t) => qualifiesSetup(t, activation.data!));
}
const filtered = filterTrades(base, minRR, directionFilter, minConfidence, actionFilter);
return sortTrades(filtered, sortColumn, sortDirection);
}, [trades, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]);
}, [trades, qualifiedOnly, activation.data, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]);
return (
<div className="space-y-6">
{/* Qualified gate toggle */}
<div className="glass-sm flex flex-wrap items-center justify-between gap-3 px-4 py-3">
<label className="flex cursor-pointer items-center gap-2.5 text-sm text-gray-300">
<input
type="checkbox"
checked={qualifiedOnly}
onChange={(e) => setQualifiedOnly(e.target.checked)}
className="h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
Qualified only
{activation.data && (
<span className="num ml-2 text-xs text-gray-500">{activationSummary(activation.data)}</span>
)}
</span>
</label>
<span className="text-xs text-gray-500">Manual filters below refine within this.</span>
</div>
{/* Filter toolbar */}
<div className="glass-sm flex flex-wrap items-end gap-4 p-4">
<Field label="Min Risk:Reward" htmlFor="min-rr">
@@ -150,7 +172,7 @@ export function SetupsPanel() {
min={0}
step={0.1}
value={minRR}
onChange={(e) => setMinRROverride(Number(e.target.value) || 0)}
onChange={(e) => setMinRR(Number(e.target.value) || 0)}
className="w-20"
/>
</div>
@@ -174,7 +196,7 @@ export function SetupsPanel() {
max={100}
step={1}
value={minConfidence}
onChange={(e) => setMinConfidenceOverride(Number(e.target.value) || 0)}
onChange={(e) => setMinConfidence(Number(e.target.value) || 0)}
className="w-24"
/>
</Field>