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
+105 -2
View File
@@ -1,4 +1,107 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getStrategies, getParams, type StrategyItem } from '@/api/strategy'
import { submitCta } from '@/api/backtest'
const router = useRouter()
const strategies = ref<StrategyItem[]>([])
const loading = ref(false)
const submitting = ref(false)
const paramsList = ref<string[]>([])
const form = reactive({
strategy: '',
symbol: '600000',
start: '2024-01-01',
end: '2024-06-30',
})
const paramValues = reactive<Record<string, string>>({})
onMounted(async () => {
loading.value = true
try {
strategies.value = await getStrategies()
if (strategies.value.length && !form.strategy) {
form.strategy = strategies.value[0].name
}
} catch {
ElMessage.error('策略列表加载失败')
} finally {
loading.value = false
}
})
watch(() => form.strategy, async (name) => {
if (!name) return
try {
const p = await getParams(name)
paramsList.value = p.parameters
Object.keys(paramValues).forEach((k) => delete paramValues[k])
p.parameters.forEach((k) => {
paramValues[k] = String(p.defaults[k] ?? '')
})
} catch {
paramsList.value = []
}
})
async function onSubmit(): Promise<void> {
if (!form.strategy || !form.symbol) {
ElMessage.warning('请选择策略并填写标的')
return
}
submitting.value = true
try {
const params: Record<string, unknown> = {}
paramsList.value.forEach((k) => {
const raw = paramValues[k]
params[k] = raw !== '' && !isNaN(Number(raw)) ? Number(raw) : raw
})
const tid = await submitCta({
symbol: form.symbol,
strategy: form.strategy,
params,
start: form.start,
end: form.end,
})
ElMessage.success('回测已提交')
router.push(`/backtest/progress/${tid}`)
} catch {
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
</script>
<template>
<el-empty description="回测 - 新建(S1 切片实现)" />
<el-card v-loading="loading">
<template #header>
<h3>新建回测</h3>
</template>
<el-form :model="form" label-width="120px">
<el-form-item label="策略">
<el-select v-model="form.strategy" placeholder="选择策略" style="width: 280px">
<el-option v-for="s in strategies" :key="s.name" :label="s.name" :value="s.name" />
</el-select>
</el-form-item>
<el-form-item v-for="k in paramsList" :key="k" :label="k">
<el-input v-model="paramValues[k]" style="width: 220px" />
</el-form-item>
<el-form-item label="标的代码">
<el-input v-model="form.symbol" placeholder="如 600000(不带交易所后缀)" style="width: 220px" />
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="form.start" type="date" value-format="YYYY-MM-DD" style="width: 220px" />
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker v-model="form.end" type="date" value-format="YYYY-MM-DD" style="width: 220px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="onSubmit">提交回测</el-button>
</el-form-item>
</el-form>
</el-card>
</template>
+39 -2
View File
@@ -1,4 +1,41 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTask } from '@/composables/useTask'
const route = useRoute()
const router = useRouter()
const taskId = String(route.params.id)
const { status, stage, start } = useTask(taskId)
start()
watch(status, (s) => {
if (s === 'done') router.push(`/backtest/result/${taskId}`)
})
function pct(): number {
if (status.value === 'done') return 100
if (status.value === 'running') return 60
if (status.value === 'failed') return 100
return 20
}
</script>
<template>
<el-empty description="回测 - 进度(S1 切片实现)" />
<el-card>
<h3>回测进行中</h3>
<p>任务 ID{{ taskId }}</p>
<p>
状态
<el-tag :type="status === 'done' ? 'success' : status === 'failed' ? 'danger' : 'warning'">
{{ status }}
</el-tag>
</p>
<p>阶段{{ stage || '—' }}</p>
<el-progress
:percentage="pct()"
:status="status === 'failed' ? 'exception' : status === 'done' ? 'success' : undefined"
/>
</el-card>
</template>
+76 -2
View File
@@ -1,4 +1,78 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
getResult, getEquityCurve, getDailyPnl, getTrades, getKline,
type EquityPoint, type PnlPoint, type Trade, type KlineBar,
} from '@/api/backtest'
import EquityChart from '@/components/charts/EquityChart.vue'
import DailyPnlChart from '@/components/charts/DailyPnlChart.vue'
import KlineChart from '@/components/charts/KlineChart.vue'
import TradesTable from '@/components/TradesTable.vue'
const route = useRoute()
const taskId = String(route.params.id)
const loading = ref(true)
const statistics = ref<Record<string, unknown>>({})
const equity = ref<EquityPoint[]>([])
const pnl = ref<PnlPoint[]>([])
const trades = ref<Trade[]>([])
const kline = ref<KlineBar[]>([])
const statEntries = computed(() =>
Object.entries(statistics.value)
.map(([k, v]) => ({
key: k,
value: typeof v === 'number' ? Math.round(v * 10000) / 10000 : v,
}))
)
onMounted(async () => {
try {
const info = await getResult(taskId)
statistics.value = info.statistics || {}
const [eq, p, tr] = await Promise.all([
getEquityCurve(taskId), getDailyPnl(taskId), getTrades(taskId),
])
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 = []
}
}
} finally {
loading.value = false
}
})
</script>
<template>
<el-empty description="回测 - 结果(S1 切片实现)" />
<div v-loading="loading">
<el-card>
<template #header><h3>统计指标</h3></template>
<el-descriptions :column="4" border>
<el-descriptions-item v-for="e in statEntries" :key="e.key" :label="e.key">
{{ e.value }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-row :gutter="16" style="margin-top: 16px">
<el-col :span="12"><el-card><EquityChart :data="equity" /></el-card></el-col>
<el-col :span="12"><el-card><DailyPnlChart :data="pnl" /></el-card></el-col>
</el-row>
<el-card style="margin-top: 16px">
<KlineChart :kline="kline" :trades="trades" />
</el-card>
<el-card style="margin-top: 16px">
<template #header><h3>成交记录</h3></template>
<TradesTable :trades="trades" />
</el-card>
</div>
</template>