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%
2.9 KB · 68 lines typescript
Raw Blame History
1import { median } from '@rareindex/shared';23export interface GradedSale {4  assetId: string;5  grader: string | null;6  grade: string | null;7  priceUsd: number;8}910export interface GradePremium {11  grader: string;12  grade: string;13  marketMultiplier: number;14  sampleSize: number;15}1617/**18 * Empirical grade premiums (§118): for each (grader, grade), the median ratio of a graded sale to19 * the median raw/ungraded (or reference-grade) price of the SAME asset. Computed per category from20 * paired evidence only — never assumed across graders.21 */22export function computeGradePremiums(sales: GradedSale[], opts: { referenceKey?: string; minPairs?: number } = {}): GradePremium[] {23  const minPairs = opts.minPairs ?? 8;24  const byAsset = new Map<string, GradedSale[]>();25  for (const s of sales) if (s.priceUsd > 0) byAsset.set(s.assetId, [...(byAsset.get(s.assetId) ?? []), s]);26  const ratios = new Map<string, number[]>();27  for (const list of byAsset.values()) {28    const key = (s: GradedSale) => (s.grader && s.grade ? `${s.grader}|${s.grade}` : 'raw');29    const groups = new Map<string, number[]>();30    for (const s of list) groups.set(key(s), [...(groups.get(key(s)) ?? []), s.priceUsd]);31    const refKey = opts.referenceKey ?? 'raw';32    let ref = median(groups.get(refKey) ?? []);33    if (ref === null) {34      // fall back to the most common graded key for this asset as reference35      const best = [...groups.entries()].filter(([k]) => k !== refKey).sort((a, b) => b[1].length - a[1].length)[0];36      if (!best || best[1].length < 2) continue;37      ref = median(best[1]);38      // express others relative to that grade, then to raw is impossible → skip unless raw exists39      continue;40    }41    for (const [k, prices] of groups) {42      if (k === refKey) continue;43      const m = median(prices)!;44      ratios.set(k, [...(ratios.get(k) ?? []), m / ref]);45    }46  }47  const out: GradePremium[] = [];48  for (const [k, rs] of ratios) {49    if (rs.length < minPairs) continue;50    const [grader, grade] = k.split('|') as [string, string];51    out.push({ grader, grade, marketMultiplier: Math.round(median(rs)! * 1000) / 1000, sampleSize: rs.length });52  }53  return out.sort((a, b) => a.grader.localeCompare(b.grader) || Number(a.grade) - Number(b.grade));54}5556/** Adjustment factor target/source from premium tables (both relative to raw). null when either is unknown. */57export function adjustmentFactor(premiums: GradePremium[], source: { grader: string | null; grade: string | null }, target: { grader: string | null; grade: string | null }): number | null {58  const mult = (g: { grader: string | null; grade: string | null }): number | null => {59    if (!g.grader || !g.grade) return 1; // raw60    const p = premiums.find((x) => x.grader === g.grader && x.grade === g.grade);61    return p ? p.marketMultiplier : null;62  };63  const s = mult(source);64  const t = mult(target);65  if (s === null || t === null || s <= 0) return null;66  return t / s;67}68