← Back
import { describe, it, expect } from 'vitest'
import {
  getBucketSize,
  bucketLevels,
  detectWalls,
  clusterWalls,
  calcImbalance,
  enrichWithPersistence,
  cleanupPersistence,
  scoreWall,
  analyzeSymbol
} from '../server/densityV2.js'

// ---- getBucketSize ----
describe('getBucketSize', () => {
  it('returns 0.05% of price', () => {
    expect(getBucketSize(60000)).toBeCloseTo(30, 1)    // BTC
    expect(getBucketSize(3500)).toBeCloseTo(1.75, 2)   // ETH
    expect(getBucketSize(0.05)).toBeCloseTo(0.000025, 6) // small alt
  })

  it('handles zero price', () => {
    expect(getBucketSize(0)).toBe(0)
  })
})

// ---- bucketLevels ----
describe('bucketLevels', () => {
  const levels = [
    { price: 100.1, notional: 5000, firstSeen: 1000, lastUpdate: 2000 },
    { price: 100.2, notional: 3000, firstSeen: 900,  lastUpdate: 2100 },
    { price: 105.0, notional: 7000, firstSeen: 1100, lastUpdate: 2200 },
  ]

  it('groups levels into buckets', () => {
    const buckets = bucketLevels(levels, 1.0) // $1 buckets
    expect(buckets.length).toBeGreaterThanOrEqual(2) // 100.x and 105.x in different buckets
  })

  it('aggregates notional within a bucket', () => {
    const buckets = bucketLevels(levels, 1.0)
    // 100.1 and 100.2 should be in the same bucket
    const bucket100 = buckets.find(b => b.anchorPrice >= 100 && b.anchorPrice <= 101)
    expect(bucket100).toBeDefined()
    expect(bucket100.totalNotional).toBe(8000) // 5000 + 3000
    expect(bucket100.levelCount).toBe(2)
  })

  it('tracks oldestSeen as minimum firstSeen', () => {
    const buckets = bucketLevels(levels, 1.0)
    const bucket100 = buckets.find(b => b.anchorPrice >= 100 && b.anchorPrice <= 101)
    expect(bucket100.oldestSeen).toBe(900) // min of 1000, 900
  })

  it('tracks newestUpdate as maximum lastUpdate', () => {
    const buckets = bucketLevels(levels, 1.0)
    const bucket100 = buckets.find(b => b.anchorPrice >= 100 && b.anchorPrice <= 101)
    expect(bucket100.newestUpdate).toBe(2100)
  })

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

  it('tracks min/max price within bucket', () => {
    const buckets = bucketLevels(levels, 1.0)
    const bucket100 = buckets.find(b => b.anchorPrice >= 100 && b.anchorPrice <= 101)
    expect(bucket100.minPrice).toBe(100.1)
    expect(bucket100.maxPrice).toBe(100.2)
  })
})

// ---- detectWalls ----
describe('detectWalls', () => {
  it('detects statistical outliers', () => {
    const buckets = [
      { totalNotional: 100, anchorPrice: 99 },
      { totalNotional: 120, anchorPrice: 100 },
      { totalNotional: 110, anchorPrice: 101 },
      { totalNotional: 90,  anchorPrice: 102 },
      { totalNotional: 5000, anchorPrice: 103 }, // outlier
    ]
    const { walls, median, stddev } = detectWalls(buckets, 2)
    expect(walls.length).toBe(1)
    expect(walls[0].anchorPrice).toBe(103)
    expect(walls[0].sizeVsMedian).toBeGreaterThan(10)
    expect(median).toBeGreaterThan(0)
    expect(stddev).toBeGreaterThan(0)
  })

  it('returns no walls if all values similar', () => {
    const buckets = [
      { totalNotional: 100, anchorPrice: 99 },
      { totalNotional: 105, anchorPrice: 100 },
      { totalNotional: 98,  anchorPrice: 101 },
      { totalNotional: 102, anchorPrice: 102 },
    ]
    const { walls } = detectWalls(buckets, 2)
    expect(walls.length).toBe(0)
  })

  it('handles fewer than 3 buckets', () => {
    const { walls } = detectWalls([{ totalNotional: 100 }], 2)
    expect(walls).toEqual([])
  })

  it('handles null/empty', () => {
    expect(detectWalls(null).walls).toEqual([])
    expect(detectWalls([]).walls).toEqual([])
  })

  it('caps sizeVsMedian at 99.9', () => {
    const buckets = [
      { totalNotional: 1, anchorPrice: 99 },
      { totalNotional: 1, anchorPrice: 100 },
      { totalNotional: 1, anchorPrice: 101 },
      { totalNotional: 1000000, anchorPrice: 102 },
    ]
    const { walls } = detectWalls(buckets, 2)
    expect(walls[0].sizeVsMedian).toBe(99.9)
  })
})

