feat(frontend): S1 回测核心页(新建/进度/结果 + ECharts 图表)
- api/strategy.ts、api/backtest.ts(含类型) - 回测-新建(策略下拉+动态参数表单+日期+提交) - useTask 组合式(轮询+WS 实时阶段)+ 进度页 - 结果页:统计全表 + 资金曲线 + 每日盈亏(红涨绿跌) + 成交表 + K线买卖点 - build 通过;result 接口扩 symbol/start/end 供 K线调用
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user