Files
easy-tdx/web-ui/src/stores/backtest.ts
T
Justin Gu 6aab7a82b5 feat(web-ui): Vue3 + ECharts 回测前端(单标的 + 组合回测)
独立前端工程(web-ui/),Vue3 + Vite + TypeScript + Pinia + ECharts。
通过 vite proxy 对接后端 /api,开发期无跨域。

单标的回测(/):
- StrategyPicker: 策略下拉 + 按 schema 动态渲染参数表单
- SymbolPicker: 选标的 + 日期范围取行情(默认3年,结束日=今天)
- KlineChart: K线主图 + 买卖点 markPoint(按 datetime 对齐)
- EquityChart: 净值曲线 + 回撤双轴图
- MetricTable: 19 项绩效指标(收益/风险/交易分组)
- TradeTable: 成交记录表

组合回测(/portfolio):
- StocksPicker: 多标的输入(增删标签)
- PortfolioCompareChart: 各标的净值叠加(归一化对比)
- PortfolioSummaryTable: 各标的绩效横向对比
- 顶部导航切换单标的/组合

契约修复(/check 审计):
- fetchBars 归一化 date/datetime 列名(日线返回 date)
- K线买卖点按完整 datetime 对齐(分钟线不再折叠)
- count 上限对齐后端 800
- ECharts 注册内联 dark 主题
- 参数表单空输入不打断编辑
- 网络错误友好提示

技术栈:Vue 3.5 / Vite 8 / TS 6 / Pinia / ECharts(按需引入 725KB)
2026-07-03 03:35:20 +08:00

130 lines
4.0 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, fetchTask } from '../api'
import type {
BacktestRequest,
BacktestResult,
Bar,
PortfolioBacktestRequest,
PortfolioResult,
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 = ''
}
return {
// state
strategies,
strategiesLoaded,
ohlcv,
barsSource,
result,
running,
error,
portfolioResult,
portfolioRunning,
// getters
hasBars,
// actions
loadStrategies,
setOhlcv,
run,
clearResult,
runPortfolio,
clearPortfolio,
}
})