From 92ed248c8a22a2507e4a45790e10e6c595c7e2ff Mon Sep 17 00:00:00 2001 From: wangzhenjia Date: Sun, 20 Sep 2026 11:40:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(screening):=20=E6=9C=80=E6=96=B0=20K=20?= =?UTF-8?q?=E7=BA=BF=E6=88=90=E4=BA=A4=E9=87=8F=E7=BC=BA=E5=A4=B1=E6=97=B6?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=8A=8A=E5=89=8D=E4=B8=80=E6=97=A5=E9=87=8F?= =?UTF-8?q?=E6=AF=94=E5=BD=93=E4=BD=9C=E4=BB=8A=E6=97=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit high-volume 先用 toFiniteNumber 过滤掉缺失成交量再做切片,索引会整体前移: 当最后一根(当日)K 线成交量缺失时,today 实际取到的是前一日成交量, baseline 窗口也一并错位,于是把昨日的量比标成"今日"并触发候选。 现在按时间戳锚定最新一根 K 线:其成交量未知时直接放弃 kline 量比(返回 undefined,不猜测),基准窗口要求完整的 20 根有效成交量。CalcIndex volumeRatio 优先路径与同币种正常路径均不变。 新增回归测试覆盖"昨日放量、今日成交量 NaN"场景,修复前该测试失败。 --- packages/shared/src/screening/strategies.test.ts | 11 +++++++++++ packages/shared/src/screening/strategies.ts | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/screening/strategies.test.ts b/packages/shared/src/screening/strategies.test.ts index 6f0c0c1f..6903c676 100644 --- a/packages/shared/src/screening/strategies.test.ts +++ b/packages/shared/src/screening/strategies.test.ts @@ -197,6 +197,17 @@ describe('market-movers rules', () => { expect(rule.compute(makeContext({ kline: bars(3), valuation: { symbol: 'AAPL.US' } }))).toBeNull() }) + it('high-volume skips the kline ratio when the latest bar volume is unknown', () => { + const rule = getScreeningStrategy('high-volume')! + const klines = bars(30, 100, 1, 100_000) + // Yesterday spiked, but today's (latest bar) volume is missing/NaN — + // the ratio must stay unknown instead of labelling yesterday's spike as today. + klines[klines.length - 2] = { ...klines[klines.length - 2], volume: 400_000 } + klines[klines.length - 1] = { ...klines[klines.length - 1], volume: Number.NaN } + const result = rule.compute(makeContext({ kline: klines, valuation: { symbol: 'AAPL.US' } })) + expect(result).toBeNull() + }) + it('unusual-movement flags amplitude far above the 20d average', () => { const rule = getScreeningStrategy('unusual-movement')! const klines = bars(30, 100, 0, 1_000_000) // flat: amplitude ~2% per bar diff --git a/packages/shared/src/screening/strategies.ts b/packages/shared/src/screening/strategies.ts index 189030d5..553fa846 100644 --- a/packages/shared/src/screening/strategies.ts +++ b/packages/shared/src/screening/strategies.ts @@ -296,10 +296,15 @@ const highVolume: ScreeningStrategyDef = { ? [...ctx.data.kline].sort((a, b) => a.timestamp - b.timestamp).map((bar) => toFiniteNumber(bar.volume)) : [] const klineRatio = (() => { - const finite = barVolumes.filter((value): value is number => value !== undefined) - if (finite.length <= BREAKOUT_WINDOW) return undefined - const today = finite[finite.length - 1] - const baseline = finite.slice(-BREAKOUT_WINDOW - 1, -1) + // "Today" must be the actual latest bar by timestamp. Filtering out + // unknown volumes first would shift the index and mislabel yesterday's + // volume as today's, so the latest bar's volume is required to be known. + const today = barVolumes[barVolumes.length - 1] + if (today === undefined) return undefined + const baseline = barVolumes + .slice(-BREAKOUT_WINDOW - 1, -1) + .filter((value): value is number => value !== undefined) + if (baseline.length < BREAKOUT_WINDOW) return undefined const avg = baseline.reduce((acc, value) => acc + value, 0) / baseline.length if (avg === 0) return undefined return today / avg