feat(frontend): S1 回测核心页(新建/进度/结果 + ECharts 图表)

- api/strategy.ts、api/backtest.ts(含类型)
- 回测-新建(策略下拉+动态参数表单+日期+提交)
- useTask 组合式(轮询+WS 实时阶段)+ 进度页
- 结果页:统计全表 + 资金曲线 + 每日盈亏(红涨绿跌) + 成交表 + K线买卖点
- build 通过;result 接口扩 symbol/start/end 供 K线调用
This commit is contained in:
2026-07-07 06:13:10 +08:00
parent 3a0e75fdc1
commit 80d7f58589
11 changed files with 531 additions and 7 deletions
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { Trade } from '@/api/backtest'
defineProps<{ trades: Trade[] }>()
</script>
<template>
<el-table :data="trades" stripe size="small" empty-text="无成交">
<el-table-column prop="datetime" label="时间" width="220" />
<el-table-column prop="direction" label="方向" width="80" />
<el-table-column prop="offset" label="开平" width="80" />
<el-table-column prop="price" label="价格" width="100" />
<el-table-column prop="volume" label="数量" width="100" />
<el-table-column prop="vt_symbol" label="标的" />
</el-table>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import type { PnlPoint } from '@/api/backtest'
const props = defineProps<{ data: PnlPoint[] }>()
const el = ref<HTMLDivElement>()
let chart: echarts.ECharts | null = null
function render(): void {
if (!chart || !props.data.length) return
chart.setOption({
title: { text: '每日盈亏', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: props.data.map((p) => p.date) },
yAxis: { type: 'value', name: '盈亏' },
series: [{
type: 'bar',
// A 股惯例:红涨绿跌
data: props.data.map((p) => ({ value: p.pnl, itemStyle: { color: p.pnl >= 0 ? '#ee6666' : '#91cc75' } })),
}],
}, true)
}
function resize(): void { chart?.resize() }
onMounted(() => {
if (el.value) chart = echarts.init(el.value)
render()
window.addEventListener('resize', resize)
})
watch(() => props.data, render, { deep: true })
onUnmounted(() => { window.removeEventListener('resize', resize); chart?.dispose() })
</script>
<template><div ref="el" class="chart-box" /></template>
<style scoped>.chart-box { width: 100%; height: 280px; }</style>
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import type { EquityPoint } from '@/api/backtest'
const props = defineProps<{ data: EquityPoint[] }>()
const el = ref<HTMLDivElement>()
let chart: echarts.ECharts | null = null
function render(): void {
if (!chart || !props.data.length) return
chart.setOption({
title: { text: '资金曲线', left: 'center' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: props.data.map((p) => p.date) },
yAxis: { type: 'value', scale: true, name: '权益' },
series: [{
type: 'line', name: '权益', smooth: true, areaStyle: { opacity: 0.1 },
data: props.data.map((p) => p.balance),
}],
}, true)
}
function resize(): void { chart?.resize() }
onMounted(() => {
if (el.value) chart = echarts.init(el.value)
render()
window.addEventListener('resize', resize)
})
watch(() => props.data, render, { deep: true })
onUnmounted(() => { window.removeEventListener('resize', resize); chart?.dispose() })
</script>
<template><div ref="el" class="chart-box" /></template>
<style scoped>.chart-box { width: 100%; height: 320px; }</style>
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
import type { KlineBar, Trade } from '@/api/backtest'
const props = defineProps<{ kline: KlineBar[]; trades: Trade[] }>()
const el = ref<HTMLDivElement>()
let chart: echarts.ECharts | null = null
function dateOf(dt: string): string {
return String(dt).slice(0, 10)
}
function render(): void {
if (!chart || !props.kline.length) return
const dates = props.kline.map((k) => dateOf(k.datetime))
const markPoints = props.trades
.map((t) => ({ t, idx: dates.indexOf(dateOf(t.datetime)) }))
.filter((x) => x.idx >= 0)
.map(({ t }) => ({
coord: [dateOf(t.datetime), t.price],
value: `${t.offset === '开' ? '买' : '卖'}${t.volume}`,
itemStyle: { color: t.offset === '开' ? '#ee6666' : '#91cc75' },
symbol: 'triangle',
symbolSize: 14,
}))
chart.setOption({
title: { text: 'K线 + 买卖点', left: 'center' },
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
xAxis: { type: 'category', data: dates, scale: true, boundaryGap: false },
yAxis: { scale: true },
series: [{
type: 'candlestick',
// ECharts order: [open, close, lowest, highest]
data: props.kline.map((k) => [k.open, k.close, k.low, k.high]),
markPoint: { data: markPoints, symbol: 'triangle', symbolSize: 14 },
}],
}, true)
}
function resize(): void { chart?.resize() }
onMounted(() => {
if (el.value) chart = echarts.init(el.value)
render()
window.addEventListener('resize', resize)
})
watch(() => [props.kline, props.trades], render, { deep: true })
onUnmounted(() => { window.removeEventListener('resize', resize); chart?.dispose() })
</script>
<template><div ref="el" class="chart-box" /></template>
<style scoped>.chart-box { width: 100%; height: 420px; }</style>