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:
2026-06-12 18:16:04 +02:00
parent d139dd0390
commit 6da65b8d8f
20 changed files with 440 additions and 29 deletions
@@ -0,0 +1,81 @@
import { useEffect, useState } from 'react';
import type { ActivationConfig } from '../../lib/types';
import { useActivationSettings, useUpdateActivationSettings } from '../../hooks/useAdmin';
import { SkeletonTable } from '../ui/Skeleton';
const DEFAULTS: ActivationConfig = {
min_rr: 2,
min_confidence: 70,
};
export function ActivationSettings() {
const { data, isLoading, isError, error } = useActivationSettings();
const update = useUpdateActivationSettings();
const [form, setForm] = useState<ActivationConfig>(DEFAULTS);
useEffect(() => {
if (data) setForm(data);
}, [data]);
const onSave = () => {
update.mutate(form as unknown as Record<string, number>);
};
const onReset = () => {
setForm(DEFAULTS);
update.mutate(DEFAULTS as unknown as Record<string, number>);
};
if (isLoading) return <SkeletonTable rows={2} cols={2} />;
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load activation thresholds'}</p>;
return (
<div className="glass p-5 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-200">Activation Thresholds</h3>
<p className="mt-1 text-xs text-gray-500">
What counts as a signal worth acting on. Used as the default Signals filters, the
Dashboard's qualified-setup metrics, and the Track Record's "qualified only" view.
All setups are still evaluated regardless, so these thresholds can be validated
against the confidence breakdown.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Risk:Reward (1 : x)</span>
<input
type="number"
min={0}
step={0.1}
value={form.min_rr}
onChange={(e) => setForm((prev) => ({ ...prev, min_rr: Number(e.target.value) }))}
className="w-full input-glass px-3 py-2 text-sm"
/>
</label>
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Confidence (%)</span>
<input
type="number"
min={0}
max={100}
step={1}
value={form.min_confidence}
onChange={(e) => setForm((prev) => ({ ...prev, min_confidence: Number(e.target.value) }))}
className="w-full input-glass px-3 py-2 text-sm"
/>
</label>
</div>
<div className="flex items-center gap-2">
<button className="btn-primary px-4 py-2 text-sm" onClick={onSave} disabled={update.isPending}>
{update.isPending ? 'Saving…' : 'Save Thresholds'}
</button>
<button className="px-4 py-2 text-sm rounded border border-white/[0.1] text-gray-300 hover:text-white" onClick={onReset} disabled={update.isPending}>
Reset to Defaults
</button>
</div>
</div>
);
}