feat(mining): 环境数据覆盖不足时弹窗补算并自动重跑挖矿

This commit is contained in:
shy3130
2026-09-11 12:07:15 +08:00
parent a90c12b3a5
commit ea4d8a8278
@@ -58,6 +58,23 @@ const PROFILE_LABELS: Record<MiningBudgetProfile, string> = {
balanced: '均衡档',
strict: '严格档',
}
// 市场环境覆盖不足的失败文案 (regime_alignment 校验抛出), 命中时弹「补算并重跑」确认。
const REGIME_COVERAGE_ERROR_RE = /市场环境数据覆盖不完整|市场环境数据为空/
const REGIME_FIRST_MISSING_RE = /缺少前一交易日环境\s*(\d{4}-\d{2}-\d{2})/
/** run.request 带 auto/auto_screening 附加字段, 而 start 接口 extra="forbid", 重跑前剥回纯 MiningRequestV1。 */
function stripMiningRequestExtras(request: MiningRun['request']): MiningRequestV1 {
const {
factor_names, strategy_ids, symbols, asset_type, start, end, budget_profile,
commission_pct, stamp_tax_pct, slippage_bps, correlation_threshold,
max_combination_factors, beam_width, max_finalists, force,
} = request
return {
factor_names, strategy_ids, symbols, asset_type, start, end, budget_profile,
commission_pct, stamp_tax_pct, slippage_bps, correlation_threshold,
max_combination_factors, beam_width, max_finalists, force,
}
}
interface MiningDraft {
assetType: 'stock' | 'etf'
@@ -606,6 +623,43 @@ export function MiningWorkbench() {
onError: error => toast(`保存失败 · ${String((error as Error).message || error)}`, 'error'),
})
// 市场环境覆盖不足: 挖矿内部 T-1 环境校验 (fail-closed) 失败时弹「补算并重跑」确认。
// 确定 → 调 regime/recompute 补算缺失区间后自动重跑同一 payload; 取消 → 仅关闭不动作。
const [regimeBackfill, setRegimeBackfill] = useState<{ payload: MiningRequestV1; start?: string } | null>(null)
const regimeErrorHandled = useRef('')
useEffect(() => {
if (task.isPending) {
regimeErrorHandled.current = ''
return
}
if (!task.error || !REGIME_COVERAGE_ERROR_RE.test(task.error)) return
if (regimeErrorHandled.current === task.error) return
const request = task.run?.request
if (!request) return
regimeErrorHandled.current = task.error
setRegimeBackfill({
payload: stripMiningRequestExtras(request),
start: REGIME_FIRST_MISSING_RE.exec(task.error)?.[1],
})
}, [task.error, task.isPending, task.run])
const regimeRecomputeAndRun = useMutation({
mutationFn: async (job: { payload: MiningRequestV1; start?: string }) => {
// start 缺省(环境数据为空)时后端从 enriched 最早日全量重算; 有 start 则只补缺口区间
const recomputed = await api.regimeRecompute(job.start)
return { recomputed, job }
},
onSuccess: ({ recomputed, job }) => {
for (const key of [QK.regimeLatest, QK.regimeCoverage, QK.regimeHistory()]) {
queryClient.invalidateQueries({ queryKey: key })
}
toast(recomputed.computed > 0 ? `环境补算完成 · 新增 ${recomputed.computed} 天,重新开始挖掘` : '环境数据已是最新,重新开始挖掘', 'success')
setRegimeBackfill(null)
submitMining(job.payload)
},
onError: error => toast(`环境补算失败 · ${String((error as Error).message || error)}`, 'error'),
})
const regimeBackdrop = useDialogBackdrop(() => setRegimeBackfill(null), () => !regimeRecomputeAndRun.isPending)
const attachRun = (run: MiningRun) => {
const params = new URLSearchParams(searchParams)
params.set('run', run.run_id)
@@ -814,6 +868,30 @@ export function MiningWorkbench() {
</div>
</div>
)}
{/* 市场环境覆盖不足的补救确认: 补算缺失区间后自动重跑, 取消则不做任何操作。 */}
{regimeBackfill && (
<div {...regimeBackdrop} className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div onClick={e => e.stopPropagation()} className="w-full max-w-sm rounded-2xl border border-border bg-surface p-5 shadow-2xl">
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
<Database className="h-4 w-4 shrink-0 text-warning" />
</div>
<p className="mt-2 text-xs leading-5 text-secondary">
{regimeBackfill.start
? <> <span className="font-mono text-foreground">{regimeBackfill.start}</span> T-1 </>
: '尚未计算市场环境数据。点击确定将执行全量计算(历史较长时耗时较久),完成后重新开始挖掘。'}
</p>
<div className="mt-4 flex justify-end gap-2">
<button type="button" disabled={regimeRecomputeAndRun.isPending} onClick={() => setRegimeBackfill(null)} className="h-8 rounded-btn border border-border px-3 text-xs text-secondary hover:bg-elevated disabled:opacity-50"></button>
<button type="button" disabled={regimeRecomputeAndRun.isPending} onClick={() => regimeRecomputeAndRun.mutate(regimeBackfill)} className="inline-flex h-8 items-center gap-1.5 rounded-btn bg-accent px-3 text-xs font-semibold text-white hover:opacity-90 disabled:opacity-50">
{regimeRecomputeAndRun.isPending ? <LoaderCircle className="h-3.5 w-3.5 animate-spin" /> : <Database className="h-3.5 w-3.5" />}
</button>
</div>
</div>
</div>
)}
</div>
)
}