From 95c918c84155dd0b16ad1311a1a9022032cf4c76 Mon Sep 17 00:00:00 2001 From: shy3130 Date: Mon, 13 Jul 2026 18:54:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(walkforward):=20=E6=B1=87=E6=80=BB=E6=95=B0?= =?UTF-8?q?=E5=80=BC=20NaN/Infinity=20=E6=B8=B2=E6=9F=93=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #104 审查发现 StrategyWalkForward 的汇总卡和净值曲线在后端返回 NaN/Infinity (零成交折等边界) 时会渲染出 "NaN" 字样或空白 SVG: - OosEquityChart: vals 含 NaN → lo/hi/span 全 NaN → SVG 路径空白, 页脚 last.toFixed(4) 显示 "NaN" - IS→OOS 退化 / 目标均值: degradation != null 对 NaN 为 true, toFixed(3) 显示 "NaN"; 颜色判断 (NaN > 0 = false) 方向也错 修复: 加 fin() 把非有限值归一为 null, JSX 外用 const 取出归一值 (TS 据此收窄类型), 下游统一走 '—' 降级。复利收益/一致性/OOS 收益 本就走 NaN-safe 的 fmtPct, 无需改。 前端 tsc -b + vite build 通过。后端未改动。 --- .../pages/backtest/StrategyWalkForward.tsx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/backtest/StrategyWalkForward.tsx b/frontend/src/pages/backtest/StrategyWalkForward.tsx index cb9a205..c154bcf 100644 --- a/frontend/src/pages/backtest/StrategyWalkForward.tsx +++ b/frontend/src/pages/backtest/StrategyWalkForward.tsx @@ -36,11 +36,17 @@ function Stat({ label, value, hint, color }: { label: string; value: string; hin ) } +/** 把 NaN/Infinity 归一为 null (走 ?! '—' 的既有降级路径), 防止 toFixed 渲染出 "NaN"。 */ +function fin(v: number | null | undefined): number | null { + return v != null && Number.isFinite(v) ? v : null +} + /** OOS 拼接净值曲线 (逐折复利) — walk-forward 核心产出的极简 SVG 折线。 */ function OosEquityChart({ curve }: { curve: { fold: number; date: string; value: number }[] }) { - if (!curve.length) return null + // 过滤非有限值: 零成交折等边界可能产出 NaN/Infinity, 混入会让坐标全 NaN → SVG 空白。 + const vals = curve.map(p => p.value).filter(v => Number.isFinite(v)) + if (!vals.length) return null const W = 600, H = 120, pad = 8 - const vals = curve.map(p => p.value) const lo = Math.min(1, ...vals), hi = Math.max(1, ...vals) const span = hi - lo || 1 // 起点补一个 value=1 基准, 让曲线从 1.0 起步 @@ -112,6 +118,12 @@ export function StrategyWalkForward() { const result = task?.result const progress = task?.progress const summary = result?.summary + // 汇总里的可空数值先归一: 零成交折等边界可能产出 NaN/Infinity, + // 直接 toFixed 会渲染出 "NaN"; 这里把非有限值转 null, 下游统一走 '—' 降级。 + const compounded = fin(summary?.compounded_oos_return) + const degradation = fin(summary?.degradation) + const avgIs = fin(summary?.avg_is_objective) + const avgOos = fin(summary?.avg_oos_objective) return (
@@ -210,20 +222,20 @@ export function StrategyWalkForward() { {result && summary && result.n_folds > 0 && (
- {/* 汇总卡 */} + {/* 汇总卡 — 可空数值已在上游用 fin() 归一, NaN/Infinity 走 '—' 而非渲染 "NaN" */}
- = 0 ? '#34d399' : '#f87171'} /> + = 0 ? '#34d399' : '#f87171'} /> 0 ? '样本外退化=过拟合' : '样本外未退化'} - color={summary.degradation != null && summary.degradation > 0 ? '#f87171' : '#34d399'} /> + value={degradation != null ? degradation.toFixed(3) : '—'} + hint={degradation != null && degradation > 0 ? '样本外退化=过拟合' : '样本外未退化'} + color={degradation != null && degradation > 0 ? '#f87171' : '#34d399'} /> 0 ? `${result.n_folds} (跳过${result.n_skipped})` : String(result.n_folds)} />
- IS 目标均值 {summary.avg_is_objective ?? '—'} · OOS 目标均值 {summary.avg_oos_objective ?? '—'} · 耗时 {(result.elapsed_ms / 1000).toFixed(1)}s + IS 目标均值 {avgIs != null ? avgIs.toFixed(3) : '—'} · OOS 目标均值 {avgOos != null ? avgOos.toFixed(3) : '—'} · 耗时 {(result.elapsed_ms / 1000).toFixed(1)}s
{/* OOS 拼接净值曲线 (walk-forward 核心产出) */} @@ -269,10 +281,10 @@ export function StrategyWalkForward() {
- {summary.degradation != null && summary.degradation > 0 && ( + {degradation != null && degradation > 0 && (
- 样本外目标较样本内退化 {summary.degradation.toFixed(3)},提示参数可能过拟合训练区间。 + 样本外目标较样本内退化 {degradation.toFixed(3)},提示参数可能过拟合训练区间。
)}