mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 21:24:16 +08:00
fix(screener): 策略池按日线/分钟周期隔离, 修复分钟视角下整池显示失效
原设计: 日线/分钟共用一份策略池。切到分钟模式后, 池中 15 个日线策略 在分钟策略列表里解析不到, 策略池对话框右侧全部标成「失效」; 此时确认 还会把混合名单回写全局池, 日线池同样被污染。 - 池按周期隔离: 'strategy-pool' (日线, 沿用原 key 存量无损) + 'strategy-pool-1m' (分钟, 新 key); useStrategyPool(timeframe) 返回 当前周期的池, 任一池变更幂等落库双 key, 切换周期不会交叉写 - 「失效」徽章回归本义: 仅当策略真被删除 (不在本周期列表) 时出现 - prune 解除仅日线限制: 各周期用自身策略列表清理各自的池, 互不误删 - 策略构建器/叠加策略创建的是日线策略, 固定入日线池 (即使分钟页保存) 验证: 分钟对话框无失效列表、可正常添加分钟红7; 日线 15/31 ↔ 分钟 1/1 往返切换稳定; 刷新后双池持久。 注: 开发中 vite HMR 热替换瞬间曾把本地日线池写空 (旧代码对话框与新 模块混布的开发态一次性产物, 生产构建无 HMR 不受影响); 已从用户截图 复原原 15 策略及顺序 (boll_breakout → five_consecutive_yang)。
This commit is contained in:
@@ -24,8 +24,10 @@ export const storage = {
|
||||
/** 查询轮询 / SSE 配置 */
|
||||
queryConfig: kv<unknown>('tf-stocks-query-config'),
|
||||
|
||||
/** 策略池 (screener) */
|
||||
/** 策略池 (screener) — 日线池 */
|
||||
strategyPool: kv<string[]>('strategy-pool'),
|
||||
/** 策略池 (screener) — 分钟池 (与日线池按周期隔离) */
|
||||
strategyPoolMinute: kv<string[]>('strategy-pool-1m'),
|
||||
|
||||
/** 自选列表列配置 */
|
||||
watchlistColumns: kv<unknown[]>('watchlist_columns'),
|
||||
|
||||
@@ -1,36 +1,65 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { storage } from '@/lib/storage'
|
||||
|
||||
export function useStrategyPool() {
|
||||
const [pool, setPool] = useState<string[]>(() => storage.strategyPool.get([]))
|
||||
export type StrategyTimeframe = '1d' | '1m'
|
||||
|
||||
// 同步写入 localStorage
|
||||
useEffect(() => { storage.strategyPool.set(pool) }, [pool])
|
||||
// 两周期各自独立持久化, 互不可见: 日线池只装日线策略, 分钟池只装分钟策略。
|
||||
// 切换周期时 pool 指向对应列表, 任何写操作都只会落到所属 key。
|
||||
const POOL_STORES: Record<StrategyTimeframe, typeof storage.strategyPool> = {
|
||||
'1d': storage.strategyPool,
|
||||
'1m': storage.strategyPoolMinute,
|
||||
}
|
||||
|
||||
const addToPool = useCallback((id: string) => {
|
||||
setPool(prev => prev.includes(id) ? prev : [...prev, id])
|
||||
}, [])
|
||||
/**
|
||||
* 策略池 — 按周期 (日线/分钟) 隔离的两份池。
|
||||
* pool 为当前周期的列表; addToPool 可显式指定目标周期
|
||||
* (如策略构建器创建的是日线策略, 即使在分钟页保存也应入日线池)。
|
||||
*/
|
||||
export function useStrategyPool(timeframe: StrategyTimeframe = '1d') {
|
||||
const [pools, setPools] = useState<Record<StrategyTimeframe, string[]>>(() => ({
|
||||
'1d': storage.strategyPool.get([]),
|
||||
'1m': storage.strategyPoolMinute.get([]),
|
||||
}))
|
||||
const pool = pools[timeframe]
|
||||
|
||||
// 任一池变化即整体落库 (两份 key 幂等重写, 避免"只写当前池"漏掉非活跃池的更新)
|
||||
useEffect(() => {
|
||||
POOL_STORES['1d'].set(pools['1d'])
|
||||
POOL_STORES['1m'].set(pools['1m'])
|
||||
}, [pools])
|
||||
|
||||
const mutate = useCallback(
|
||||
(tf: StrategyTimeframe, updater: (prev: string[]) => string[]) => {
|
||||
setPools(prev => ({ ...prev, [tf]: updater(prev[tf]) }))
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const addToPool = useCallback((id: string, tf: StrategyTimeframe = timeframe) => {
|
||||
mutate(tf, prev => (prev.includes(id) ? prev : [...prev, id]))
|
||||
}, [mutate, timeframe])
|
||||
|
||||
const removeFromPool = useCallback((id: string) => {
|
||||
setPool(prev => prev.filter(x => x !== id))
|
||||
}, [])
|
||||
mutate(timeframe, prev => prev.filter(x => x !== id))
|
||||
}, [mutate, timeframe])
|
||||
|
||||
const reorderPool = useCallback((newOrder: string[]) => {
|
||||
setPool(newOrder)
|
||||
}, [])
|
||||
mutate(timeframe, () => newOrder)
|
||||
}, [mutate, timeframe])
|
||||
|
||||
// 清除池中不存在于 validIds 的失效策略(如本地开发残留的自定义策略)。
|
||||
// 调用方需以"当前周期自己的策略列表"传入, 各周期只清理各自的池。
|
||||
// 仅当确实有失效项时才更新,避免无谓重渲染。
|
||||
const prune = useCallback((validIds: Iterable<string>) => {
|
||||
const validSet = validIds instanceof Set ? validIds : new Set(validIds)
|
||||
setPool(prev => {
|
||||
mutate(timeframe, prev => {
|
||||
if (prev.length === 0) return prev
|
||||
const next = prev.filter(id => validSet.has(id))
|
||||
return next.length === prev.length ? prev : next
|
||||
})
|
||||
}, [])
|
||||
}, [mutate, timeframe])
|
||||
|
||||
const isInPool = useCallback((id: string) => pool.includes(id), [pool])
|
||||
|
||||
return { pool, addToPool, removeFromPool, reorderPool, prune, isInPool }
|
||||
return { pool, pools, addToPool, removeFromPool, reorderPool, prune, isInPool }
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Screener() {
|
||||
const [builderMode, setBuilderMode] = useState<'create' | 'modify'>('create')
|
||||
const [showStore, setShowStore] = useState(false)
|
||||
const [showComposite, setShowComposite] = useState(false)
|
||||
const { pool, addToPool, removeFromPool, reorderPool, prune } = useStrategyPool()
|
||||
const { pool, addToPool, removeFromPool, reorderPool, prune } = useStrategyPool(timeframe)
|
||||
const [cardSize, setCardSize] = useState<CardSize>(loadCardSize)
|
||||
// 日k蜡烛图显示开关(仅当 candle 列可见时才有意义;持久化)
|
||||
const [dailyKChartVisible, setDailyKChartVisible] = useState<boolean>(() => storage.screenerCandle.get(true))
|
||||
@@ -203,14 +203,13 @@ export function Screener() {
|
||||
// 关键: 仅当本次拉取成功且返回非空列表时才 prune。
|
||||
// 拉取中/失败/返回空(如引擎 reload 瞬时把某策略跳过)时一律不碰池,
|
||||
// 否则会把用户池里仍有效的 ID 永久清空并写入 localStorage,导致卡片全没。
|
||||
// 日线/分钟池按周期隔离, 各自用自身周期的列表清理, 互不影响。
|
||||
useEffect(() => {
|
||||
if (strategies.isError) return // 拉取失败: 不 prune
|
||||
if (!strategies.isSuccess) return // 加载中: 不 prune
|
||||
if (allStrategyIds.size === 0) return // 空列表: 不 prune
|
||||
// 分钟模式的列表只含分钟策略, prune 会误删池中的日线策略 → 仅日线模式清理
|
||||
if (timeframe !== '1d') return
|
||||
prune(allStrategyIds)
|
||||
}, [allStrategyIds, prune, strategies.isError, strategies.isSuccess, timeframe])
|
||||
}, [allStrategyIds, prune, strategies.isError, strategies.isSuccess])
|
||||
|
||||
// 策略文件加载失败时提示用户(避免"策略静默消失"被误判为正常)
|
||||
const loadErrors = strategies.data?.load_errors ?? []
|
||||
@@ -1056,7 +1055,7 @@ export function Screener() {
|
||||
if (!data.presets.some(s => s.id === id)) {
|
||||
throw new Error(`策略 ${id} 已保存但未加载,请检查策略代码`)
|
||||
}
|
||||
addToPool(id)
|
||||
addToPool(id, '1d') // 构建器创建的是日线策略, 即使在分钟页保存也入日线池
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1065,7 +1064,7 @@ export function Screener() {
|
||||
onClose={() => setShowComposite(false)}
|
||||
onSavedId={async id => {
|
||||
await qc.fetchQuery({ queryKey: QK.screenerStrategies('all'), queryFn: () => api.screenerStrategies(), staleTime: 0 })
|
||||
addToPool(id)
|
||||
addToPool(id, '1d') // 叠加策略为日线策略, 固定入日线池
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user