TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/**2 * Repeat-sales index (Bailey–Muth–Nourse / Case–Shiller flavour, §190). Each pair of consecutive3 * sales of the SAME variant gives one observation: log(p2/p1) = Σ_t D_t·β_t where D_t = +1 for the4 * period of the second sale, −1 for the first. Solved by ordinary least squares via normal5 * equations (small dense system; periods are months). Level_t = exp(β_t), first period = 1.6 */7export interface RepeatSale {8 variantId: string;9 date: string; // YYYY-MM-DD10 priceUsd: number;11}1213export interface RepeatSalesResult {14 periods: string[]; // YYYY-MM15 levels: number[]; // relative to first period = 116 pairs: number;17 /** number of pairs contributing to each period */18 support: number[];19}2021export function monthKey(date: string): string {22 return date.slice(0, 7);23}2425function solveNormal(A: number[][], b: number[]): number[] | null {26 const n = b.length;27 const M = A.map((row, i) => [...row, b[i]!]);28 for (let col = 0; col < n; col++) {29 let piv = col;30 for (let r = col + 1; r < n; r++) if (Math.abs(M[r]![col]!) > Math.abs(M[piv]![col]!)) piv = r;31 if (Math.abs(M[piv]![col]!) < 1e-12) return null;32 [M[col], M[piv]] = [M[piv]!, M[col]!];33 for (let r = 0; r < n; r++) {34 if (r === col) continue;35 const f = M[r]![col]! / M[col]![col]!;36 if (f === 0) continue;37 for (let c = col; c <= n; c++) M[r]![c]! -= f * M[col]![c]!;38 }39 }40 return M.map((row, i) => row[n]! / row[i]!);41}4243export function repeatSalesIndex(sales: RepeatSale[], opts: { minPairs?: number; maxAbsLogReturnPerMonth?: number } = {}): RepeatSalesResult | null {44 const minPairs = opts.minPairs ?? 10;45 const byVariant = new Map<string, RepeatSale[]>();46 for (const s of sales) if (s.priceUsd > 0) byVariant.set(s.variantId, [...(byVariant.get(s.variantId) ?? []), s]);47 const pairs: Array<{ t1: string; t2: string; y: number }> = [];48 for (const list of byVariant.values()) {49 list.sort((a, b) => a.date.localeCompare(b.date));50 for (let i = 1; i < list.length; i++) {51 const a = list[i - 1]!;52 const b = list[i]!;53 const t1 = monthKey(a.date);54 const t2 = monthKey(b.date);55 if (t1 === t2) continue;56 const y = Math.log(b.priceUsd / a.priceUsd);57 const months = monthsBetween(t1, t2);58 const cap = (opts.maxAbsLogReturnPerMonth ?? 0.5) * Math.max(1, months);59 if (Math.abs(y) > cap) continue; // glitch guard60 pairs.push({ t1, t2, y });61 }62 }63 if (pairs.length < minPairs) return null;64 const periods = [...new Set(pairs.flatMap((p) => [p.t1, p.t2]))].sort();65 const idx = new Map(periods.map((p, i) => [p, i]));66 const k = periods.length - 1; // first period fixed at 067 if (k < 1) return null;68 const A: number[][] = Array.from({ length: k }, () => Array(k).fill(0));69 const b: number[] = Array(k).fill(0);70 const support = Array(periods.length).fill(0) as number[];71 for (const p of pairs) {72 const i1 = idx.get(p.t1)! - 1;73 const i2 = idx.get(p.t2)! - 1;74 support[i1 + 1]!++;75 support[i2 + 1]!++;76 // x vector: +1 at i2, -1 at i1 (skip index -1 = base period)77 const entries: Array<[number, number]> = [];78 if (i2 >= 0) entries.push([i2, 1]);79 if (i1 >= 0) entries.push([i1, -1]);80 for (const [r, xr] of entries) {81 b[r]! += xr * p.y;82 for (const [c, xc] of entries) A[r]![c]! += xr * xc;83 }84 }85 // ridge for stability on sparse months86 for (let i = 0; i < k; i++) A[i]![i]! += 1e-6;87 const beta = solveNormal(A, b);88 if (!beta) return null;89 return { periods, levels: [1, ...beta.map((x) => Math.exp(x))], pairs: pairs.length, support };90}9192export function monthsBetween(a: string, b: string): number {93 const [ya, ma] = a.split('-').map(Number) as [number, number];94 const [yb, mb] = b.split('-').map(Number) as [number, number];95 return (yb - ya) * 12 + (mb - ma);96}97