fix(backtest): 结果页垃圾值/无图表端到端修复(empyrical×numpy2.0根因)

根因: empyrical 0.5.5 引用 numpy2.0 已移除的 np.NINF → compute_metrics 静默崩 → _metrics.json 不生成 → 结果页回退 vnpy 原始字段(单位混乱: total_return当百分数、max_drawdown当元 → 前端×100显 3305%/-50M%)。
- metrics.py: 导入 empyrical 前补回 np 别名(NINF/Inf/PINF/NaN/NAN/infty)
- routes.py: benchmark-curve/risk-series 缺 metrics 文件时返空200(不再404拖垮整页); get_result 从 statistics 抽 relative_metrics
- cta_engine.py: bench_df 日期 strip tz 防 pct_change 崩; metrics 块加 traceback 日志
- Result.vue: onMounted 用 Promise.allSettled 隔离7端点, 单接口失败不拖垮整页
- result_store.py: _safe_read_json 容错迁移后残留 NAS 绝对路径, stale path 不崩 list_results
- datareader.py: read_index_daily 改从 vnpy DB 读 + 前缀解析交易所(sh→SSE, 避免 000300 被 guess_exchange 误判 SZSE)
This commit is contained in:
2026-07-17 08:24:01 +08:00
parent 37c850d0c5
commit 7fe3fb0844
6 changed files with 159 additions and 66 deletions
+67 -32
View File
@@ -110,40 +110,75 @@ const filteredBenchmarkCurve = computed(() => filterDataByTimeRange(benchmarkCur
const filteredRiskSeries = computed(() => filterDataByTimeRange(riskSeries.value))
onMounted(async () => {
try {
const [info, relMetrics, benchCurve, riskSer, eq, p, tr] = await Promise.all([
getResult(taskId),
getRelativeMetrics(taskId),
getBenchmarkCurve(taskId),
getRiskSeries(taskId),
getEquityCurve(taskId),
getDailyPnl(taskId),
getTrades(taskId),
])
// 请求隔离:任一接口失败(如 benchmark-curve/risk-series 数据缺失)不得阻塞
// 其余请求。statistics/equity/trades 有数据时必须正常渲染。用 Promise.allSettled
// 保留并发,逐个取值,失败项保留默认空值 + console.warn。
const settled = await Promise.allSettled([
getResult(taskId),
getRelativeMetrics(taskId),
getBenchmarkCurve(taskId),
getRiskSeries(taskId),
getEquityCurve(taskId),
getDailyPnl(taskId),
getTrades(taskId),
])
const [rInfo, rRel, rBench, rRisk, rEq, rPnl, rTr] = settled
statistics.value = info.statistics || {}
relativeMetrics.value = relMetrics
benchmarkCurve.value = benchCurve
riskSeries.value = riskSer
equity.value = eq
pnl.value = p
trades.value = tr
if (info.symbol && info.start && info.end) {
try {
kline.value = await getKline(info.symbol, info.start, info.end)
} catch {
kline.value = []
}
}
try {
logText.value = await getLog(taskId)
} catch {
logText.value = ''
}
} finally {
loading.value = false
if (rInfo.status === 'fulfilled') {
statistics.value = rInfo.value.statistics || {}
} else {
console.warn('[Result] getResult failed:', rInfo.reason)
}
if (rRel.status === 'fulfilled') {
relativeMetrics.value = rRel.value
} else {
console.warn('[Result] getRelativeMetrics failed:', rRel.reason)
}
if (rBench.status === 'fulfilled') {
benchmarkCurve.value = rBench.value
} else {
console.warn('[Result] getBenchmarkCurve failed:', rBench.reason)
}
if (rRisk.status === 'fulfilled') {
riskSeries.value = rRisk.value
} else {
console.warn('[Result] getRiskSeries failed:', rRisk.reason)
}
if (rEq.status === 'fulfilled') {
equity.value = rEq.value
} else {
console.warn('[Result] getEquityCurve failed:', rEq.reason)
}
if (rPnl.status === 'fulfilled') {
pnl.value = rPnl.value
} else {
console.warn('[Result] getDailyPnl failed:', rPnl.reason)
}
if (rTr.status === 'fulfilled') {
trades.value = rTr.value
} else {
console.warn('[Result] getTrades failed:', rTr.reason)
}
// kline 依赖 getResult 返回的 symbol/start/end,单独隔离
const info = rInfo.status === 'fulfilled' ? rInfo.value : null
if (info?.symbol && info?.start && info?.end) {
try {
kline.value = await getKline(info.symbol, info.start, info.end)
} catch (e) {
console.warn('[Result] getKline failed:', e)
kline.value = []
}
}
try {
logText.value = await getLog(taskId)
} catch (e) {
console.warn('[Result] getLog failed:', e)
logText.value = ''
}
loading.value = false
})
// 每日收益格式化:浮点精度 → 2 位;日期去 00:00:00