修复股票与ETF切换误清策略池

This commit is contained in:
shy3130
2026-07-21 21:39:05 +08:00
parent 0ac6d6f56c
commit b4dae4bca1
2 changed files with 29 additions and 17 deletions
+2 -2
View File
@@ -1402,9 +1402,9 @@ export const api = {
: '/api/watchlist/enriched',
),
screenerStrategies: async (assetType: 'stock' | 'etf' = 'stock') => {
screenerStrategies: async (assetType?: 'stock' | 'etf') => {
const data = await request<{ strategies: StrategyDetail[]; load_errors?: StrategyLoadError[] }>(
`/api/strategies?asset_type=${assetType}&timeframe=1d`,
`/api/strategies?${assetType ? `asset_type=${assetType}&` : ''}timeframe=1d`,
)
return { presets: data.strategies, load_errors: data.load_errors }
},
+27 -15
View File
@@ -120,8 +120,8 @@ export function Screener() {
const screenerAutoRun = prefs?.screener_auto_run ?? true
const strategies = useQuery({
queryKey: QK.screenerStrategies(assetType),
queryFn: () => api.screenerStrategies(assetType),
queryKey: QK.screenerStrategies('all'),
queryFn: () => api.screenerStrategies(),
})
// 卡片首屏只读取轻量摘要;明细在点击策略或“全部”时按需加载。
@@ -154,25 +154,37 @@ export function Screener() {
if (latest) setAsOf(latest)
}, [dataStatus.data?.enriched?.latest_date])
const strategyPresets = useMemo(
() => (strategies.data?.presets ?? []).filter(s => s.asset_types.includes(assetType)),
[strategies.data, assetType],
)
// 策略 ID → 名称映射
const strategyIdToName = useMemo(() => {
const map: Record<string, string> = {}
for (const p of strategies.data?.presets ?? []) {
for (const p of strategyPresets) {
map[p.id] = p.name
}
return map
}, [strategies.data])
}, [strategyPresets])
// 策略 ID → 完整对象映射(避免每张卡片 find 遍历)
const strategyMap = useMemo(() => {
const map = new Map<string, ScreenerStrategy>()
for (const p of strategies.data?.presets ?? []) {
for (const p of strategyPresets) {
map.set(p.id, p)
}
return map
}, [strategies.data])
}, [strategyPresets])
const availableStrategyIds = useMemo(() => new Set((strategies.data?.presets ?? []).map(s => s.id)), [strategies.data])
const allStrategyIds = useMemo(
() => new Set((strategies.data?.presets ?? []).map(s => s.id)),
[strategies.data],
)
const availableStrategyIds = useMemo(
() => new Set(strategyPresets.map(s => s.id)),
[strategyPresets],
)
const visiblePool = useMemo(() => pool.filter(id => availableStrategyIds.has(id)), [pool, availableStrategyIds])
// 策略列表加载后,自动清除池中失效的自定义策略(如本地开发残留的、
@@ -183,9 +195,9 @@ export function Screener() {
useEffect(() => {
if (strategies.isError) return // 拉取失败: 不 prune
if (!strategies.isSuccess) return // 加载中: 不 prune
if (availableStrategyIds.size === 0) return // 空列表: 不 prune
prune(availableStrategyIds)
}, [availableStrategyIds, prune, strategies.isError, strategies.isSuccess])
if (allStrategyIds.size === 0) return // 空列表: 不 prune
prune(allStrategyIds)
}, [allStrategyIds, prune, strategies.isError, strategies.isSuccess])
// 策略文件加载失败时提示用户(避免"策略静默消失"被误判为正常)
const loadErrors = strategies.data?.load_errors ?? []
@@ -400,7 +412,7 @@ export function Screener() {
useEffect(() => {
// ETF 模式无股票盘后缓存/ runAll, 单策略走实时单跑, 不触发 runAll
if (assetType !== 'stock') return
if (!asOf || !strategies.data?.presets?.length || !summaryQuery.isSuccess || runAll.isPending || visiblePool.length === 0) return
if (!asOf || strategyPresets.length === 0 || !summaryQuery.isSuccess || runAll.isPending || visiblePool.length === 0) return
const runKey = `${asOf}|${visiblePool.join(',')}`
if (runAllDateRef.current === runKey) return
// 缓存已覆盖当前策略池 → 秒加载, 不触发 runAll
@@ -412,7 +424,7 @@ export function Screener() {
if (!screenerAutoRun) return
runAllDateRef.current = runKey
runAll.mutate({ date: asOf, strategyIds: missingStrategyIds })
}, [asOf, strategies.data, summaryQuery.isSuccess, visiblePool, cacheCoversPool, missingStrategyIds, screenerAutoRun, assetType, runAll.isPending])
}, [asOf, strategyPresets.length, summaryQuery.isSuccess, visiblePool, cacheCoversPool, missingStrategyIds, screenerAutoRun, assetType, runAll.isPending])
const run = useMutation({
mutationFn: ({ id, date }: { id: string; date: string }) =>
@@ -621,7 +633,7 @@ export function Screener() {
<Layers className="h-3.5 w-3.5" />
<span className="ml-0.5 min-w-[28px] h-4 flex items-center justify-center rounded-full bg-accent/15 text-accent text-[10px] font-bold">
{visiblePool.length}/{strategies.data?.presets?.length ?? 0}
{visiblePool.length}/{strategyPresets.length}
</span>
</button>
{/* 创建策略 */}
@@ -943,9 +955,9 @@ export function Screener() {
open={showBuilder}
onClose={() => setShowBuilder(false)}
mode={builderMode}
existingStrategyIds={availableStrategyIds}
existingStrategyIds={allStrategyIds}
onSavedId={async id => {
const data = await qc.fetchQuery({ queryKey: QK.screenerStrategies('stock'), queryFn: () => api.screenerStrategies('stock'), staleTime: 0 })
const data = await qc.fetchQuery({ queryKey: QK.screenerStrategies('all'), queryFn: () => api.screenerStrategies(), staleTime: 0 })
if (!data.presets.some(s => s.id === id)) {
throw new Error(`策略 ${id} 已保存但未加载,请检查策略代码`)
}