Files
signal-platform/frontend/src/hooks/useAdmin.ts
T
dennisthiessen c34f3cb1a4
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 46s
Deploy / deploy (push) Successful in 28s
redesign activation gate to expected value + make pipelines cron-configurable
Diagnosing "no qualified signals for 5 days": setups were generated but none
qualified. The gate required BOTH a high min_rr (2.0) AND a high
min_target_probability (60), which became contradictory after the Jun-15
probability recalibration — probability already embeds R:R via the 1/(rr+1) ruin
term, so high-R:R targets are inherently low-probability and nothing cleared both.

Gate is now expected value (R): p*rr - (1-p) from the primary target's
probability. R:R and confidence stay as floors; high-conviction / exclude-conflicts
/ min-target-probability become optional tighteners (default off). Defaults:
min_expected_value=0.15, min_rr=1.2, min_confidence=55. EV is only enforced when
computable. Migration 009 clears stored activation_* rows so the new defaults
apply. Backtest sweeps min_expected_value instead of target probability.

Scheduling: pipelines are now cron-configurable in Admin -> Jobs. daily_pipeline
(full, default 0 7 * * *) plus a new light intraday_pipeline (OHLCV + outcome eval,
default hourly US session) that keeps prices/live-R:R current without setup churn.
Fundamentals on its own early weekly cron. Timezone configurable (default
Europe/Berlin). Moving interval->CronTrigger also fixes the restart-deferral bug
where an interval job's countdown resets on every process restart.

319 backend unit tests pass; frontend tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:46:38 +02:00

