Files
easy-tdx/web-ui/src/stores/backtest.ts
T
Justin Gu 87fafe9131 feat(backtest): 参数网格寻优(optimizer + 前端寻优页)
对单个策略的 1-2 个参数做网格搜索,遍历用户指定的取值列表笛卡尔积,
每个组合跑一次回测,按 total_return 排序,返回排名表 + 热力图。

后端:
- ParamGridOptimizer(backtest/optimizer.py):itertools.product 遍历网格,
  每点 entry.build(params) + BacktestEngine.run(df),复用同一 DataFrame
- 网格大小上限 200 防组合爆炸,单点失败容错(跳过不中断)
- 2 参数时生成热力图矩阵(x/y 轴取值 + cell 收益率)
- POST /backtest/optimize/run/async 端点(后台任务)
- OptimizeBacktestRequest schema(param_grid 1-2 参数)

前端(/optimize 寻优页):
- ParamGridPicker:勾选 1-2 个寻优参数,逗号分隔填取值列表
- OptimizeResultTable:网格点排名表(按收益降序,最优高亮)
- OptimizeHeatmap:2 参数热力图(ECharts heatmap,绿→红映射收益)
- 最优点「查看」按钮跳转单标的页用该参数回测

测试:821 passed(+10 寻优器单测 + 3 寻优路由测试)
2026-07-03 03:55:35 +08:00

175 lines
5.3 KiB
TypeScript
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.
// 回测状态管理(Pinia)。
// 持有:策略列表、当前 OHLCV、回测结果、运行状态、错误信息。
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import {
fetchStrategies,
formatError,
runBacktest,
submitPortfolioTask,
submitOptimizeTask,
fetchTask,
} from '../api'
import type {
BacktestRequest,
BacktestResult,
Bar,
PortfolioBacktestRequest,
PortfolioResult,
OptimizeBacktestRequest,
OptimizeResult,
StrategySchema,
} from '../types'
export const useBacktestStore = defineStore('backtest', () => {
// ── 策略 ─────────────────────────────────────────────────────────────────
const strategies = ref<StrategySchema[]>([])
const strategiesLoaded = ref(false)
async function loadStrategies() {
if (strategiesLoaded.value) return
const resp = await fetchStrategies()
strategies.value = resp.strategies
strategiesLoaded.value = true
}
// ── OHLCV 行情(前端始终持有,回测与 K 线共用) ───────────────────────────
const ohlcv = ref<Bar[]>([])
const barsSource = ref<string>('') // 来源描述,如 "SZ:000001 DAY×250"
function setOhlcv(bars: Bar[], source: string) {
ohlcv.value = bars
barsSource.value = source
}
const hasBars = computed(() => ohlcv.value.length >= 2)
// ── 回测结果 ──────────────────────────────────────────────────────────────
const result = ref<BacktestResult | null>(null)
const running = ref(false)
const error = ref<string>('')
/** 运行同步回测(内联 OHLCV)。 */
async function run(req: Omit<BacktestRequest, 'ohlcv'>) {
if (!hasBars.value) {
error.value = '请先取行情数据或粘贴 OHLCV'
return
}
running.value = true
error.value = ''
try {
const fullReq: BacktestRequest = { ...req, ohlcv: ohlcv.value }
result.value = await runBacktest(fullReq)
} catch (e) {
error.value = formatError(e)
result.value = null
} finally {
running.value = false
}
}
function clearResult() {
result.value = null
error.value = ''
}
// ── 组合回测(Phase 3) ───────────────────────────────────────────────────
const portfolioResult = ref<PortfolioResult | null>(null)
const portfolioRunning = ref(false)
/** 提交组合回测后台任务并轮询直到完成。 */
async function runPortfolio(req: PortfolioBacktestRequest) {
portfolioRunning.value = true
error.value = ''
portfolioResult.value = null
try {
const { task_id } = await submitPortfolioTask(req)
// 轮询
const start = Date.now()
// eslint-disable-next-line no-constant-condition
while (true) {
const state = await fetchTask(task_id)
if (state.status === 'done' && state.result) {
portfolioResult.value = state.result as PortfolioResult
break
}
if (state.status === 'failed') {
throw new Error(state.error || '组合回测失败')
}
if (Date.now() - start > 120_000) throw new Error('组合回测超时(120s')
await new Promise((r) => setTimeout(r, 300))
}
} catch (e) {
error.value = formatError(e)
portfolioResult.value = null
} finally {
portfolioRunning.value = false
}
}
function clearPortfolio() {
portfolioResult.value = null
error.value = ''
}
// ── 参数网格寻优(Phase 4) ─────────────────────────────────────────────
const optimizeResult = ref<OptimizeResult | null>(null)
const optimizeRunning = ref(false)
/** 提交寻优后台任务并轮询直到完成。 */
async function runOptimize(req: OptimizeBacktestRequest) {
optimizeRunning.value = true
error.value = ''
optimizeResult.value = null
try {
const { task_id } = await submitOptimizeTask(req)
const start = Date.now()
// eslint-disable-next-line no-constant-condition
while (true) {
const state = await fetchTask(task_id)
if (state.status === 'done' && state.result) {
optimizeResult.value = state.result as OptimizeResult
break
}
if (state.status === 'failed') {
throw new Error(state.error || '寻优失败')
}
if (Date.now() - start > 180_000) throw new Error('寻优超时(180s')
await new Promise((r) => setTimeout(r, 400))
}
} catch (e) {
error.value = formatError(e)
optimizeResult.value = null
} finally {
optimizeRunning.value = false
}
}
return {
// state
strategies,
strategiesLoaded,
ohlcv,
barsSource,
result,
running,
error,
portfolioResult,
portfolioRunning,
optimizeResult,
optimizeRunning,
// getters
hasBars,
// actions
loadStrategies,
setOhlcv,
run,
clearResult,
runPortfolio,
clearPortfolio,
runOptimize,
}
})