feat: add selectable daily backtest cadence
This commit is contained in:
@@ -201,9 +201,11 @@ export interface TriggerJobResponse {
|
||||
status: 'triggered' | 'busy' | 'blocked' | 'not_found';
|
||||
message: string;
|
||||
target_model?: BacktestTargetModel;
|
||||
cadence?: BacktestCadence;
|
||||
}
|
||||
|
||||
export type BacktestTargetModel = 'production_gtl' | 'structural_sr';
|
||||
export type BacktestCadence = 'weekly' | 'daily';
|
||||
|
||||
export function listJobs() {
|
||||
return apiClient.get<JobStatus[]>('admin/jobs').then((r) => r.data);
|
||||
@@ -219,7 +221,10 @@ export function toggleJob(jobName: string, enabled: boolean) {
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function triggerJob(jobName: string, options?: { target_model?: BacktestTargetModel }) {
|
||||
export function triggerJob(
|
||||
jobName: string,
|
||||
options?: { target_model?: BacktestTargetModel; cadence?: BacktestCadence },
|
||||
) {
|
||||
return apiClient
|
||||
.post<TriggerJobResponse>(`admin/jobs/${jobName}/trigger`, options)
|
||||
.then((r) => r.data);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useBacktestReport } from '../../hooks/useMarketRegime';
|
||||
import { triggerJob } from '../../api/admin';
|
||||
import type { BacktestTargetModel } from '../../api/admin';
|
||||
import type { BacktestCadence, BacktestTargetModel } from '../../api/admin';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Callout } from '../ui/Callout';
|
||||
import { Disclosure } from '../ui/Disclosure';
|
||||
@@ -145,6 +145,7 @@ export function BacktestPanel() {
|
||||
const [selectedStrategy, setSelectedStrategy] = useState('');
|
||||
const [selectedLookback, setSelectedLookback] = useState('');
|
||||
const [targetModel, setTargetModel] = useState<BacktestTargetModel>('production_gtl');
|
||||
const [cadence, setCadence] = useState<BacktestCadence>('weekly');
|
||||
|
||||
const monitor = report?.portfolio_monitor ?? null;
|
||||
const activeStrategy =
|
||||
@@ -161,11 +162,11 @@ export function BacktestPanel() {
|
||||
);
|
||||
|
||||
const run = useMutation({
|
||||
mutationFn: () => triggerJob('backtest', { target_model: targetModel }),
|
||||
mutationFn: () => triggerJob('backtest', { target_model: targetModel, cadence }),
|
||||
onSuccess: (res) => {
|
||||
if (res.status === 'triggered') {
|
||||
const label = targetModel === 'production_gtl' ? 'Live GTL' : 'Structural S/R comparison';
|
||||
toast.addToast('success', `${label} backtest started — results appear when it finishes.`);
|
||||
toast.addToast('success', `${label} ${cadence} backtest started — results appear when it finishes.`);
|
||||
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['backtest-report'] }), 8000);
|
||||
} else {
|
||||
toast.addToast('info', res.message || 'Could not start backtest');
|
||||
@@ -180,7 +181,7 @@ export function BacktestPanel() {
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<Disclosure summary="How this is measured">
|
||||
<p className="max-w-2xl text-xs text-gray-400">
|
||||
The backtest replays the current config weekly through history — at each point the setup is
|
||||
The backtest replays the current config at the selected cadence — at each point the setup is
|
||||
rebuilt using only data up to that day (no lookahead) and the following ~30 trading days decide
|
||||
its outcome — then simulates one capital-constrained book against the S&P 500. Sentiment and
|
||||
fundamentals are held neutral (no point-in-time history). ~6 months is roughly one market regime,
|
||||
@@ -238,6 +239,56 @@ export function BacktestPanel() {
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className="grid w-full grid-cols-2 gap-2 sm:w-[34rem]">
|
||||
<legend className="mb-1 text-[11px] font-medium uppercase tracking-wider text-gray-500">
|
||||
Entry cadence
|
||||
</legend>
|
||||
<label
|
||||
className={`cursor-pointer rounded-lg border px-3 py-2 transition-colors focus-within:ring-2 focus-within:ring-blue-400/60 ${
|
||||
cadence === 'weekly'
|
||||
? 'border-blue-400/60 bg-blue-500/10'
|
||||
: 'border-white/10 bg-white/[0.03] hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
className="sr-only"
|
||||
type="radio"
|
||||
name="backtest-cadence"
|
||||
value="weekly"
|
||||
checked={cadence === 'weekly'}
|
||||
onChange={() => setCadence('weekly')}
|
||||
/>
|
||||
<span className="flex items-center justify-between gap-2 text-sm font-medium text-gray-100">
|
||||
Weekly
|
||||
<span className="rounded-full border border-blue-400/40 bg-blue-400/10 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-widest text-blue-300">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1 block text-[11px] leading-4 text-gray-500">
|
||||
Resource-safe server run at five-session intervals.
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className={`cursor-pointer rounded-lg border px-3 py-2 transition-colors focus-within:ring-2 focus-within:ring-amber-400/60 ${
|
||||
cadence === 'daily'
|
||||
? 'border-amber-400/50 bg-amber-500/10'
|
||||
: 'border-white/10 bg-white/[0.03] hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
className="sr-only"
|
||||
type="radio"
|
||||
name="backtest-cadence"
|
||||
value="daily"
|
||||
checked={cadence === 'daily'}
|
||||
onChange={() => setCadence('daily')}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-200">Daily</span>
|
||||
<span className="mt-1 block text-[11px] leading-4 text-amber-300/80">
|
||||
Research run: roughly 5× the replay work; prefer the offline snapshot runner.
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<Button onClick={() => run.mutate()} loading={run.isPending} className="shrink-0">
|
||||
{run.isPending ? 'Starting…' : report ? 'Re-run backtest' : 'Run backtest'}
|
||||
</Button>
|
||||
@@ -257,7 +308,8 @@ export function BacktestPanel() {
|
||||
<>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Ran {timeAgo(report.generated_at)} · {report.tickers} tickers · {report.candidates} setups
|
||||
({report.qualified} qualified) · weekly cadence, {report.params.horizon_days}-day horizon
|
||||
({report.qualified} qualified) · {report.params.entry_cadence ?? 'weekly'} cadence,
|
||||
{' '}{report.params.horizon_days}-day horizon
|
||||
{report.params.cost_per_side_pct != null && (
|
||||
<> · net of {report.params.cost_per_side_pct}%/side costs</>
|
||||
)}
|
||||
|
||||
@@ -356,6 +356,7 @@ export interface BacktestPortfolioMonitorRun extends BacktestPortfolioPolicy {
|
||||
label: string;
|
||||
description: string;
|
||||
is_production: boolean;
|
||||
comparison_arm?: 'live_no_lockdown' | 'live_lockdown_5' | null;
|
||||
entry_variant: string;
|
||||
exit_policy: string;
|
||||
reentry_lockdown_sessions?: number;
|
||||
@@ -370,6 +371,7 @@ export interface BacktestPortfolioMonitor {
|
||||
label: string;
|
||||
description: string;
|
||||
is_production: boolean;
|
||||
comparison_arm?: 'live_no_lockdown' | 'live_lockdown_5' | null;
|
||||
reentry_lockdown_sessions?: number;
|
||||
}[];
|
||||
lookbacks: { lookback: string; label: string }[];
|
||||
@@ -404,6 +406,9 @@ export interface BacktestReport {
|
||||
qualified: number;
|
||||
params: {
|
||||
step_days: number;
|
||||
step_sessions?: number;
|
||||
entry_cadence?: 'weekly' | 'daily';
|
||||
signal_eval_cadence?: 'weekly';
|
||||
horizon_days: number;
|
||||
min_lookback: number;
|
||||
cost_per_side_pct?: number;
|
||||
|
||||
Reference in New Issue
Block a user