Files
signal-platform/frontend/src/components/admin/ActivationSettings.tsx
T
dennisthiessen aadec7d403
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m8s
Deploy / deploy (push) Successful in 35s
promote residual momentum ranking
2026-07-02 21:00:39 +02:00

155 lines
6.5 KiB
TypeScript

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_momentum_percentile: 80,
min_rr: 1.2,
min_confidence: 55,
require_high_conviction: false,
exclude_conflicts: false,
exclude_neutral: true,
};
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);
};
const onReset = () => {
setForm(DEFAULTS);
update.mutate(DEFAULTS);
};
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 Gate</h3>
<p className="mt-1 text-xs text-gray-500">
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. The core selection is
<span className="text-gray-300"> residual cross-sectional momentum</span> the ticker must rank in the
top slice of the universe by beta-adjusted 12-1 month momentum, the production signal promoted
from the backtest. R:R and confidence stay as floors. Tune the cutoff against the Track Record's
momentum sweep to see what actually wins.
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Residual Momentum Percentile</span>
<input
type="number"
min={0}
max={100}
step={5}
value={form.min_momentum_percentile}
onChange={(e) => setForm((prev) => ({ ...prev, min_momentum_percentile: Number(e.target.value) }))}
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-600">Ticker's residual 12-1 momentum rank. 80 = top 20% of the universe. 0 disables. The core gate.</span>
</label>
<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"
/>
<span className="text-[11px] text-gray-600">Floor only keeps symmetric/negative trades out.</span>
</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="border-t border-white/[0.06] pt-4">
<label className="flex cursor-pointer items-start gap-2.5 text-sm text-gray-300">
<input
type="checkbox"
checked={form.exclude_neutral}
onChange={(e) => setForm((prev) => ({ ...prev, exclude_neutral: e.target.checked }))}
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
Require a directional call (exclude NEUTRAL)
<span className="mt-0.5 block text-[11px] text-gray-500">
On by default. A NEUTRAL ("No Clear Setup") recommendation isn't a tradeable signal, so it
never qualifies or becomes a top pick. Turn off to also count no-clear-direction residual momentum leaders.
</span>
</span>
</label>
</div>
<div className="border-t border-white/[0.06] pt-4">
<p className="text-xs font-medium uppercase tracking-widest text-gray-500">Optional tighteners</p>
<p className="mt-1 text-[11px] text-gray-600">Off by default — turn on to be more selective on top of the momentum gate.</p>
<div className="mt-3 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>
<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>
);
}