← Back
import { describe, it, expect } from 'vitest'
import {
  detectPinBar,
  confirmSweep,
  scoreConfidence,
  findRoundNumbers,
  mergeLevels,
  clusterLevels,
} from '../server/liq-sweep.js'

// ---- detectPinBar ----
describe('detectPinBar', () => {
  it('detects bullish pin bar (long lower wick)', () => {
    // open=100, high=101, low=90, close=100.5
    // range=11, lowerWick=10, body=0.5
    const candle = { open: 100, high: 101, low: 90, close: 100.5 }
    const result = detectPinBar(candle)
    expect(result).not.toBeNull()
    expect(result.direction).toBe('LONG')
    expect(result.wickRatio).toBeGreaterThan(0.6)
    expect(result.bodyRatio).toBeLessThan(0.33)
  })

  it('detects bearish pin bar (long upper wick)', () => {
    // open=100, high=111, low=99.5, close=100
    // range=11.5, upperWick=11, body=0
    const candle = { open: 100, high: 111, low: 99.5, close: 100 }
    const result = detectPinBar(candle)
    expect(result).not.toBeNull()
    expect(result.direction).toBe('SHORT')
    expect(result.wickRatio).toBeGreaterThan(0.6)
  })

  it('returns null for doji (no significant wick)', () => {
    const candle = { open: 100, high: 100.5, low: 99.5, close: 100.1 }
    const result = detectPinBar(candle)
    expect(result).toBeNull()
  })

  it('returns null for big body candle', () => {
    // body is most of range
    const candle = { open: 90, high: 101, low: 89, close: 100 }
    const result = detectPinBar(candle)
    expect(result).toBeNull()
  })

  it('returns null for zero-range candle', () => {
    const candle = { open: 100, high: 100, low: 100, close: 100 }
    expect(detectPinBar(candle)).toBeNull()
  })

  it('returns null for null input', () => {
    expect(detectPinBar(null)).toBeNull()
  })

  it('filters tiny candles when prevCandles provided', () => {
    // Previous candles have range ~10, but this candle has range ~1 (noise)
    const prevCandles = [
      { high: 110, low: 100 },
      { high: 112, low: 101 },
      { high: 109, low: 99 },
    ]
    // Tiny candle with pin bar shape but too small range
    const candle = { open: 100, high: 100.3, low: 99, close: 100.2 }
    const result = detectPinBar(candle, prevCandles)
    expect(result).toBeNull()
  })

  it('allows custom wick/body ratios', () => {
    // Candle with 50% wick — normally not enough, but with lowered threshold
    const candle = { open: 100, high: 100.5, low: 95, close: 100 }
    // range=5.5, lowerWick=5, bodyRatio=0 — wick is ~0.91
    const strict = detectPinBar(candle, [], { wickMinRatio: 0.95 })
    const relaxed = detectPinBar(candle, [], { wickMinRatio: 0.5 })
    expect(strict).toBeNull()
    expect(relaxed).not.toBeNull()
  })
})

