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>
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import type { ScheduleConfig } from '../../lib/types';
|
|
import { useScheduleSettings, useUpdateScheduleSettings } from '../../hooks/useAdmin';
|
|
import { SkeletonTable } from '../ui/Skeleton';
|
|
|
|
const DEFAULTS: ScheduleConfig = {
|
|
schedule_timezone: 'Europe/Berlin',
|
|
schedule_daily_pipeline_cron: '0 7 * * *',
|
|
schedule_intraday_pipeline_cron: '0 14-22 * * 1-5',
|
|
schedule_fundamentals_cron: '0 4 * * 1',
|
|
};
|
|
|
|
const FIELDS: { key: keyof ScheduleConfig; label: string; hint: string; mono?: boolean }[] = [
|
|
{
|
|
key: 'schedule_timezone',
|
|
label: 'Timezone',
|
|
hint: 'IANA name, e.g. Europe/Berlin. All times below are in this zone.',
|
|
},
|
|
{
|
|
key: 'schedule_daily_pipeline_cron',
|
|
label: 'Daily pipeline (full)',
|
|
hint: 'OHLCV → sentiment → R:R scan → outcomes → regime. Default 07:00 so data is ready by 8.',
|
|
mono: true,
|
|
},
|
|
{
|
|
key: 'schedule_intraday_pipeline_cron',
|
|
label: 'Intraday pipeline (light)',
|
|
hint: 'Refresh prices + resolve outcomes. Default hourly across the US session, weekdays.',
|
|
mono: true,
|
|
},
|
|
{
|
|
key: 'schedule_fundamentals_cron',
|
|
label: 'Fundamentals (weekly)',
|
|
hint: 'Slow, rate-limited. Default early Monday so it finishes well before the day starts.',
|
|
mono: true,
|
|
},
|
|
];
|
|
|
|
export function ScheduleSettings() {
|
|
const { data, isLoading, isError, error } = useScheduleSettings();
|
|
const update = useUpdateScheduleSettings();
|
|
|
|
const [form, setForm] = useState<ScheduleConfig>(DEFAULTS);
|
|
|
|
useEffect(() => {
|
|
if (data) setForm(data);
|
|
}, [data]);
|
|
|
|
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 schedule'}</p>;
|
|
|
|
return (
|
|
<div className="glass p-5 space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-200">Pipeline Schedule</h3>
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
When the jobs run, as 5-field cron (<span className="num">min hour day month weekday</span>).
|
|
Saved changes apply to the running scheduler immediately — no redeploy. The big nightly run
|
|
does the full refresh; the light intraday run just keeps prices current.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{FIELDS.map((f) => (
|
|
<label key={f.key} className="block space-y-1">
|
|
<span className="text-xs text-gray-400">{f.label}</span>
|
|
<input
|
|
type="text"
|
|
value={form[f.key]}
|
|
spellCheck={false}
|
|
onChange={(e) => setForm((prev) => ({ ...prev, [f.key]: e.target.value }))}
|
|
className={`w-full input-glass px-3 py-2 text-sm ${f.mono ? 'num' : ''}`}
|
|
/>
|
|
<span className="block text-[11px] text-gray-600">{f.hint}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
className="btn-primary px-4 py-2 text-sm"
|
|
onClick={() => update.mutate(form)}
|
|
disabled={update.isPending}
|
|
>
|
|
{update.isPending ? 'Saving…' : 'Save Schedule'}
|
|
</button>
|
|
<button
|
|
className="px-4 py-2 text-sm rounded border border-white/[0.1] text-gray-300 hover:text-white"
|
|
onClick={() => {
|
|
setForm(DEFAULTS);
|
|
update.mutate(DEFAULTS);
|
|
}}
|
|
disabled={update.isPending}
|
|
>
|
|
Reset to Defaults
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|