diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 171d415..f55cb0f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2560,7 +2560,8 @@ export const api = { if (start) params.set('start', start) if (end) params.set('end', end) const qs = params.toString() - return request<{ ok: boolean; computed: number; phase_days?: number; mainline_rows?: number }>(`/api/regime/recompute${qs ? `?${qs}` : ''}`, { method: 'POST' }) + // 补算需扫 enriched 全市场数据, 大区间耗时超过默认超时, 放宽到 5 分钟 + return request<{ ok: boolean; computed: number; phase_days?: number; mainline_rows?: number }>(`/api/regime/recompute${qs ? `?${qs}` : ''}`, { method: 'POST', timeoutMs: 300_000 }) }, regimePhases: (start?: string, end?: string) => { const params = new URLSearchParams() diff --git a/frontend/src/pages/backtest/StrategyBacktest.tsx b/frontend/src/pages/backtest/StrategyBacktest.tsx index 16c0fc0..e285822 100644 --- a/frontend/src/pages/backtest/StrategyBacktest.tsx +++ b/frontend/src/pages/backtest/StrategyBacktest.tsx @@ -208,6 +208,31 @@ const ADVANCED_TABS: { id: AdvancedSettingsTab; label: string }[] = [ { id: 'range', label: '回测范围' }, ] const toSignalId = (sig: string) => (sig.startsWith('signal_') || sig.startsWith('csg_')) ? sig : `signal_${sig}` + +/** 环境数据缺口的一键补算入口: 补算区间后自动重跑回测 */ +function RegimeRecomputeButton({ from, to, pending, onClick }: { + from: string + to?: string + pending: boolean + onClick: () => void +}) { + return ( +
+ + + 将补算 {from} ~ {to || '今天'} 的市场环境数据,完成后自动重跑本次回测 + +
+ ) +} + const numOrNull = (v: string) => v === '' || Number.isNaN(Number(v)) ? null : Number(v) const clamp = (v: number, min?: number, max?: number) => { let next = v @@ -1204,6 +1229,27 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: { }) } + // 环境数据缺口识别: fail-closed 报错文案中提取缺失首日, 供"一键补算并重跑"。 + // 「数据为空」变体无日期, 用回测起点兜底; 预热区间不足的变体不在此处理(文案已自说明)。 + const regimeGapStart = useMemo(() => { + const msg = backtestTask?.error ?? result?.error ?? '' + if (msg.includes('市场环境数据覆盖不完整') && msg.includes('请先补算')) { + const m = msg.match(/缺少前一交易日环境[::\s]*(\d{4}-\d{2}-\d{2})/) + return m?.[1] ?? start + } + if (msg.includes('市场环境数据为空')) return start + return null + }, [backtestTask?.error, result?.error, start]) + + const regimeRecompute = useMutation({ + mutationFn: () => api.regimeRecompute(regimeGapStart ?? undefined, end || undefined), + onSuccess: data => { + toast(`环境数据补算完成 (新增 ${data.computed} 天),自动重新回测`, 'success') + handleRun() + }, + onError: e => toast(`环境数据补算失败 · ${String((e as Error)?.message || e)}`, 'error'), + }) + // 提取统计 const s = result?.stats const pick = (...keys: string[]) => { @@ -2022,6 +2068,14 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: { {result?.error && (
{result.error} + {regimeGapStart && ( + regimeRecompute.mutate()} + /> + )}
)} @@ -2031,6 +2085,14 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: { {result && (
本次回测未生成新结果,下方仍展示上一次成功结果。
)} + {regimeGapStart && ( + regimeRecompute.mutate()} + /> + )} )}