SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.8 KB · 97 lines typescript
Raw Blame History
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