// ---- confirmSweep ----
describe('confirmSweep', () => {
  it('confirms bullish sweep (wick below level, close above)', () => {
    // low=98.5, level=99 → penetration = (99-98.5)/99*100 = 0.505% (within 1.5%)
    const candle = { open: 100, high: 101, low: 98.5, close: 100.5 }
    const pinBar = { direction: 'LONG' }
    const levels = [
      { price: 99, type: 'swing_low', strength: 5, source: 'swing' },
    ]
    const result = confirmSweep(candle, pinBar, levels)
    expect(result).not.toBeNull()
    expect(result.sweptLevel).toBe(99)
    expect(result.levelType).toBe('swing_low')
    expect(result.sweepDepthPct).toBeGreaterThan(0)
    expect(result.sweepDepthPct).toBeLessThanOrEqual(1.5)
    expect(result.levelsSwept).toBe(1)
  })

  it('confirms bearish sweep (wick above level, close below)', () => {
    // high=101.5, level=101 → penetration = (101.5-101)/101*100 = 0.495%
    const candle = { open: 100, high: 101.5, low: 99.5, close: 99.8 }
    const pinBar = { direction: 'SHORT' }
    const levels = [
      { price: 101, type: 'swing_high', strength: 7, source: 'swing' },
    ]
    const result = confirmSweep(candle, pinBar, levels)
    expect(result).not.toBeNull()
    expect(result.sweptLevel).toBe(101)
    expect(result.sweepDepthPct).toBeGreaterThan(0)
    expect(result.sweepDepthPct).toBeLessThanOrEqual(1.5)
  })

  it('returns null when no levels swept', () => {
    const candle = { open: 100, high: 101, low: 99, close: 100.5 }
    const pinBar = { direction: 'LONG' }
    const levels = [
      { price: 90, type: 'swing_low', strength: 5, source: 'swing' }, // too far below
    ]
    expect(confirmSweep(candle, pinBar, levels)).toBeNull()
  })

  it('picks strongest level when multiple swept', () => {
    // low=98, levels at 99 and 98.5 → penetration < 1.5% for both
    const candle = { open: 100, high: 101, low: 98, close: 100.5 }
    const pinBar = { direction: 'LONG' }
    const levels = [
      { price: 99, type: 'swing_low', strength: 3, source: 'swing' },
      { price: 98.5, type: 'round_number', strength: 8, source: 'round' },
    ]
    const result = confirmSweep(candle, pinBar, levels)
    expect(result).not.toBeNull()
    expect(result.strength).toBe(8) // picks the stronger level
    expect(result.levelsSwept).toBe(2)
  })

  it('rejects too-deep penetration', () => {
    const candle = { open: 100, high: 101, low: 80, close: 100.5 }
    const pinBar = { direction: 'LONG' }
    const levels = [
      { price: 97, type: 'swing_low', strength: 5, source: 'swing' },
    ]
    // penetration = (97 - 80) / 97 * 100 = 17.5% — way over default 1.5%
    expect(confirmSweep(candle, pinBar, levels)).toBeNull()
  })

  it('returns null for null/empty inputs', () => {
    expect(confirmSweep(null, null, [])).toBeNull()
    expect(confirmSweep({}, { direction: 'LONG' }, [])).toBeNull()
  })
})

// ---- scoreConfidence ----
describe('scoreConfidence', () => {
  it('returns base score for minimal input', () => {
    const score = scoreConfidence({ wickRatio: 0.6, levelStrength: 1 })
    expect(score).toBeGreaterThanOrEqual(30)
    expect(score).toBeLessThanOrEqual(95)
  })

  it('increases with better wick ratio', () => {
    const low = scoreConfidence({ wickRatio: 0.6 })
    const high = scoreConfidence({ wickRatio: 0.9 })
    expect(high).toBeGreaterThan(low)
  })

  it('increases with stronger level', () => {
    const weak = scoreConfidence({ levelStrength: 1 })
    const strong = scoreConfidence({ levelStrength: 10 })
    expect(strong).toBeGreaterThan(weak)
  })

  it('increases with volume spike', () => {
    const noVol = scoreConfidence({ volumeRatio: null })
    const volSpike = scoreConfidence({ volumeRatio: 5 })
    expect(volSpike).toBeGreaterThan(noVol)
  })

  it('increases with OI drop', () => {
    const noOi = scoreConfidence({ oiChangePct: null })
    const oiDrop = scoreConfidence({ oiChangePct: -2 })
    expect(oiDrop).toBeGreaterThan(noOi)
  })

  it('increases with counter-trend context', () => {
    const none = scoreConfidence({ trendContext: null })
    const counter = scoreConfidence({ trendContext: 'counter' })
    expect(counter).toBeGreaterThan(none)
  })

  it('increases with wall absorbed', () => {
    const no = scoreConfidence({ wallAbsorbed: false })
    const yes = scoreConfidence({ wallAbsorbed: true })
    expect(yes).toBeGreaterThan(no)
  })

  it('clamps to 30-95 range', () => {
    // Minimum possible
    const min = scoreConfidence({ wickRatio: 0.6, levelStrength: 0 })
    expect(min).toBeGreaterThanOrEqual(30)

    // Maximum possible — all bonuses
    const max = scoreConfidence({
      wickRatio: 0.95,
      levelStrength: 10,
      levelsSwept: 3,
      volumeRatio: 10,
      oiChangePct: -5,
      trendContext: 'counter',
      fundingContext: 'extreme',
      wallAbsorbed: true,
    })
    expect(max).toBeLessThanOrEqual(95)
  })
})

