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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user