359 lines
10 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as adminApi from '../api/admin';
import { useToast } from '../components/ui/Toast';
import type { TickerUniverse } from '../lib/types';
// ── Users ──
export function useUsers() {
return useQuery({
queryKey: ['admin', 'users'],
queryFn: () => adminApi.listUsers(),
});
}
export function useCreateUser() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (data: {
username: string;
password: string;
role: string;
has_access: boolean;
}) => adminApi.createUser(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to create user');
},
});
}
export function useUpdateAccess() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: ({ userId, hasAccess }: { userId: number; hasAccess: boolean }) =>
adminApi.updateAccess(userId, hasAccess),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update access');
},
});
}
export function useResetPassword() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: ({ userId, password }: { userId: number; password: string }) =>
adminApi.resetPassword(userId, password),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
addToast('success', 'Password reset successfully');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to reset password');
},
});
}
// ── Settings ──
export function useSettings() {
return useQuery({
queryKey: ['admin', 'settings'],
queryFn: () => adminApi.listSettings(),
});
}
export function useUpdateSetting() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: ({ key, value }: { key: string; value: string }) =>
adminApi.updateSetting(key, value),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'settings'] });
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update setting');
},
});
}
export function useRecommendationSettings() {
return useQuery({
queryKey: ['admin', 'recommendation-settings'],
queryFn: () => adminApi.getRecommendationSettings(),
});
}
export function useUpdateRecommendationSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: Record<string, number>) =>
adminApi.updateRecommendationSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'recommendation-settings'] });
addToast('success', 'Recommendation settings updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update recommendation settings');
},
});
}
export function useActivationSettings() {
return useQuery({
queryKey: ['admin', 'activation-settings'],
queryFn: () => adminApi.getActivationSettings(),
});
}
export function useUpdateActivationSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: Partial<import('../lib/types').ActivationConfig>) =>
adminApi.updateActivationSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'activation-settings'] });
qc.invalidateQueries({ queryKey: ['activation'] });
qc.invalidateQueries({ queryKey: ['performance'] });
addToast('success', 'Activation gate updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update activation thresholds');
},
});
}
export function useScheduleSettings() {
return useQuery({
queryKey: ['admin', 'schedule-settings'],
queryFn: () => adminApi.getScheduleSettings(),
});
}
export function useUpdateScheduleSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: Partial<import('../lib/types').ScheduleConfig>) =>
adminApi.updateScheduleSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'schedule-settings'] });
qc.invalidateQueries({ queryKey: ['admin', 'jobs'] });
addToast('success', 'Schedule updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update schedule');
},
});
}
export function useSentimentSettings() {
return useQuery({
queryKey: ['admin', 'sentiment-settings'],
queryFn: () => adminApi.getSentimentSettings(),
});
}
export function useUpdateSentimentSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: { provider?: string; model?: string; api_key?: string }) =>
adminApi.updateSentimentSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'sentiment-settings'] });
addToast('success', 'Sentiment provider updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update sentiment provider');
},
});
}
export function useTestSentimentProvider() {
const { addToast } = useToast();
return useMutation({
mutationFn: (ticker: string) => adminApi.testSentimentSettings(ticker),
onError: (error: Error) => {
addToast('error', error.message || 'Sentiment test failed');
},
});
}
export function useAlertSettings() {
return useQuery({
queryKey: ['admin', 'alert-settings'],
queryFn: () => adminApi.getAlertSettings(),
});
}
export function useUpdateAlertSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: Parameters<typeof adminApi.updateAlertSettings>[0]) =>
adminApi.updateAlertSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'alert-settings'] });
addToast('success', 'Alert settings updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update alert settings');
},
});
}
export function useTestAlert() {
const { addToast } = useToast();
return useMutation({
mutationFn: () => adminApi.testAlertSettings(),
onSuccess: (result) => {
if (result.ok) addToast('success', 'Test alert sent — check Telegram.');
else addToast('error', result.error || 'Test alert failed');
},
onError: (error: Error) => {
addToast('error', error.message || 'Test alert failed');
},
});
}
export function useTickerUniverseSetting() {
return useQuery({
queryKey: ['admin', 'ticker-universe'],
queryFn: () => adminApi.getTickerUniverseSetting(),
});
}
export function useUpdateTickerUniverseSetting() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (universe: TickerUniverse) => adminApi.updateTickerUniverseSetting(universe),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'ticker-universe'] });
addToast('success', 'Default ticker universe updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update default ticker universe');
},
});
}
export function useBootstrapTickers() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: ({ universe, pruneMissing }: { universe: TickerUniverse; pruneMissing: boolean }) =>
adminApi.bootstrapTickers(universe, pruneMissing),
onSuccess: (result) => {
qc.invalidateQueries({ queryKey: ['tickers'] });
qc.invalidateQueries({ queryKey: ['admin', 'ticker-universe'] });
addToast(
'success',
`Bootstrap done: +${result.added}, existing ${result.already_tracked}, deleted ${result.deleted}`,
);
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to bootstrap tickers');
},
});
}
// ── Jobs ──
export function useJobs() {
return useQuery({
queryKey: ['admin', 'jobs'],
queryFn: () => adminApi.listJobs(),
refetchInterval: (query) => {
const jobs = (query.state.data ?? []) as adminApi.JobStatus[];
const hasRunning = jobs.some((job) => job.running);
return hasRunning ? 2_000 : 15_000;
},
});
}
export function usePipelineReadiness() {
return useQuery({
queryKey: ['admin', 'pipeline-readiness'],
queryFn: () => adminApi.getPipelineReadiness(),
refetchInterval: 20_000,
});
}
export function useToggleJob() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: ({ jobName, enabled }: { jobName: string; enabled: boolean }) =>
adminApi.toggleJob(jobName, enabled),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'jobs'] });
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to toggle job');
},
});
}
export function useTriggerJob() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (jobName: string) => adminApi.triggerJob(jobName),
onSuccess: (result) => {
qc.invalidateQueries({ queryKey: ['admin', 'jobs'] });
if (result.status === 'triggered') {
addToast('success', result.message || 'Job triggered successfully');
return;
}
addToast('info', result.message || 'Job could not be triggered');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to trigger job');
},
});
}
// ── Data Cleanup ──
export function useCleanupData() {
const { addToast } = useToast();
return useMutation({
mutationFn: (olderThanDays: number) => adminApi.cleanupData(olderThanDays),
onSuccess: (data) => {
addToast('success', (data as { message: string }).message || 'Cleanup completed');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to cleanup data');
},
});
}