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:
dennisthiessenandClaude Fable 5 committed 2026-06-12 18:16:04 +02:00
1 parent d139dd0390
commit 6da65b8d8f
20 files changed
+440 -29

No files matched your search

@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { useTrades } from '../../hooks/useTrades';
import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../scanner/TradeTable';
import { SkeletonTable } from '../ui/Skeleton';
@@ -94,16 +95,22 @@ function sortTrades(
export function SetupsPanel() {
const { data: trades, isLoading, isError, error } = useTrades();
const activation = useActivation();
const queryClient = useQueryClient();
const toast = useToast();
const [minRR, setMinRR] = useState(0);
// 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);
const [directionFilter, setDirectionFilter] = useState<DirectionFilter>('both');
const [minConfidence, setMinConfidence] = useState(0);
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: () => {
@@ -143,7 +150,7 @@ export function SetupsPanel() {
min={0}
step={0.1}
value={minRR}
onChange={(e) => setMinRR(Number(e.target.value) || 0)}
onChange={(e) => setMinRROverride(Number(e.target.value) || 0)}
className="w-20"
/>
</div>
@@ -167,7 +174,7 @@ export function SetupsPanel() {
max={100}
step={1}
value={minConfidence}
onChange={(e) => setMinConfidence(Number(e.target.value) || 0)}
onChange={(e) => setMinConfidenceOverride(Number(e.target.value) || 0)}
className="w-24"
/>
</Field>
@@ -1,4 +1,6 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { usePerformance } from '../../hooks/usePerformance';
import { triggerJob } from '../../api/admin';
import { Button } from '../ui/Button';
@@ -89,7 +91,14 @@ function BreakdownTable({ rows, labelHeader, mapLabel }: {
}
export function TrackRecordPanel() {
const { data, isLoading, isError, error } = usePerformance();
const [qualifiedOnly, setQualifiedOnly] = useState(true);
const activation = useActivation();
const params = qualifiedOnly && activation.data
? { min_rr: activation.data.min_rr, min_confidence: activation.data.min_confidence }
: undefined;
const { data, isLoading, isError, error } = usePerformance(params);
const queryClient = useQueryClient();
const toast = useToast();
@@ -106,6 +115,26 @@ export function TrackRecordPanel() {
return (
<div className="space-y-6">
<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 signals only
{activation.data && (
<span className="num ml-2 text-xs text-gray-500">
R:R ≥ {activation.data.min_rr.toFixed(1)} · conf ≥ {activation.data.min_confidence.toFixed(0)}%
</span>
)}
</span>
</label>
<p className="text-xs text-gray-500">Confidence breakdown always covers all setups.</p>
</div>
<div className="flex items-start justify-between gap-4">
<Disclosure summary="How outcomes are measured">
<p className="text-xs text-gray-400">
@@ -138,8 +167,9 @@ export function TrackRecordPanel() {
{data && data.overall.total === 0 && (
<Callout variant="empty">
No evaluated setups yet. Outcomes appear once setups are old enough for their stop or
target to be hit — the evaluator runs nightly, or click Evaluate Now.
{qualifiedOnly
? 'No evaluated setups meet the activation thresholds yet. Untick "Qualified signals only" to see all evaluated setups, or wait for more outcomes.'
: 'No evaluated setups yet. Outcomes appear once setups are old enough for their stop or target to be hit — the evaluator runs nightly, or click Evaluate Now.'}
{data.pending > 0 && ` ${data.pending} setup${data.pending === 1 ? '' : 's'} pending evaluation.`}
</Callout>
)}