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:
@@ -6,6 +6,9 @@ import { SkeletonTable } from '../ui/Skeleton';
|
||||
const DEFAULTS: ActivationConfig = {
|
||||
min_rr: 2,
|
||||
min_confidence: 70,
|
||||
min_target_probability: 60,
|
||||
require_high_conviction: true,
|
||||
exclude_conflicts: true,
|
||||
};
|
||||
|
||||
export function ActivationSettings() {
|
||||
@@ -19,12 +22,12 @@ export function ActivationSettings() {
|
||||
}, [data]);
|
||||
|
||||
const onSave = () => {
|
||||
update.mutate(form as unknown as Record<string, number>);
|
||||
update.mutate(form);
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
setForm(DEFAULTS);
|
||||
update.mutate(DEFAULTS as unknown as Record<string, number>);
|
||||
update.mutate(DEFAULTS);
|
||||
};
|
||||
|
||||
if (isLoading) return <SkeletonTable rows={2} cols={2} />;
|
||||
@@ -33,16 +36,16 @@ export function ActivationSettings() {
|
||||
return (
|
||||
<div className="glass p-5 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-200">Activation Thresholds</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-200">Activation Gate</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.
|
||||
What counts as a signal worth acting on. Drives the Dashboard's "Qualified" metric, the
|
||||
Signals "Qualified only" view, and the Track Record's qualified stats. All setups are
|
||||
still evaluated regardless — tighten the gate, then watch qualified expectancy in the
|
||||
Track Record to find what actually wins.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Min Risk:Reward (1 : x)</span>
|
||||
<input
|
||||
@@ -53,6 +56,7 @@ export function ActivationSettings() {
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, min_rr: Number(e.target.value) }))}
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">Set above your scanner floor or it does nothing.</span>
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Min Confidence (%)</span>
|
||||
@@ -66,6 +70,50 @@ export function ActivationSettings() {
|
||||
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 Target Probability (%)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={form.min_target_probability}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, min_target_probability: Number(e.target.value) }))}
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">Best target's probability must clear this. 0 disables.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="flex cursor-pointer items-start gap-2.5 text-sm text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.require_high_conviction}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, require_high_conviction: e.target.checked }))}
|
||||
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
|
||||
/>
|
||||
<span>
|
||||
Require high conviction
|
||||
<span className="mt-0.5 block text-[11px] text-gray-500">
|
||||
Only LONG (High) / SHORT (High) — the signals must clearly pick a side.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-start gap-2.5 text-sm text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.exclude_conflicts}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, exclude_conflicts: e.target.checked }))}
|
||||
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
|
||||
/>
|
||||
<span>
|
||||
Exclude conflicted setups
|
||||
<span className="mt-0.5 block text-[11px] text-gray-500">
|
||||
Risk level must be Low — drops setups with contradicting signals.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user