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