fix: 寻优跳过参数范围/对比支持组合/日线x轴年份/寻优跳转填充参数

4 个独立修复:

1. 寻优跳过参数范围检查:Param.validate 加 skip_bounds 参数,
   RegisteredStrategy.build 透传,ParamGridOptimizer 传 skip_bounds=True。
   修复寻优时 fast=250 被 max_value=60 拦截的问题(探索超范围值是寻优目的)。

2. 对比页支持组合回测:CompareView 新增 extractComparable() 统一提取净值+指标,
   支持单标的(performance/equity_curve)和组合(total_performance/combined_equity)。
   修复勾选组合回测任务报「非单标的回测」错误。

3. K线x轴日线显示完整年份:isIntraday 判断从 length>10 改为检查时分秒非零
   (日线归一化后带 T00:00:00 后缀,长度也 >10 导致误判为分钟线,slice 砍年份)。

4. 寻优跳转填充参数:BacktestView 加 useRoute(),onMounted 读 query.strategy +
   query.params,await nextTick 后覆盖(避免 StrategyPicker watch 重置)。
   修复寻优点「查看」跳转后参数未填充、仍用默认值的问题。

测试:823 passed, mypy 211 files OK, vue-tsc OK
This commit is contained in:
Justin Gu
2026-07-03 11:32:44 +08:00
parent 80d62aec56
commit 8323223937
5 changed files with 110 additions and 34 deletions
+7 -2
View File
@@ -30,8 +30,13 @@ function buildOption(): echarts.EChartsCoreOption {
const keyIndex = new Map<string, number>()
keys.forEach((k, i) => keyIndex.set(k, i))
// x 轴显示:日线只显示日期,分钟线显示日期+时间
const isIntraday = keys.some((k) => k.length > 10)
// x 轴显示:日线只显示日期 YYYY-MM-DD,分钟线显示 MM-DD HH:mm。
// 判断分钟线:datetime 有非零时分秒(日线归一化后是 T00:00:00)。
// 不能用 length > 10 判断——日线带 T00:00:00 后缀长度也是 19。
const isIntraday = keys.some((k) => {
const time = k.slice(11, 19) // HH:MM:SS 部分
return time && time !== '00:00:00'
})
const dates = keys.map((k) => (isIntraday ? k.replace('T', ' ').slice(5, 16) : k.slice(0, 10)))
const ohlc = props.bars.map((b) => [b.open, b.close, b.low, b.high])
+22 -3
View File
@@ -2,7 +2,8 @@
// 回测主页面:左配置面板 / 右报告面板。
// 编排:取行情 → 选策略+参数 → 回测 → 展示 K线+净值+指标+成交。
import { onMounted, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import EquityChart from '../components/EquityChart.vue'
import KlineChart from '../components/KlineChart.vue'
@@ -14,6 +15,7 @@ import type { ExecutionMode } from '../types'
import { useBacktestStore } from '../stores/backtest'
const store = useBacktestStore()
const route = useRoute()
// 表单状态(v-model 给子组件)
const strategy = ref('ma_cross')
@@ -25,10 +27,27 @@ const execution = ref<ExecutionMode>('next_open')
const EXECUTIONS: ExecutionMode[] = ['next_open', 'next_close', 'this_close', 'worst', 'best']
onMounted(() => {
store.loadStrategies().catch((e) => {
onMounted(async () => {
await store.loadStrategies().catch((e) => {
store.error = `加载策略列表失败:${e instanceof Error ? e.message : e}`
})
// 从 URL query 读取寻优页传来的 strategy + params(跳转自动填充)
const qStrategy = route.query.strategy as string | undefined
const qParams = route.query.params as string | undefined
if (qStrategy) {
strategy.value = qStrategy
// 等待 StrategyPicker 的 watch(selectedSchema) 触发完默认值重置后,
// 再用 query 的 params 覆盖,避免被 watch 重置掉
await nextTick()
}
if (qParams) {
try {
params.value = JSON.parse(qParams) as Record<string, number | string | boolean>
} catch {
// query 参数解析失败,忽略
}
}
})
async function onRun() {
+44 -13
View File
@@ -1,19 +1,26 @@
<script setup lang="ts">
// 结果对比页面:选 2-4 个已完成的回测 task,叠加净值曲线 + 横向指标对比。
// 支持单标的回测(equity_curve)和组合回测(combined_equity)的混合对比。
import { onMounted, ref, watch } from 'vue'
import CompareChart from '../components/CompareChart.vue'
import CompareTable from '../components/CompareTable.vue'
import { fetchTask, fetchTaskList, formatError } from '../api'
import type { BacktestResult, TaskSummary } from '../types'
import type { BacktestResult, EquityPoint, TaskSummary } from '../types'
/** 统一的可对比项:无论单标的还是组合,都归一化为 equity + performance。 */
interface CompareItem {
label: string
equity: EquityPoint[]
performance: Record<string, number>
}
const taskList = ref<TaskSummary[]>([])
const loading = ref(false)
const error = ref('')
const selectedIds = ref<Set<string>>(new Set())
// 已加载的详情(task_id → BacktestResult
const details = ref<Map<string, { label: string; result: BacktestResult }>>(new Map())
const details = ref<Map<string, CompareItem>>(new Map())
onMounted(loadTasks)
@@ -22,7 +29,6 @@ async function loadTasks() {
error.value = ''
try {
const resp = await fetchTaskList(20)
// 只显示已完成的单标的回测(status=done 且 result 是 BacktestResult
taskList.value = resp.tasks.filter((t) => t.status === 'done')
} catch (e) {
error.value = formatError(e)
@@ -31,37 +37,62 @@ async function loadTasks() {
}
}
/** 从 task result 提取可对比的 equity + performance。
* 支持单标的(equity_curve/performance)和组合(combined_equity/total_performance)。 */
function extractComparable(
result: Record<string, unknown> | null,
): { equity: EquityPoint[]; performance: Record<string, number> } | null {
if (!result) return null
// 单标的回测
if (result.performance && result.equity_curve) {
return {
equity: result.equity_curve as EquityPoint[],
performance: result.performance as Record<string, number>,
}
}
// 组合回测
if (result.total_performance && result.combined_equity) {
return {
equity: result.combined_equity as EquityPoint[],
performance: result.total_performance as Record<string, number>,
}
}
return null
}
async function toggle(taskId: string) {
if (selectedIds.value.has(taskId)) {
selectedIds.value.delete(taskId)
details.value.delete(taskId)
} else {
if (selectedIds.value.size >= 4) return // 最多 4 个
if (selectedIds.value.size >= 4) return
selectedIds.value.add(taskId)
// 拉详情
try {
const state = await fetchTask(taskId)
const result = state.result as BacktestResult
if (!result?.performance || !result?.equity_curve) {
throw new Error('该任务结果不可对比(非单标的回测')
const comparable = extractComparable(state.result as Record<string, unknown> | null)
if (!comparable) {
throw new Error('该任务结果不含净值曲线(寻优任务等不可对比)')
}
details.value.set(taskId, { label: state.description, result })
details.value.set(taskId, { label: state.description, ...comparable })
} catch (e) {
selectedIds.value.delete(taskId)
error.value = e instanceof Error ? e.message : String(e)
}
}
// 触发响应式
selectedIds.value = new Set(selectedIds.value)
details.value = new Map(details.value)
}
const compareItems = ref<Array<{ label: string; result: BacktestResult }>>([])
function refreshItems() {
compareItems.value = Array.from(details.value.values())
// CompareChart/CompareTable 期望 { label, result: BacktestResult }
// 这里把归一化的 equity/performance 包装回去
compareItems.value = Array.from(details.value.values()).map((item) => ({
label: item.label,
result: { equity_curve: item.equity, performance: item.performance } as unknown as BacktestResult,
}))
}
// 监听 details 变化刷新对比项
watch(details, refreshItems, { deep: true })
</script>