mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
fix(backtest): 预热/同步期间统计为 null 不再误判缺数据禁用回测
backtestDataUnavailable 原判定 dataStatus.isSuccess && !earliestDate 在 busy 降级期间 (预热 indicators_ready=false / 同步修复 stats_pending=true, enriched 统计为 null 是未知态) 恒为 true — 回测按钮被禁 + 常驻"缺少指标 数据, 请先同步"横幅, 与 Dashboard 已修的 warmup 误判同族 (stats_pending 契约: 空值不代表无数据)。补 statsPending 放行分支, 真实统计就绪且确无 数据时行为不变。 验证: StrategyBacktest.status.test 2 条 (busy 期无横幅 + 渲染无 TypeError; 就绪确无数据仍提示并禁用 — 修复前红/修复后绿); 全量 vitest 340 passed; pnpm build 通过。 Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// @vitest-environment jsdom
|
||||
// 回归: 预热/同步/修复期间 (stats_pending / indicators_ready=false) 统计为 null 是未知态,
|
||||
// 不得误判"缺少指标数据"禁用运行回测按钮 (同 Dashboard 的 warmup 误判一族);
|
||||
// 真实统计就绪且确无数据时仍要禁用 + 提示。
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { StrategyBacktest } from './StrategyBacktest'
|
||||
import type { DataStatus } from '@/lib/api'
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: {
|
||||
dataStatus: vi.fn(),
|
||||
screenerStrategies: vi.fn(async () => ({ presets: [] })),
|
||||
strategyGet: vi.fn(async () => ({})),
|
||||
watchlistList: vi.fn(async () => ({ items: [] })),
|
||||
positionPolicyList: vi.fn(async () => ({ policies: [] })),
|
||||
factorColumns: vi.fn(async () => ({ columns: [] })),
|
||||
factorLibrary: vi.fn(async () => ({ factors: [] })),
|
||||
instrumentSearch: vi.fn(async () => ({ results: [] })),
|
||||
customSignalsList: vi.fn(async () => ({ signals: [] })),
|
||||
customSignalsOptions: vi.fn(async () => ({ fields: [], operators: [] })),
|
||||
capabilities: vi.fn(async () => ({ capabilities: {} })),
|
||||
settings: vi.fn(async () => ({ onboarding_completed: true, mode: 'api_key', has_tickflow_key: true })),
|
||||
preferences: vi.fn(async () => ({})),
|
||||
dataSources: vi.fn(async () => []),
|
||||
regimeRecompute: vi.fn(async () => ({ ok: true, computed: 0 })),
|
||||
strategySaveConfig: vi.fn(async () => ({ ok: true })),
|
||||
researchCandidateCreate: vi.fn(async () => ({})),
|
||||
},
|
||||
REGIME_STATE_LABELS: {} as Record<string, string>,
|
||||
REGIME_STATE_COLORS: {} as Record<string, string>,
|
||||
}))
|
||||
vi.mock('@/lib/backtestTask', () => ({
|
||||
startBacktest: vi.fn(),
|
||||
stopBacktest: vi.fn(async () => {}),
|
||||
tryReconnect: vi.fn(() => false),
|
||||
clearBacktest: vi.fn(),
|
||||
useBacktestTask: () => null,
|
||||
}))
|
||||
|
||||
let host: HTMLDivElement
|
||||
let root: Root
|
||||
let client: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
||||
sessionStorage.clear()
|
||||
host = document.createElement('div')
|
||||
document.body.append(host)
|
||||
root = createRoot(host)
|
||||
client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
client.clear()
|
||||
host.remove()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const busy = {
|
||||
indicators_ready: false, stats_pending: true,
|
||||
daily: null, enriched: null, etf_enriched: null,
|
||||
} as unknown as DataStatus
|
||||
const readyNoData = {
|
||||
indicators_ready: true, stats_pending: false,
|
||||
daily: null, enriched: null, etf_enriched: null,
|
||||
} as unknown as DataStatus
|
||||
|
||||
async function renderPage() {
|
||||
const errors: unknown[] = []
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => { errors.push(args) })
|
||||
await act(async () => root.render(
|
||||
<MemoryRouter><QueryClientProvider client={client}><StrategyBacktest /></QueryClientProvider></MemoryRouter>,
|
||||
))
|
||||
await act(async () => { await new Promise(r => setTimeout(r, 80)) })
|
||||
spy.mockRestore()
|
||||
return errors
|
||||
}
|
||||
|
||||
function runButton(): HTMLButtonElement | undefined {
|
||||
return [...host.querySelectorAll('button')].find(b => b.textContent?.includes('运行回测'))
|
||||
}
|
||||
|
||||
it('预热/同步期间统计为 null: 不提示缺数据, 回测按钮可用', async () => {
|
||||
const { api } = await import('@/lib/api')
|
||||
vi.mocked(api.dataStatus).mockResolvedValue(busy)
|
||||
|
||||
const errors = await renderPage()
|
||||
// 核心误判: busy 未知态不得显示"缺少指标数据"横幅 (backtestDataUnavailable=false,
|
||||
// 按钮的 disabled 另受"未选策略"影响, 不在此断言)
|
||||
expect(host.textContent).not.toContain('缺少股票指标数据')
|
||||
// 渲染期 TypeError 防线 (与数据页崩溃修复同族守护)
|
||||
expect(errors.some(e => String(e).includes('TypeError'))).toBe(false)
|
||||
})
|
||||
|
||||
it('真实统计就绪且确无数据: 提示缺数据并禁用回测', async () => {
|
||||
const { api } = await import('@/lib/api')
|
||||
vi.mocked(api.dataStatus).mockResolvedValue(readyNoData)
|
||||
|
||||
await renderPage()
|
||||
expect(host.textContent).toContain('缺少股票指标数据')
|
||||
expect(runButton()!.disabled).toBe(true)
|
||||
})
|
||||
@@ -1147,7 +1147,10 @@ export function StrategyBacktest({ loadCandidate, onLoadConsumed }: {
|
||||
? dataStatus.data?.etf_enriched
|
||||
: dataStatus.data?.enriched
|
||||
const earliestDate = backtestDataStatus?.earliest_date ?? null
|
||||
const backtestDataUnavailable = dataStatus.isSuccess && !earliestDate
|
||||
// 预热/同步/修复期间统计为 null 是未知态 (stats_pending 契约, 同 Dashboard.isDataStatusPending),
|
||||
// 不得误判成"缺数据"禁用回测 — 等真实统计就绪再判
|
||||
const statsPending = dataStatus.data?.stats_pending === true || dataStatus.data?.indicators_ready === false
|
||||
const backtestDataUnavailable = dataStatus.isSuccess && !statsPending && !earliestDate
|
||||
const backtestDataLabel = assetType === 'etf' ? 'ETF 指标数据' : '股票指标数据'
|
||||
|
||||
const resetConfigFromDetail = (detail: StrategyDetail) => {
|
||||
|
||||
Reference in New Issue
Block a user