Files
sanguo_vnpy_v2/frontend/src/views/backtest/OptimizeResult.vue
T
claude_dev 6ed71ed8cf fix(optimize): 参数优化端到端修复(空statistics/500/不跳页)
根因: vnpy run_optimization 孙进程 spawn re-import setting.py 重置 DB → load 0根 → 空 statistics;连带 task_id 不一致 + statistics 含 date/numpy 序列化失败 + 前端单页轮询 180s 超时。
修复(不动 vnpy 源码):
- cta_optimizer: vt_setting.json 适配(vnpy 原生机制 setting.py:43 load_json,孙进程拿到正确 DB) + task_id 贯通 + 主聚合持久化(combos) + 过滤空 statistics 记录
- result_store: _json_default(date→isoformat + numpy→原生),save_result 两处 json.dumps 加 default
- routes: optimization-results 改读 DB 主聚合, fallback 内存(重启不丢)
- runner: _opt_worker 加 task_id 参数 + submit_optimize 传递(对齐 _cta_worker)
- 前端: Optimize 提交后跳 progress;Progress 用 opt_ 前缀判断跳 optimize-result;新建 OptimizeResult 参数表格页;router 加路由
验证: 端到端 optimization-results http=200 + 4组合非空 statistics + 浏览器提交→跳 optimize-result 页表格渲染。
2026-07-17 10:58:49 +08:00

139 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getOptimizationResults, type OptRow } from '@/api/backtest'
const route = useRoute()
const taskId = String(route.params.id)
const loading = ref(false)
const results = ref<OptRow[]>([])
onMounted(async () => {
loading.value = true
try {
results.value = await getOptimizationResults(taskId)
// 默认按总收益降序
const retKey = statColumns.value.find((k) => /return|收益/i.test(k))
if (retKey) { sortKey.value = retKey; sortDir.value = 'desc' }
} catch {
ElMessage.error('优化结果加载失败')
} finally {
loading.value = false
}
})
const statColumns = computed(() => {
const set = new Set<string>()
results.value.forEach((r) => Object.keys(r.statistics || {}).forEach((k) => set.add(k)))
return Array.from(set)
})
// 排序
const sortKey = ref('')
const sortDir = ref<'desc' | 'asc'>('desc')
function setSort(k: string): void {
if (sortKey.value === k) {
sortDir.value = sortDir.value === 'desc' ? 'asc' : 'desc'
} else {
sortKey.value = k
sortDir.value = 'desc'
}
}
const sortedResults = computed(() => {
if (!sortKey.value) return results.value
const k = sortKey.value
const dir = sortDir.value === 'desc' ? -1 : 1
return [...results.value].sort((a, b) => {
const va = Number(a.statistics?.[k] ?? NaN)
const vb = Number(b.statistics?.[k] ?? NaN)
if (!Number.isFinite(va) && !Number.isFinite(vb)) return 0
if (!Number.isFinite(va)) return 1
if (!Number.isFinite(vb)) return -1
return (va - vb) * dir
})
})
// Top1 行(按当前排序首项)
const topParamsKey = computed(() => {
if (!sortedResults.value.length) return ''
return JSON.stringify(sortedResults.value[0].params)
})
function topRowClass({ row }: { row: OptRow }): string {
return JSON.stringify(row.params) === topParamsKey.value ? 'top-row' : ''
}
function fmt(v: unknown): string {
return typeof v === 'number' ? (Math.round(v * 10000) / 10000).toString() : v == null ? '' : String(v)
}
function isUp(k: string): boolean {
return /return|收益|sharpe|夏普|alpha|ir/i.test(k)
}
</script>
<template>
<div class="page opt-result-page">
<div class="page-head">
<div>
<h2 class="page-title">优化结果</h2>
<p class="page-subtitle">任务 ID<span class="mono">{{ taskId }}</span></p>
</div>
</div>
<el-card class="blk" shadow="never" v-loading="loading">
<template #header>
<span class="section-title">参数组合 <span class="count-badge">{{ results.length }}</span></span>
<span class="muted sort-hint">点击列头排序 · 当前
<strong v-if="sortKey" class="mono">{{ sortKey }} ({{ sortDir === 'desc' ? '降序' : '升序' }})</strong>
<em v-else>未排序</em>
</span>
</template>
<el-table v-if="results.length" :data="sortedResults" size="small" :row-class-name="topRowClass">
<el-table-column label="参数" min-width="220">
<template #default="{ row }">
<span v-for="(v, k) in row.params" :key="String(k)" class="param-pair mono">
{{ k }}=<strong>{{ fmt(v) }}</strong>
</span>
</template>
</el-table-column>
<el-table-column
v-for="k in statColumns" :key="k" :label="k" :align="isUp(k) ? 'right' : 'right'"
:class-name="isUp(k) ? 'num up-col' : 'num'"
>
<template #header>
<span class="th-sort" :class="{ active: sortKey === k }" @click="setSort(k)">
{{ k }}
<span v-if="sortKey === k">{{ sortDir === 'desc' ? '' : '' }}</span>
</span>
</template>
<template #default="{ row }">
<span class="mono">{{ fmt(row.statistics?.[k]) }}</span>
</template>
</el-table-column>
</el-table>
<span v-else class="muted">无有效优化结果</span>
</el-card>
</div>
</template>
<style scoped>
.opt-result-page { display: flex; flex-direction: column; gap: 16px; }
.blk { border: 1px solid var(--border-2); }
.sort-hint { margin-left: 12px; font-size: 12px; }
.th-sort { cursor: pointer; user-select: none; }
.th-sort.active { color: var(--brand); font-weight: 700; }
.th-sort:hover { color: var(--brand); }
.param-pair { margin-right: 10px; color: var(--text-2); }
.param-pair strong { color: var(--text); }
:deep(.num) { font-family: var(--mono); }
:deep(.top-row) { background: rgba(63, 185, 80, 0.10) !important; }
:deep(.top-row td) { font-weight: 600; }
</style>