TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import { marketDepth } from './depth.js';3import { bandFor, fairPrices, liquidationModel, type ListingLifecycle } from './liquidation.js';4import { classifySale, countVerification } from './verification.js';56describe('marketDepth (§25–§26)', () => {7 it('counts asks around RIV and derives the spread from the best ask', () => {8 const d = marketDepth([950, 1000, 1040, 1150, 1300, 700], 1000)!;9 expect(d.asks).toBe(6);10 expect(d.within5).toBe(3); // 950, 1000, 104011 expect(d.within10).toBe(3);12 expect(d.within20).toBe(4); // + 115013 expect(d.belowRiv).toBe(2);14 expect(d.aboveRiv).toBe(3);15 expect(d.lowestAsk).toBe(700);16 expect(d.medianAsk).toBe(1020);17 expect(d.askRivSpread).toBeCloseTo(-0.3, 4);18 });19 it('returns null without a valuation and no spread without asks', () => {20 expect(marketDepth([100], null)).toBeNull();21 expect(marketDepth([], 100)).toMatchObject({ asks: 0, askRivSpread: null, lowestAsk: null, medianAsk: null });22 expect(marketDepth([null, -5, 0], 100)!.asks).toBe(0);23 });24});2526const lc = (days: number, outcome: ListingLifecycle['outcome'], askToRiv?: number): ListingLifecycle => ({ firstSeen: new Date('2026-01-01T00:00:00Z'), lastSeen: new Date(Date.UTC(2026, 0, 1 + days)), outcome, askToRiv });2728describe('liquidationModel (§24, §30)', () => {29 it('separates sold from withdrawn and needs 5 observations per band', () => {30 const rows = [...Array(6)].map((_, i) => lc(3 + i, 'sold', 0.85)).concat([...Array(6)].map((_, i) => lc(20 + i, 'sold', 1.05)), [lc(9, 'sold', 0.95), lc(11, 'sold', 0.97)], [lc(40, 'ended', 1.2), lc(35, 'removed', 1.3)]);31 const m = liquidationModel(rows);32 expect(m.sold.n).toBe(14);33 expect(m.withdrawn.n).toBe(2);34 expect(m.withdrawn.median).toBe(37.5);35 expect(m.sellThrough).toBeCloseTo(14 / 16, 3);36 const byBand = Object.fromEntries(m.bands.map((b) => [b.band, b]));37 expect(byBand.le90!.medianDays).toBe(5.5);38 expect(byBand.b100_110!.medianDays).toBe(22.5);39 expect(byBand.b90_100!.n).toBe(2);40 expect(byBand.b90_100!.medianDays).toBeNull(); // not enough data41 expect(byBand.gt110!.medianDays).toBeNull(); // withdrawn never count as sales42 });43 it('bands by ask / RIV ratio', () => {44 expect(bandFor(0.5)).toBe('le90');45 expect(bandFor(0.9)).toBe('le90');46 expect(bandFor(0.95)).toBe('b90_100');47 expect(bandFor(1.0)).toBe('b90_100');48 expect(bandFor(1.1)).toBe('b100_110');49 expect(bandFor(2)).toBe('gt110');50 });51});5253describe('fairPrices (§213–§214)', () => {54 it('uses only the valuation band and observed time-to-sale', () => {55 const m = liquidationModel([...Array(5)].map((_, i) => lc(4 + i, 'sold', 0.88)).concat([...Array(5)].map((_, i) => lc(15 + i, 'sold', 0.98))));56 const f = fairPrices({ riv: 1000, low: 880, high: 1120 }, m);57 expect(f.buyAggressive).toBe(880);58 expect(f.buyFair).toBe(1000);59 expect(f.sellFast).toBe(900); // max(low 880, 0.9 × RIV)60 expect(f.sellFastDays).toBe(6);61 expect(f.sellTypical).toBe(1000);62 expect(f.sellTypicalDays).toBe(17);63 expect(f.sellPatient).toBe(1120);64 expect(f.sellPatientDays).toBeNull();65 });66 it('leaves time components null without band evidence', () => {67 const f = fairPrices({ riv: 1000, low: 900, high: 1100 }, liquidationModel([lc(3, 'sold', 0.9)]));68 expect(f.sellFast).toBeNull();69 expect(f.sellFastDays).toBeNull();70 expect(f.sellTypical).toBe(1000);71 expect(fairPrices({ riv: null, low: null, high: null }, null)).toMatchObject({ buyFair: null, sellPatient: null });72 });73});7475describe('classifySale (§39)', () => {76 it('labels by status, confidence, source and sale type', () => {77 expect(classifySale({ status: 'excluded', confidence: 0.9, flags: ['bundle'] }).label).toBe('excluded');78 expect(classifySale({ status: 'flagged', confidence: 0.9, flags: ['abnormally_high_price'] })).toMatchObject({ label: 'unverified' });79 expect(classifySale({ status: 'valid', confidence: 0.5, trust: 0.9 }).label).toBe('unverified');80 expect(classifySale({ status: 'valid', confidence: 0.9, trust: 0.3 }).label).toBe('unverified');81 expect(classifySale({ status: 'valid', confidence: 0.7, sourceType: 'auction_house', trust: 0.7 }).label).toBe('verified');82 expect(classifySale({ status: 'valid', confidence: 0.85, saleType: 'auction', sourceType: 'marketplace', trust: 0.6 }).label).toBe('verified');83 expect(classifySale({ status: 'valid', confidence: 0.9, sourceType: 'marketplace', saleType: 'fixed_price', trust: 0.85 }).label).toBe('verified');84 expect(classifySale({ status: 'valid', confidence: 0.8, sourceType: 'marketplace', saleType: 'fixed_price', trust: 0.6 }).label).toBe('likely');85 });86 it('counts labels', () => {87 expect(countVerification(['verified', 'likely', 'likely', 'excluded'])).toEqual({ verified: 1, likely: 2, unverified: 0, excluded: 1 });88 });89});90