fix(portfolio): 历史组合结果可查看(PortfolioBacktest查看模式+History跳转/筛选/标签); 回测等待加已耗时提示(治卡顿错觉) [nas]
CI/CD / test (push) Successful in 10s
CI/CD / nas-deploy (push) Successful in 24s
CI/CD / nas-verify (push) Successful in 8s

This commit is contained in:
2026-08-13 08:34:34 +08:00
parent e07cf7cd81
commit c833c07fd6
2 changed files with 39 additions and 7 deletions
+4 -2
View File
@@ -9,7 +9,7 @@ const tasks = ref<TaskListItem[]>([])
const loading = ref(false) const loading = ref(false)
// 筛选 // 筛选
const typeFilter = ref<'all' | 'cta' | 'optimize' | 'factor'>('all') const typeFilter = ref<'all' | 'cta' | 'portfolio' | 'optimize' | 'factor'>('all')
const statusFilter = ref<'all' | 'done' | 'failed'>('all') const statusFilter = ref<'all' | 'done' | 'failed'>('all')
const keyword = ref('') const keyword = ref('')
// 分页 // 分页
@@ -19,6 +19,7 @@ const pageSize = 20
const typeOptions = [ const typeOptions = [
{ key: 'all', label: '全部' }, { key: 'all', label: '全部' },
{ key: 'cta', label: '回测' }, { key: 'cta', label: '回测' },
{ key: 'portfolio', label: '组合' },
{ key: 'optimize', label: '参数优化' }, { key: 'optimize', label: '参数优化' },
{ key: 'factor', label: '因子分析' }, { key: 'factor', label: '因子分析' },
] as const ] as const
@@ -77,10 +78,11 @@ async function load(): Promise<void> {
function open(row: TaskListItem): void { function open(row: TaskListItem): void {
if (row.type === 'factor') router.push(`/factor/result/${row.task_id}`) if (row.type === 'factor') router.push(`/factor/result/${row.task_id}`)
else if (row.type === 'optimize') router.push(`/backtest/optimize-result/${row.task_id}`) else if (row.type === 'optimize') router.push(`/backtest/optimize-result/${row.task_id}`)
else if (row.type === 'portfolio') router.push(`/backtest/portfolio?task_id=${row.task_id}`)
else router.push(`/backtest/result/${row.task_id}`) else router.push(`/backtest/result/${row.task_id}`)
} }
const typeLabelMap: Record<string, string> = { cta: '回测', optimize: '优化', factor: '因子' } const typeLabelMap: Record<string, string> = { cta: '回测', portfolio: '组合', optimize: '优化', factor: '因子' }
function typeLabel(t: string): string { function typeLabel(t: string): string {
return typeLabelMap[t] || t return typeLabelMap[t] || t
} }
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRoute } from 'vue-router'
import type { EChartsCoreOption } from 'echarts' import type { EChartsCoreOption } from 'echarts'
import { useChart } from '@/composables/useChart' import { useChart } from '@/composables/useChart'
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark' import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
@@ -109,8 +110,29 @@ function renderEquity(): void {
} }
setEquityOption(option) setEquityOption(option)
} }
const route = useRoute()
const viewTaskId = computed(() => (route.query.task_id as string) || '')
const elapsedSec = ref(0)
let elapsedTimer: ReturnType<typeof setInterval> | null = null
async function loadResult(tid: string): Promise<void> {
try {
const r = await getPortfolioResult(tid)
result.value = r
equityCurve.value = r.equity_curve || []
stocks.value = r.stocks_selected || []
trades.value = r.trades || []
metrics.value = r.metrics || null
period.value = r.period || null
await nextTick()
renderEquity()
} catch {
ElMessage.error('加载历史结果失败')
}
}
onMounted(() => { onMounted(() => {
// 等 dom 挂载后渲染(若有初始数据) if (viewTaskId.value) loadResult(viewTaskId.value)
}) })
onUnmounted(() => { onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer) if (pollTimer) clearInterval(pollTimer)
@@ -130,6 +152,10 @@ async function onSubmit(): Promise<void> {
submitting.value = true submitting.value = true
errorMsg.value = '' errorMsg.value = ''
stageHint.value = '' stageHint.value = ''
elapsedSec.value = 0
elapsedTimer = setInterval(() => {
elapsedSec.value += 1
}, 1000)
result.value = null result.value = null
equityCurve.value = [] equityCurve.value = []
stocks.value = [] stocks.value = []
@@ -185,6 +211,10 @@ async function onSubmit(): Promise<void> {
ElMessage.error('回测失败,详见页面提示') ElMessage.error('回测失败,详见页面提示')
} finally { } finally {
submitting.value = false submitting.value = false
if (elapsedTimer) {
clearInterval(elapsedTimer)
elapsedTimer = null
}
} }
} }
</script> </script>
@@ -193,14 +223,14 @@ async function onSubmit(): Promise<void> {
<div class="page bt-portfolio"> <div class="page bt-portfolio">
<div class="page-head"> <div class="page-head">
<div> <div>
<h2 class="page-title">组合策略回测</h2> <h2 class="page-title">{{ viewTaskId ? '组合回测结果' : '组合策略回测' }}</h2>
<p class="page-subtitle"> <p class="page-subtitle">
BulletTrade + {{ strategyLabel }} · 本地执行回测 · MVP 验证链路 BulletTrade + {{ strategyLabel }} · 本地执行回测 · MVP 验证链路
</p> </p>
</div> </div>
</div> </div>
<el-card class="blk" shadow="never"> <el-card v-if="!viewTaskId" class="blk" shadow="never">
<template #header><span class="section-title">回测参数</span></template> <template #header><span class="section-title">回测参数</span></template>
<el-form :model="form" label-width="120px"> <el-form :model="form" label-width="120px">
<el-form-item label="标的池"> <el-form-item label="标的池">
@@ -267,7 +297,7 @@ async function onSubmit(): Promise<void> {
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="submit-bar"> <div v-if="!viewTaskId" class="submit-bar">
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
@@ -277,7 +307,7 @@ async function onSubmit(): Promise<void> {
{{ submitting ? `回测中${stageHint ? '(' + stageHint + ')' : '...'}` : '开始回测' }} {{ submitting ? `回测中${stageHint ? '(' + stageHint + ')' : '...'}` : '开始回测' }}
</el-button> </el-button>
<span v-if="submitting" class="muted form-hint"> <span v-if="submitting" class="muted form-hint">
异步执行,请勿关闭页面 异步执行 · 已等待 {{ elapsedSec }}s · 组合回测加载大库数据预计 1-3 分钟,请勿关闭
</span> </span>
</div> </div>
</el-card> </el-card>