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()}
+ />
+ )}
)}