TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import { computeValuation } from './valuation.js';3import { detectOutliers } from './outliers.js';4import { adjustmentFactor, computeGradePremiums } from './premiums.js';5import { assessAsk, dealScore, isPlausibleChange, liquidityScore, rarityScore, momentumScore, valueOpportunity, trendingScore } from './scores.js';67const now = new Date('2026-09-01T00:00:00Z');8const d = (daysAgo: number) => new Date(now.getTime() - daysAgo * 86_400_000);9const sale = (id: string, price: number, daysAgo: number, extra: Partial<Parameters<typeof computeValuation>[0]['sales'][number]> = {}) => ({ id, priceUsd: price, date: d(daysAgo), trust: 0.8, confidence: 0.9, ...extra });1011describe('computeValuation', () => {12 it('returns no number without evidence', () => {13 const v = computeValuation({ sales: [], now });14 expect(v.riv).toBeNull();15 expect(v.label).toBe('insufficient');16 expect(v.basis).toBe('none');17 });18 it('builds a robust ensemble from transactions', () => {19 const sales = [sale('a', 1000, 5), sale('b', 1100, 15), sale('c', 950, 30), sale('d', 1050, 60), sale('e', 990, 90), sale('f', 5000, 20, { status: 'excluded' }), sale('g', 1200, 10, { quantity: 3 })];20 const v = computeValuation({ sales, now });21 expect(v.sampleSize).toBe(5);22 expect(v.riv!).toBeGreaterThan(950);23 expect(v.riv!).toBeLessThan(1100);24 expect(v.low!).toBeLessThanOrEqual(v.riv!);25 expect(v.high!).toBeGreaterThanOrEqual(v.riv!);26 expect(v.methods.median5).toBe(1000);27 expect(v.confidence).toBeGreaterThan(0.4);28 expect(v.salesUsed).not.toContain('f');29 });30 it('caps confidence for guide-only estimates', () => {31 const v = computeValuation({ sales: [], observations: [{ priceUsd: 120, date: d(1), priceKind: 'market' }, { priceUsd: 118, date: d(8), priceKind: 'market' }], now });32 expect(v.basis).toBe('guide');33 expect(v.riv).toBe(119);34 expect(v.confidence).toBeLessThanOrEqual(0.45);35 });36 it('extends the window when recent evidence is thin', () => {37 const v = computeValuation({ sales: [sale('a', 500, 400), sale('b', 520, 500), sale('c', 480, 700)], now });38 expect(v.windowDays).toBe(1095);39 expect(v.sampleSize).toBe(3);40 expect(v.confidence).toBeLessThanOrEqual(0.7);41 });42 it('uses grade-adjusted comps when the variant has no sales', () => {43 const v = computeValuation({ sales: [], comps: [{ priceUsd: 100, date: d(3), adjustment: 2.5 }, { priceUsd: 110, date: d(9), adjustment: 2.5 }, { priceUsd: 90, date: d(20), adjustment: 2.5 }], now });44 expect(v.basis).toBe('comps');45 expect(v.riv).toBe(250);46 });47});4849describe('outliers', () => {50 it('flags abnormal prices without deleting', () => {51 const pts = [100, 105, 98, 102, 101, 99, 1500, 3].map((p, i) => ({ id: `s${i}`, priceUsd: p, date: d(i) }));52 const flags = detectOutliers(pts);53 expect(flags.map((f) => f.id).sort()).toEqual(['s6', 's7']);54 expect(flags.find((f) => f.id === 's6')?.reason).toBe('abnormally_high_price');55 });56 it('needs at least five points', () => {57 expect(detectOutliers([{ id: 'a', priceUsd: 1, date: now }, { id: 'b', priceUsd: 1000, date: now }])).toEqual([]);58 });59});6061describe('grade premiums', () => {62 it('measures multipliers empirically from paired assets', () => {63 const sales: Parameters<typeof computeGradePremiums>[0] = [];64 for (let a = 0; a < 10; a++) {65 sales.push({ assetId: `A${a}`, grader: null, grade: null, priceUsd: 100 });66 sales.push({ assetId: `A${a}`, grader: 'psa', grade: '10', priceUsd: 230 + a });67 sales.push({ assetId: `A${a}`, grader: 'psa', grade: '9', priceUsd: 150 });68 }69 const p = computeGradePremiums(sales, { minPairs: 8 });70 const psa10 = p.find((x) => x.grade === '10')!;71 expect(psa10.marketMultiplier).toBeGreaterThan(2.2);72 expect(psa10.sampleSize).toBe(10);73 expect(adjustmentFactor(p, { grader: 'psa', grade: '9' }, { grader: 'psa', grade: '10' })!).toBeCloseTo(psa10.marketMultiplier / 1.5, 2);74 expect(adjustmentFactor(p, { grader: 'bgs', grade: '10' }, { grader: 'psa', grade: '10' })).toBeNull();75 });76});7778describe('scores', () => {79 it('liquidity and rarity stay null without evidence', () => {80 expect(liquidityScore({ salesPerMonth: 0, activeListings: 0, sources: 0, medianDaysBetweenSales: null, askSoldSpread: null })).toBeNull();81 expect(rarityScore({ population: null, productionQuantity: null, listingsPerYear: null, salesPerYear: null, populationGrowthPct: null })).toBeNull();82 });83 it('orders sensibly', () => {84 const liquid = liquidityScore({ salesPerMonth: 40, activeListings: 60, sources: 5, medianDaysBetweenSales: 1, askSoldSpread: 0.05 })!;85 const illiquid = liquidityScore({ salesPerMonth: 0.2, activeListings: 1, sources: 1, medianDaysBetweenSales: 200, askSoldSpread: 0.6 })!;86 expect(liquid).toBeGreaterThan(80);87 expect(illiquid).toBeLessThan(20);88 expect(rarityScore({ population: 5, productionQuantity: null, listingsPerYear: 1, salesPerYear: 1, populationGrowthPct: null })!).toBeGreaterThan(rarityScore({ population: 50000, productionQuantity: null, listingsPerYear: 500, salesPerYear: 800, populationGrowthPct: null })!);89 expect(momentumScore({ priceChange: 0.25, volumeNow: 10, volumePrev: 5 })!).toBeGreaterThan(0);90 expect(valueOpportunity(2950, 3850, 0.91)).toBeCloseTo(-0.2338, 3);91 expect(valueOpportunity(2950, 3850, 0.3)).toBeNull();92 expect(valueOpportunity(2950, 3850, 0.91, 2)).toBeNull(); // thin sample never qualifies an ask93 expect(trendingScore({ priceMomentum: 50, volumeMomentum: 30, searchMomentum: null, listingMomentum: 10, newsMomentum: null })!).toBeGreaterThan(50);94 });95});9697describe('ask vs RIV gate (§83–§84, §196)', () => {98 it('signs a discount negative and classifies deal / fair / premium', () => {99 expect(assessAsk({ askUsd: 2650, rivUsd: 3180, confidence: 0.8, sampleSize: 42, basis: 'transactions' })).toMatchObject({ verdict: 'deal', discount: -0.1667 });100 expect(assessAsk({ askUsd: 3200, rivUsd: 3180, confidence: 0.8, sampleSize: 42 }).verdict).toBe('fair');101 expect(assessAsk({ askUsd: 4000, rivUsd: 3180, confidence: 0.8, sampleSize: 42 }).verdict).toBe('premium');102 });103 it('never turns the 1986 Fleer case into a deal: a $472 slab against a $5.48 raw RIV is an anomaly', () => {104 const a = assessAsk({ askUsd: 472.2, rivUsd: 5.48, confidence: 0.78, sampleSize: 18, basis: 'transactions' });105 expect(a.verdict).toBe('anomaly');106 expect(a.discount).toBeNull();107 expect(a.ratio).toBeCloseTo(86.17, 1);108 expect(assessAsk({ askUsd: 0.4, rivUsd: 5.48, confidence: 0.78, sampleSize: 18 }).verdict).toBe('anomaly'); // implausibly low109 });110 it('refuses comps/guide valuations, thin samples, low confidence and variant mismatches', () => {111 expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 20, basis: 'comps' })).toMatchObject({ verdict: 'ungated', reasons: ['riv_basis_comps'] });112 expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 3 }).reasons).toContain('riv_sample');113 expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.45, sampleSize: 30 }).reasons).toContain('riv_confidence');114 expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 30, sameVariant: false }).reasons).toContain('variant_mismatch');115 expect(assessAsk({ askUsd: 100, rivUsd: 120, confidence: 0.9, sampleSize: 30, matchConfidence: 0.5 }).reasons).toContain('match_confidence');116 });117 it('adds fees before comparing', () => {118 expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30, feesUsd: 150 }).verdict).toBe('fair');119 expect(assessAsk({ askUsd: 900, rivUsd: 1000, confidence: 0.9, sampleSize: 30 }).verdict).toBe('deal');120 });121 it('sends asks more than 50 % below RIV to review instead of calling them deals (§174)', () => {122 const a = assessAsk({ askUsd: 115.8, rivUsd: 1077.54, confidence: 0.8, sampleSize: 58 }); // a booster pack ask matched to a booster-box RIV123 expect(a.verdict).toBe('review');124 expect(a.discount).toBeCloseTo(-0.8925, 3);125 expect(valueOpportunity(115.8, 1077.54, 0.8, 58)).toBeNull();126 expect(dealScore({ discount: -0.89, confidence: 0.8, sampleSize: 58, liquidity: 60 })).toBe(0);127 expect(assessAsk({ askUsd: 520, rivUsd: 1000, confidence: 0.8, sampleSize: 58 }).verdict).toBe('deal');128 });129 it('deal score is 0 without a gated discount and rewards confidence, depth and liquidity', () => {130 expect(dealScore({ discount: null, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);131 expect(dealScore({ discount: 0.2, confidence: 0.9, sampleSize: 30, liquidity: 70 })).toBe(0);132 const strong = dealScore({ discount: -0.3, confidence: 0.9, sampleSize: 40, liquidity: 80 });133 const weak = dealScore({ discount: -0.3, confidence: 0.55, sampleSize: 6, liquidity: 10 });134 expect(strong).toBeGreaterThan(weak);135 expect(strong).toBeLessThanOrEqual(100);136 expect(isPlausibleChange(85.17)).toBe(false);137 expect(isPlausibleChange(0.42)).toBe(true);138 });139});140