// ---- findRoundNumbers ----
describe('findRoundNumbers', () => {
  it('returns round levels near BTC price', () => {
    const levels = findRoundNumbers(60500, 2)
    expect(levels.length).toBeGreaterThan(0)
    // Should include 60000 and 61000
    const prices = levels.map(l => l.price)
    expect(prices).toContain(60000)
    expect(prices).toContain(61000)
  })

  it('assigns higher strength to full round levels', () => {
    const levels = findRoundNumbers(60500, 2)
    const full = levels.find(l => l.price === 60000)
    const half = levels.find(l => l.price === 60500)
    expect(full.strength).toBeGreaterThan(half.strength)
  })

  it('handles small altcoin prices', () => {
    const levels = findRoundNumbers(1.5, 2)
    expect(levels.length).toBeGreaterThan(0)
    levels.forEach(l => {
      expect(l.type).toBe('round_number')
      expect(l.source).toBe('round')
    })
  })

  it('returns empty for zero/negative price', () => {
    expect(findRoundNumbers(0)).toEqual([])
    expect(findRoundNumbers(-10)).toEqual([])
  })

  it('respects windowPct', () => {
    const narrow = findRoundNumbers(60500, 0.5)
    const wide = findRoundNumbers(60500, 5)
    expect(wide.length).toBeGreaterThan(narrow.length)
  })
})

// ---- mergeLevels ----
describe('mergeLevels', () => {
  it('merges levels within 0.15% keeping stronger', () => {
    const levels = [
      { price: 100.00, strength: 3, type: 'swing_low' },
      { price: 100.10, strength: 7, type: 'round_number' }, // within 0.1%
    ]
    const merged = mergeLevels(levels)
    expect(merged.length).toBe(1)
    expect(merged[0].strength).toBe(7) // kept stronger
  })

  it('keeps distant levels separate', () => {
    const levels = [
      { price: 100, strength: 3, type: 'swing_low' },
      { price: 105, strength: 5, type: 'swing_high' },
    ]
    const merged = mergeLevels(levels)
    expect(merged.length).toBe(2)
  })

  it('returns empty for empty input', () => {
    expect(mergeLevels([])).toEqual([])
  })

  it('handles single level', () => {
    const levels = [{ price: 100, strength: 5, type: 'swing_low' }]
    const merged = mergeLevels(levels)
    expect(merged.length).toBe(1)
  })
})

// ---- clusterLevels ----
describe('clusterLevels', () => {
  it('clusters nearby same-type swings', () => {
    const raws = [
      { price: 100, type: 'swing_high', time: 1000, volume: 100 },
      { price: 100.1, type: 'swing_high', time: 2000, volume: 200 },
    ]
    const result = clusterLevels(raws, 0.0015)
    expect(result.length).toBe(1)
    expect(result[0].touches).toBe(2)
    expect(result[0].price).toBe(100.1) // freshest
  })

  it('does not cluster different types', () => {
    const raws = [
      { price: 100, type: 'swing_high', time: 1000, volume: 100 },
      { price: 100.1, type: 'swing_low', time: 2000, volume: 200 },
    ]
    const result = clusterLevels(raws, 0.0015)
    expect(result.length).toBe(2)
  })

  it('returns empty for empty input', () => {
    expect(clusterLevels([], 0.0015)).toEqual([])
  })

  it('strength increases with touches', () => {
    const raws = [
      { price: 100, type: 'swing_high', time: 1000, volume: 100 },
      { price: 100.05, type: 'swing_high', time: 2000, volume: 100 },
      { price: 100.1, type: 'swing_high', time: 3000, volume: 100 },
    ]
    const result = clusterLevels(raws, 0.0015)
    expect(result[0].touches).toBe(3)
    expect(result[0].strength).toBe(7) // min(10, 3*2+1)
  })
})

📜 Git History

4492574fix+test: comprehensive code audit — 11 bugfixes + 148 new tests8 weeks ago
Show last diff
Loading...