import { median } from '@rareindex/shared'; export interface GradedSale { assetId: string; grader: string | null; grade: string | null; priceUsd: number; } export interface GradePremium { grader: string; grade: string; marketMultiplier: number; sampleSize: number; } /** * Empirical grade premiums (§118): for each (grader, grade), the median ratio of a graded sale to * the median raw/ungraded (or reference-grade) price of the SAME asset. Computed per category from * paired evidence only — never assumed across graders. */ export function computeGradePremiums(sales: GradedSale[], opts: { referenceKey?: string; minPairs?: number } = {}): GradePremium[] { const minPairs = opts.minPairs ?? 8; const byAsset = new Map(); for (const s of sales) if (s.priceUsd > 0) byAsset.set(s.assetId, [...(byAsset.get(s.assetId) ?? []), s]); const ratios = new Map(); for (const list of byAsset.values()) { const key = (s: GradedSale) => (s.grader && s.grade ? `${s.grader}|${s.grade}` : 'raw'); const groups = new Map(); for (const s of list) groups.set(key(s), [...(groups.get(key(s)) ?? []), s.priceUsd]); const refKey = opts.referenceKey ?? 'raw'; let ref = median(groups.get(refKey) ?? []); if (ref === null) { // fall back to the most common graded key for this asset as reference const best = [...groups.entries()].filter(([k]) => k !== refKey).sort((a, b) => b[1].length - a[1].length)[0]; if (!best || best[1].length < 2) continue; ref = median(best[1]); // express others relative to that grade, then to raw is impossible → skip unless raw exists continue; } for (const [k, prices] of groups) { if (k === refKey) continue; const m = median(prices)!; ratios.set(k, [...(ratios.get(k) ?? []), m / ref]); } } const out: GradePremium[] = []; for (const [k, rs] of ratios) { if (rs.length < minPairs) continue; const [grader, grade] = k.split('|') as [string, string]; out.push({ grader, grade, marketMultiplier: Math.round(median(rs)! * 1000) / 1000, sampleSize: rs.length }); } return out.sort((a, b) => a.grader.localeCompare(b.grader) || Number(a.grade) - Number(b.grade)); } /** Adjustment factor target/source from premium tables (both relative to raw). null when either is unknown. */ export function adjustmentFactor(premiums: GradePremium[], source: { grader: string | null; grade: string | null }, target: { grader: string | null; grade: string | null }): number | null { const mult = (g: { grader: string | null; grade: string | null }): number | null => { if (!g.grader || !g.grade) return 1; // raw const p = premiums.find((x) => x.grader === g.grader && x.grade === g.grade); return p ? p.marketMultiplier : null; }; const s = mult(source); const t = mult(target); if (s === null || t === null || s <= 0) return null; return t / s; }