feat(web-ui): 策略库组合回测结果加组合评级 + 市场前缀纠正

- StrategiesView: 组合回测结果区新增「组合评级」详情块,复用
  gradePortfolio(与 /portfolio 页和单标的回测页同口径的 5 维度评分)
- StrategiesView: 新增 normalizeSymbol,发请求前用 detectMarket 重算
  市场前缀,纠正历史保存策略的错配(如 SZ:515030 → SH:515030)
- BacktestView: fullSymbol 改用 market.ts 的 detectMarket,修复 5 开头
  沪市基金/ETF 被错判为深市的问题(与 SymbolPicker/StocksPicker 统一)
This commit is contained in:
Justin Gu
2026-07-06 00:59:13 +08:00
parent 6f121dc156
commit 5aec4996c6
2 changed files with 35 additions and 7 deletions
+4 -5
View File
@@ -14,6 +14,7 @@ import StrategyPicker from '../components/StrategyPicker.vue'
import SymbolPicker from '../components/SymbolPicker.vue' import SymbolPicker from '../components/SymbolPicker.vue'
import TradeTable from '../components/TradeTable.vue' import TradeTable from '../components/TradeTable.vue'
import { formatError, saveStrategy } from '../api' import { formatError, saveStrategy } from '../api'
import { detectMarket } from '../market'
import { gradePerformance } from '../grading' import { gradePerformance } from '../grading'
import type { Category, ExecutionMode } from '../types' import type { Category, ExecutionMode } from '../types'
import { useBacktestStore } from '../stores/backtest' import { useBacktestStore } from '../stores/backtest'
@@ -121,12 +122,10 @@ const grade = computed(() =>
) )
// 当前股票完整代码(市场:6位),从 SymbolPicker 同步来的 code 是纯数字, // 当前股票完整代码(市场:6位),从 SymbolPicker 同步来的 code 是纯数字,
// 需要带上市场前缀。复用 SymbolPicker 内部已经算好的前缀更稳妥——这里简单按 // 需要带上市场前缀。复用 market.ts 的 detectMarket(与 SymbolPicker /
// 交易所规则推断(6 位代码:6/9 开头 SH,其余 SZ;8/4 开头 BJ // StocksPicker 同一套规则),避免分叉导致 ETF/基金(5 开头)等被错判市场
function fullSymbol(code6: string): string { function fullSymbol(code6: string): string {
if (/^(6|9)/.test(code6)) return `SH:${code6}` return `${detectMarket(code6)}:${code6}`
if (/^(8|4)/.test(code6)) return `BJ:${code6}`
return `SZ:${code6}`
} }
function openSaveForm() { function openSaveForm() {
+31 -2
View File
@@ -7,6 +7,7 @@ import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import EquityChart from '../components/EquityChart.vue' import EquityChart from '../components/EquityChart.vue'
import GradeDetails from '../components/GradeDetails.vue'
import MetricTable from '../components/MetricTable.vue' import MetricTable from '../components/MetricTable.vue'
import PortfolioCompareChart from '../components/PortfolioCompareChart.vue' import PortfolioCompareChart from '../components/PortfolioCompareChart.vue'
import PortfolioSummaryTable from '../components/PortfolioSummaryTable.vue' import PortfolioSummaryTable from '../components/PortfolioSummaryTable.vue'
@@ -16,6 +17,8 @@ import {
formatError, formatError,
saveStrategy, saveStrategy,
} from '../api' } from '../api'
import { gradePortfolio } from '../grading'
import { detectMarket } from '../market'
import type { MultiStrategyItem, Performance, SavedStrategy } from '../types' import type { MultiStrategyItem, Performance, SavedStrategy } from '../types'
import { useBacktestStore } from '../stores/backtest' import { useBacktestStore } from '../stores/backtest'
@@ -78,6 +81,17 @@ function clearSelection() {
selectedIds.value = new Set() selectedIds.value = new Set()
} }
/** 纠正历史保存策略的市场前缀。
* 早期 BacktestView.fullSymbol 硬编码市场判断(漏判 5 开头的沪市基金/ETF),
* 导致部分历史保存的策略 symbol 被错标(如 SZ:515030,应为 SH:515030),
* 后端按错配市场取到 0 根 K 线被静默跳过。
* 这里在发请求前用 detectMarket 重算前缀,纠正历史数据 + 兜底未来。 */
function normalizeSymbol(raw: string): string {
if (!raw) return raw
const code = raw.includes(':') ? raw.split(':').pop()! : raw
return `${detectMarket(code)}:${code}`
}
/** 组合回测:把勾选的策略组装成 MultiStrategyItem[],各跑原标的,资金均分。 */ /** 组合回测:把勾选的策略组装成 MultiStrategyItem[],各跑原标的,资金均分。 */
async function onComboBacktest() { async function onComboBacktest() {
if (selectedStrategies.value.length === 0) return if (selectedStrategies.value.length === 0) return
@@ -93,7 +107,7 @@ async function onComboBacktest() {
strategy: s.strategy, strategy: s.strategy,
strategy_label: s.strategy_label || s.strategy, strategy_label: s.strategy_label || s.strategy,
params: s.params, params: s.params,
symbol: s.context.symbol as string, symbol: normalizeSymbol(s.context.symbol as string),
category: (s.context.category as MultiStrategyItem['category']) || 'DAY', category: (s.context.category as MultiStrategyItem['category']) || 'DAY',
start_date: (s.context.start_date as string) || undefined, start_date: (s.context.start_date as string) || undefined,
end_date: (s.context.end_date as string) || undefined, end_date: (s.context.end_date as string) || undefined,
@@ -190,7 +204,11 @@ async function onLoadMulti(s: SavedStrategy) {
`结果区会显示截至今天的策略信号("哪些该买/该卖")。`, `结果区会显示截至今天的策略信号("哪些该买/该卖")。`,
)) return )) return
const today = isoToday() const today = isoToday()
const items = rawItems.map((it) => ({ ...it, end_date: today })) const items = rawItems.map((it) => ({
...it,
symbol: normalizeSymbol(it.symbol),
end_date: today,
}))
const cash = typeof ctx.cash === 'number' ? ctx.cash : 1_000_000 const cash = typeof ctx.cash === 'number' ? ctx.cash : 1_000_000
lastComboItems.value = items lastComboItems.value = items
lastComboCash.value = cash lastComboCash.value = cash
@@ -393,6 +411,12 @@ const comboPerf = computed<Performance | null>(() => {
volatility: get('volatility'), volatility: get('volatility'),
} }
}) })
// 组合评级:从 combined_equity 重算夏普/卡玛/回撤/波动率等 5 维度评分,
// 与 /portfolio 页和单标的回测页同口径(复用 gradePortfolio)。
const comboGrade = computed(() =>
store.multiStrategyResult ? gradePortfolio(store.multiStrategyResult) : null,
)
</script> </script>
<template> <template>
@@ -628,6 +652,11 @@ const comboPerf = computed<Performance | null>(() => {
</div> </div>
<div v-if="store.multiStrategyResult" class="combo-content"> <div v-if="store.multiStrategyResult" class="combo-content">
<div v-if="comboGrade" class="combo-chart-block">
<h4>组合评级</h4>
<GradeDetails :result="comboGrade" expanded />
</div>
<div class="combo-summary"> <div class="combo-summary">
<div class="combo-stat"> <div class="combo-stat">
<span class="label">组合总收益</span> <span class="label">组合总收益</span>