import { describe, expect, it } from 'vitest'; import { chainLinkedIndex, horizonReturns } from './chain.js'; import { repeatSalesIndex } from './repeat-sales.js'; import { correlationMatrix } from './correlation.js'; describe('chain-linked index', () => { it('chains equal-weight log returns and survives composition changes', () => { const series = [ { id: 'a', points: [{ date: '2024-01-01', value: 100 }, { date: '2024-01-02', value: 110 }, { date: '2024-01-03', value: 121 }] }, { id: 'b', points: [{ date: '2024-01-01', value: 50 }, { date: '2024-01-02', value: 50 }, { date: '2024-01-03', value: 55 }] }, { id: 'c', points: [{ date: '2024-01-03', value: 10 }, { date: '2024-01-04', value: 20 }] }, // joins later, doubles ]; const idx = chainLinkedIndex(series, { baseDate: '2024-01-01', baseValue: 1000, minConstituents: 2 }); expect(idx[0]).toMatchObject({ date: '2024-01-01', value: 1000 }); // day 2: a +10% (log .0953), b 0 → mean .0477 → ×1.0488 expect(idx[1]!.value).toBeCloseTo(1000 * Math.exp((Math.log(1.1) + 0) / 2), 2); // day 4: only c has a value → below minConstituents → not published expect(idx.at(-1)!.date).toBe('2024-01-03'); expect(idx.at(-1)!.constituents).toBe(3); }); it('publishes nothing below the constituent threshold', () => { const idx = chainLinkedIndex([{ id: 'a', points: [{ date: '2024-01-01', value: 1 }, { date: '2024-01-02', value: 2 }] }], { baseDate: '2024-01-01', baseValue: 1000, minConstituents: 5 }); expect(idx).toEqual([]); }); it('computes horizon returns', () => { const pts = Array.from({ length: 400 }, (_, i) => ({ date: new Date(Date.UTC(2025, 0, 1) + i * 86_400_000).toISOString().slice(0, 10), value: 1000 * (1 + i / 400) })); const h = horizonReturns(pts); expect(h['1d']!).toBeGreaterThan(0); expect(h['1y']!).toBeGreaterThan(h['30d']!); expect(h['10y']).toBeNull(); expect(h.all!).toBeCloseTo(399 / 400, 5); }); }); describe('repeat-sales', () => { it('recovers a known monthly path', () => { // true levels: Jan 1.0, Feb 1.1, Mar 1.21, Apr 1.1 const levels: Record = { '2024-01': 1, '2024-02': 1.1, '2024-03': 1.21, '2024-04': 1.1 }; const sales: Parameters[0] = []; const months = Object.keys(levels); for (let v = 0; v < 15; v++) { const base = 100 + v * 10; const m1 = months[v % 3]!; const m2 = months[(v % 3) + 1]!; sales.push({ variantId: `v${v}`, date: `${m1}-10`, priceUsd: base * levels[m1]! }); sales.push({ variantId: `v${v}`, date: `${m2}-12`, priceUsd: base * levels[m2]! }); } const r = repeatSalesIndex(sales, { minPairs: 5 })!; expect(r.periods).toEqual(months); expect(r.levels[1]!).toBeCloseTo(1.1, 3); expect(r.levels[2]!).toBeCloseTo(1.21, 3); expect(r.levels[3]!).toBeCloseTo(1.1, 3); }); it('returns null with too few pairs', () => { expect(repeatSalesIndex([{ variantId: 'a', date: '2024-01-01', priceUsd: 1 }, { variantId: 'a', date: '2024-02-01', priceUsd: 2 }])).toBeNull(); }); }); describe('correlation', () => { it('finds perfect correlation of identical series', () => { const s = Array.from({ length: 30 }, (_, i) => ({ date: `2024-01-${String(i + 1).padStart(2, '0')}`, value: 100 + Math.sin(i) * 10 })); const m = correlationMatrix({ A: s, B: s.map((p) => ({ ...p, value: p.value * 2 })) }); expect(m.find((x) => x.a === 'A' && x.b === 'B')!.r).toBe(1); }); });