c34f3cb1a4
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>
359 lines
10 KiB
TypeScript
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');
|
|
},
|
|
});
|
|
}
|