// ---- clusterWalls ----
describe('clusterWalls', () => {
  it('merges adjacent walls', () => {
    const walls = [
      { anchorPrice: 100.5, totalNotional: 5000, levelCount: 3, minPrice: 100, maxPrice: 101, oldestSeen: 1000, newestUpdate: 2000, sizeVsMedian: 10 },
      { anchorPrice: 101.5, totalNotional: 3000, levelCount: 2, minPrice: 101, maxPrice: 102, oldestSeen: 1100, newestUpdate: 2100, sizeVsMedian: 8 },
    ]
    const clusters = clusterWalls(walls, 1.0, 2)
    expect(clusters.length).toBe(1)
    expect(clusters[0].totalNotional).toBe(8000)
    expect(clusters[0].levelCount).toBe(5)
  })

  it('keeps distant walls separate', () => {
    const walls = [
      { anchorPrice: 100.5, totalNotional: 5000, levelCount: 3, minPrice: 100, maxPrice: 101, oldestSeen: 1000, newestUpdate: 2000, sizeVsMedian: 10 },
      { anchorPrice: 110.5, totalNotional: 3000, levelCount: 2, minPrice: 110, maxPrice: 111, oldestSeen: 1100, newestUpdate: 2100, sizeVsMedian: 8 },
    ]
    const clusters = clusterWalls(walls, 1.0, 2)
    expect(clusters.length).toBe(2)
  })

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

// ---- calcImbalance ----
describe('calcImbalance', () => {
  it('returns 0 for equal bids and asks', () => {
    const bids = [{ notional: 100 }, { notional: 200 }]
    const asks = [{ notional: 150 }, { notional: 150 }]
    expect(calcImbalance(bids, asks)).toBe(0)
  })

  it('returns positive for bid-heavy', () => {
    const bids = [{ notional: 800 }]
    const asks = [{ notional: 200 }]
    const imb = calcImbalance(bids, asks)
    expect(imb).toBeCloseTo(0.6, 2)
  })

  it('returns negative for ask-heavy', () => {
    const bids = [{ notional: 200 }]
    const asks = [{ notional: 800 }]
    const imb = calcImbalance(bids, asks)
    expect(imb).toBeCloseTo(-0.6, 2)
  })

  it('returns 0 for empty arrays', () => {
    expect(calcImbalance([], [])).toBe(0)
  })
})

// ---- enrichWithPersistence ----
describe('enrichWithPersistence', () => {
  it('marks new wall as "new"', () => {
    const map = new Map()
    const wall = { anchorPrice: 100, totalNotional: 5000 }
    const enriched = enrichWithPersistence(wall, map, 'BTCUSDT', 'bid', 100)
    expect(enriched.status).toBe('new')
    expect(enriched.ageMins).toBe(0)
    expect(map.size).toBe(1)
  })

  it('marks wall >3min as "confirmed"', () => {
    const map = new Map()
    const bucketSize = getBucketSize(100)
    const bucketIdx = Math.floor(100 / bucketSize)
    // Pre-seed a record 4 minutes old
    map.set(`BTCUSDT:bid:${bucketIdx}`, {
      firstSeen: Date.now() - 4 * 60000,
      lastSeen: Date.now() - 1000,
      peakNotional: 5000
    })
    const wall = { anchorPrice: 100, totalNotional: 5500 }
    const enriched = enrichWithPersistence(wall, map, 'BTCUSDT', 'bid', 100)
    expect(enriched.status).toBe('confirmed')
    expect(enriched.ageMins).toBeGreaterThanOrEqual(3)
  })

  it('marks wall >10min as "strong"', () => {
    const map = new Map()
    const bucketSize = getBucketSize(100)
    const bucketIdx = Math.floor(100 / bucketSize)
    map.set(`BTCUSDT:ask:${bucketIdx}`, {
      firstSeen: Date.now() - 15 * 60000,
      lastSeen: Date.now() - 1000,
      peakNotional: 4000
    })
    const wall = { anchorPrice: 100, totalNotional: 6000 }
    const enriched = enrichWithPersistence(wall, map, 'BTCUSDT', 'ask', 100)
    expect(enriched.status).toBe('strong')
    expect(enriched.peakNotional).toBe(6000) // updated peak
  })

  it('fuzzy matches ±1 bucket offset', () => {
    const map = new Map()
    const bucketSize = getBucketSize(100)
    const bucketIdx = Math.floor(100 / bucketSize)
    // Seed at neighboring bucket
    map.set(`BTCUSDT:bid:${bucketIdx - 1}`, {
      firstSeen: Date.now() - 5 * 60000,
      lastSeen: Date.now() - 500,
      peakNotional: 4000
    })
    const wall = { anchorPrice: 100, totalNotional: 5000 }
    const enriched = enrichWithPersistence(wall, map, 'BTCUSDT', 'bid', 100)
    expect(enriched.status).toBe('confirmed')
    // Old key should be migrated
    expect(map.has(`BTCUSDT:bid:${bucketIdx - 1}`)).toBe(false)
    expect(map.has(`BTCUSDT:bid:${bucketIdx}`)).toBe(true)
  })
})

// ---- cleanupPersistence ----
describe('cleanupPersistence', () => {
  it('removes entries older than 5 minutes', () => {
    const map = new Map()
    map.set('A', { lastSeen: Date.now() - 400000 }) // >5min
    map.set('B', { lastSeen: Date.now() - 100000 }) // <5min
    cleanupPersistence(map)
    expect(map.has('A')).toBe(false)
    expect(map.has('B')).toBe(true)
  })

  it('enforces hard cap of 10000', () => {
    const map = new Map()
    const now = Date.now()
    for (let i = 0; i < 10500; i++) {
      map.set(`key${i}`, { lastSeen: now }) // all fresh
    }
    cleanupPersistence(map)
    expect(map.size).toBeLessThanOrEqual(10000)
  })
})

// ---- scoreWall ----
describe('scoreWall', () => {
  it('scores higher for closer walls', () => {
    const close = scoreWall({ anchorPrice: 100, sizeVsMedian: 10, status: 'new' }, 101)
    const far = scoreWall({ anchorPrice: 100, sizeVsMedian: 10, status: 'new' }, 110)
    expect(close).toBeGreaterThan(far)
  })

  it('scores higher for larger walls', () => {
    const big = scoreWall({ anchorPrice: 100, sizeVsMedian: 20, status: 'new' }, 101)
    const small = scoreWall({ anchorPrice: 100, sizeVsMedian: 5, status: 'new' }, 101)
    expect(big).toBeGreaterThan(small)
  })

  it('multiplies persistence bonus', () => {
    const base = scoreWall({ anchorPrice: 100, sizeVsMedian: 10, status: 'new' }, 101)
    const confirmed = scoreWall({ anchorPrice: 100, sizeVsMedian: 10, status: 'confirmed' }, 101)
    const strong = scoreWall({ anchorPrice: 100, sizeVsMedian: 10, status: 'strong' }, 101)
    expect(confirmed).toBeGreaterThan(base)
    expect(strong).toBeGreaterThan(confirmed)
  })

  it('caps sizeVsMedian at 50', () => {
    const capped = scoreWall({ anchorPrice: 100, sizeVsMedian: 100, status: 'new' }, 101)
    const at50 = scoreWall({ anchorPrice: 100, sizeVsMedian: 50, status: 'new' }, 101)
    expect(capped).toBe(at50)
  })
})

// ---- analyzeSymbol (integration) ----
describe('analyzeSymbol', () => {
  it('returns full analysis structure', () => {
    // Build fake order book: one big bid wall + uniform noise
    const bidLevels = []
    const askLevels = []
    const markPrice = 100

    // Normal noise levels
    for (let i = 0; i < 50; i++) {
      bidLevels.push({ price: 99 - i * 0.01, notional: 100, firstSeen: Date.now(), lastUpdate: Date.now() })
      askLevels.push({ price: 101 + i * 0.01, notional: 100, firstSeen: Date.now(), lastUpdate: Date.now() })
    }
    // Big bid wall at 99.5
    bidLevels.push({ price: 99.5, notional: 50000, firstSeen: Date.now(), lastUpdate: Date.now() })

    const result = analyzeSymbol({
      symbol: 'TESTUSDT',
      markPrice,
      bidLevels,
      askLevels,
      persistenceMap: new Map(),
      windowPct: 2,
      nSigma: 2
    })

    expect(result.symbol).toBe('TESTUSDT')
    expect(result.markPrice).toBe(100)
    expect(result.imbalance).toBeDefined()
    expect(result.imbalanceLabel).toMatch(/BULLISH|BEARISH|NEUTRAL/)
    expect(result.wallCount).toBeGreaterThanOrEqual(1)
    expect(result.support).not.toBeNull()
    expect(result.stats).toBeDefined()
    expect(result.stats.bucketSize).toBeGreaterThan(0)
  })

  it('returns null support/resistance when no walls', () => {
    // All levels identical → no outliers
    const levels = Array.from({ length: 10 }, (_, i) => ({
      price: 100 + i * 0.01, notional: 100, firstSeen: Date.now(), lastUpdate: Date.now()
    }))
    const result = analyzeSymbol({
      symbol: 'TESTUSDT',
      markPrice: 100.05,
      bidLevels: levels.filter(l => l.price < 100.05),
      askLevels: levels.filter(l => l.price >= 100.05),
      persistenceMap: new Map(),
    })
    expect(result.support).toBeNull()
    expect(result.resistance).toBeNull()
    expect(result.wallCount).toBe(0)
  })
})

📜 Git History

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