Files
easy_tdx_max/web-ui/src/components/CompareChart.vue
T
Justin Gu ff3476f83a feat(backtest): 回测结果对比页
选 2-4 个已完成的回测 task,叠加净值曲线 + 横向指标对比。

后端(最小增量):
- task_runner.list_recent(limit):返回最近 N 个任务摘要(LRU 倒序)
- GET /backtest/tasks?limit=20:任务摘要列表端点(不含完整 result)
- TaskSummary / TaskListResponse schema

前端(/compare 对比页):
- CompareView:左栏勾选已完成 task,右栏叠加对比
- CompareChart:多 task 净值叠加图(归一化为初始=1)
- CompareTable:多 task 指标横向对比表(8 项指标)
- 按 result 结构判断可对比性(仅单标的 BacktestResult 可对比)

复用现有 task_runner LRU 表,无新增存储。测试:823 passed(+2 列表端点)
2026-07-03 04:11:29 +08:00

87 lines
2.2 KiB
Vue

<script setup lang="ts">
// 多 task 净值叠加对比图(归一化为初始=1)。
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import echarts from '../echarts-setup'
import type { BacktestResult } from '../types'
const props = defineProps<{
items: Array<{ label: string; result: BacktestResult }>
}>()
const container = ref<HTMLDivElement>()
let chart: echarts.ECharts | null = null
function render() {
if (!container.value || props.items.length === 0) return
chart ??= echarts.init(container.value, 'dark')
chart.setOption(buildOption(), true)
}
function buildOption(): echarts.EChartsCoreOption {
const seriesData = props.items.map((item) => {
const ec = item.result.equity_curve
const initial = ec[0]?.total || 1
return {
name: item.label,
dates: ec.map((e) => e.datetime.slice(0, 10)),
values: ec.map((e) => e.total / initial),
}
})
const allDates = Array.from(new Set(seriesData.flatMap((s) => s.dates))).sort()
return {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
legend: { top: 0, data: seriesData.map((s) => s.name) },
grid: { left: '8%', right: '5%', top: 30, bottom: 50 },
xAxis: { type: 'category', data: allDates, boundaryGap: false },
yAxis: { type: 'value', scale: true, name: '归一化净值' },
dataZoom: [{ type: 'inside', start: 0, end: 100 }],
series: seriesData.map((s) => {
const valMap = new Map(s.dates.map((d, i) => [d, s.values[i]]))
let last = 1
const aligned = allDates.map((d) => {
const v = valMap.get(d)
if (v !== undefined) last = v
return last
})
return {
name: s.name,
type: 'line',
data: aligned,
symbol: 'none',
lineStyle: { width: 1.5 },
}
}),
}
}
function resize() {
chart?.resize()
}
onMounted(() => {
render()
window.addEventListener('resize', resize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
chart = null
})
watch(() => props.items, render)
</script>
<template>
<div ref="container" class="compare-chart"></div>
</template>
<style scoped>
.compare-chart {
width: 100%;
height: 380px;
}
</style>