Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/shared/src/screening/strategies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions packages/shared/src/screening/strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading