diff --git a/web-ui/src/views/BacktestView.vue b/web-ui/src/views/BacktestView.vue index 0d7d0d3..e9e56fa 100644 --- a/web-ui/src/views/BacktestView.vue +++ b/web-ui/src/views/BacktestView.vue @@ -14,6 +14,7 @@ import StrategyPicker from '../components/StrategyPicker.vue' import SymbolPicker from '../components/SymbolPicker.vue' import TradeTable from '../components/TradeTable.vue' import { formatError, saveStrategy } from '../api' +import { detectMarket } from '../market' import { gradePerformance } from '../grading' import type { Category, ExecutionMode } from '../types' import { useBacktestStore } from '../stores/backtest' @@ -121,12 +122,10 @@ const grade = computed(() => ) // 当前股票完整代码(市场:6位),从 SymbolPicker 同步来的 code 是纯数字, -// 需要带上市场前缀。复用 SymbolPicker 内部已经算好的前缀更稳妥——这里简单按 -// 交易所规则推断(6 位代码:6/9 开头 SH,其余 SZ;8/4 开头 BJ)。 +// 需要带上市场前缀。复用 market.ts 的 detectMarket(与 SymbolPicker / +// StocksPicker 同一套规则),避免分叉导致 ETF/基金(5 开头)等被错判市场。 function fullSymbol(code6: string): string { - if (/^(6|9)/.test(code6)) return `SH:${code6}` - if (/^(8|4)/.test(code6)) return `BJ:${code6}` - return `SZ:${code6}` + return `${detectMarket(code6)}:${code6}` } function openSaveForm() { diff --git a/web-ui/src/views/StrategiesView.vue b/web-ui/src/views/StrategiesView.vue index 39b7ab1..8561551 100644 --- a/web-ui/src/views/StrategiesView.vue +++ b/web-ui/src/views/StrategiesView.vue @@ -7,6 +7,7 @@ import { computed, nextTick, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import EquityChart from '../components/EquityChart.vue' +import GradeDetails from '../components/GradeDetails.vue' import MetricTable from '../components/MetricTable.vue' import PortfolioCompareChart from '../components/PortfolioCompareChart.vue' import PortfolioSummaryTable from '../components/PortfolioSummaryTable.vue' @@ -16,6 +17,8 @@ import { formatError, saveStrategy, } from '../api' +import { gradePortfolio } from '../grading' +import { detectMarket } from '../market' import type { MultiStrategyItem, Performance, SavedStrategy } from '../types' import { useBacktestStore } from '../stores/backtest' @@ -78,6 +81,17 @@ function clearSelection() { 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[],各跑原标的,资金均分。 */ async function onComboBacktest() { if (selectedStrategies.value.length === 0) return @@ -93,7 +107,7 @@ async function onComboBacktest() { strategy: s.strategy, strategy_label: s.strategy_label || s.strategy, params: s.params, - symbol: s.context.symbol as string, + symbol: normalizeSymbol(s.context.symbol as string), category: (s.context.category as MultiStrategyItem['category']) || 'DAY', start_date: (s.context.start_date as string) || undefined, end_date: (s.context.end_date as string) || undefined, @@ -190,7 +204,11 @@ async function onLoadMulti(s: SavedStrategy) { `结果区会显示截至今天的策略信号("哪些该买/该卖")。`, )) return 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 lastComboItems.value = items lastComboCash.value = cash @@ -393,6 +411,12 @@ const comboPerf = computed(() => { volatility: get('volatility'), } }) + +// 组合评级:从 combined_equity 重算夏普/卡玛/回撤/波动率等 5 维度评分, +// 与 /portfolio 页和单标的回测页同口径(复用 gradePortfolio)。 +const comboGrade = computed(() => + store.multiStrategyResult ? gradePortfolio(store.multiStrategyResult) : null, +)