6ed71ed8cf
根因: 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 页表格渲染。
139 lines
4.6 KiB
Vue
139 lines
4.6 KiB
Vue
<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>
|