Finalize GTL and retire S/R research harness

This commit is contained in:
2026-07-13 17:58:04 +02:00
parent 9d362bd568
commit bee5a5ce89
35 changed files with 374 additions and 5385779 deletions
+5 -2
View File
@@ -200,8 +200,11 @@ export interface TriggerJobResponse {
job: string;
status: 'triggered' | 'busy' | 'blocked' | 'not_found';
message: string;
target_model?: BacktestTargetModel;
}
export type BacktestTargetModel = 'production_gtl' | 'structural_sr';
export function listJobs() {
return apiClient.get<JobStatus[]>('admin/jobs').then((r) => r.data);
}
@@ -216,9 +219,9 @@ export function toggleJob(jobName: string, enabled: boolean) {
.then((r) => r.data);
}
export function triggerJob(jobName: string) {
export function triggerJob(jobName: string, options?: { target_model?: BacktestTargetModel }) {
return apiClient
.post<TriggerJobResponse>(`admin/jobs/${jobName}/trigger`)
.post<TriggerJobResponse>(`admin/jobs/${jobName}/trigger`, options)
.then((r) => r.data);
}
@@ -2,6 +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 { Button } from '../ui/Button';
import { Callout } from '../ui/Callout';
import { Disclosure } from '../ui/Disclosure';
@@ -143,6 +144,7 @@ export function BacktestPanel() {
const toast = useToast();
const [selectedStrategy, setSelectedStrategy] = useState('');
const [selectedLookback, setSelectedLookback] = useState('');
const [targetModel, setTargetModel] = useState<BacktestTargetModel>('production_gtl');
const monitor = report?.portfolio_monitor ?? null;
const activeStrategy =
@@ -159,10 +161,11 @@ export function BacktestPanel() {
);
const run = useMutation({
mutationFn: () => triggerJob('backtest'),
mutationFn: () => triggerJob('backtest', { target_model: targetModel }),
onSuccess: (res) => {
if (res.status === 'triggered') {
toast.addToast('success', 'Backtest started — results appear when it finishes (a minute or two).');
const label = targetModel === 'production_gtl' ? 'Live GTL' : 'Structural S/R comparison';
toast.addToast('success', `${label} backtest started — results appear when it finishes.`);
setTimeout(() => queryClient.invalidateQueries({ queryKey: ['backtest-report'] }), 8000);
} else {
toast.addToast('info', res.message || 'Could not start backtest');
@@ -184,9 +187,61 @@ export function BacktestPanel() {
so read it as directional.
</p>
</Disclosure>
<Button onClick={() => run.mutate()} loading={run.isPending} className="shrink-0">
{run.isPending ? 'Starting…' : report ? 'Re-run backtest' : 'Run backtest'}
</Button>
<div className="flex w-full flex-col gap-3 sm:w-auto sm:items-end">
<fieldset className="grid w-full grid-cols-1 gap-2 sm:w-[34rem] sm:grid-cols-2">
<legend className="mb-1 text-[11px] font-medium uppercase tracking-wider text-gray-500">
Target model for this run
</legend>
<label
className={`cursor-pointer rounded-lg border px-3 py-2 transition-colors focus-within:ring-2 focus-within:ring-blue-400/60 ${
targetModel === 'production_gtl'
? '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-target-model"
value="production_gtl"
checked={targetModel === 'production_gtl'}
onChange={() => setTargetModel('production_gtl')}
/>
<span className="flex items-center justify-between gap-2 text-sm font-medium text-gray-100">
Live GTL
<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">
Production
</span>
</span>
<span className="mt-1 block text-[11px] leading-4 text-gray-500">
Exact target path used by the live scanner and scheduled backtest.
</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 ${
targetModel === 'structural_sr'
? '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-target-model"
value="structural_sr"
checked={targetModel === 'structural_sr'}
onChange={() => setTargetModel('structural_sr')}
/>
<span className="text-sm font-medium text-gray-200">Structural S/R</span>
<span className="mt-1 block text-[11px] leading-4 text-gray-500">
Comparison only; uses chart structure as the target source.
</span>
</label>
</fieldset>
<Button onClick={() => run.mutate()} loading={run.isPending} className="shrink-0">
{run.isPending ? 'Starting…' : report ? 'Re-run backtest' : 'Run backtest'}
</Button>
</div>
</div>
{isLoading && <Callout variant="empty">Loading</Callout>}
@@ -206,6 +261,10 @@ export function BacktestPanel() {
{report.params.cost_per_side_pct != null && (
<> · net of {report.params.cost_per_side_pct}%/side costs</>
)}
{' '}· target model:{' '}
<span className={report.params.is_production_target_model === false ? 'text-amber-300' : 'text-blue-300'}>
{report.params.target_model_label ?? 'Legacy report (model not recorded)'}
</span>
</p>
{monitor && monitorRun ? (
+3
View File
@@ -399,6 +399,9 @@ export interface BacktestReport {
horizon_days: number;
min_lookback: number;
cost_per_side_pct?: number;
target_model?: 'production_gtl' | 'structural_sr';
target_model_label?: string;
is_production_target_model?: boolean;
};
overall_qualified: BacktestBucket;
overall_all: BacktestBucket;