import { mad, median } from '@rareindex/shared'; export interface PricePoint { id: string; priceUsd: number; date: Date; quantity?: number; isBundle?: boolean; } export interface OutlierFlag { id: string; reason: string; score: number; } /** * Robust outlier detection on log prices within a variant (§116): modified z-score * 0.6745·(x − median)/MAD > threshold. Needs ≥ 5 points; with a MAD of 0 (identical prices) any * different price beyond 3× is flagged. Never deletes — returns flags for audit. */ export function detectOutliers(points: PricePoint[], threshold = 3.5): OutlierFlag[] { const valid = points.filter((p) => p.priceUsd > 0 && !p.isBundle && (p.quantity ?? 1) === 1); if (valid.length < 5) return []; const logs = valid.map((p) => Math.log(p.priceUsd)); const med = median(logs)!; const m = mad(logs)!; const out: OutlierFlag[] = []; for (let i = 0; i < valid.length; i++) { const p = valid[i]!; const dev = logs[i]! - med; let score: number; if (m > 0) score = (0.6745 * dev) / m; else score = Math.abs(dev) > Math.log(3) ? Math.sign(dev) * threshold * 2 : 0; if (Math.abs(score) > threshold) out.push({ id: p.id, reason: score > 0 ? 'abnormally_high_price' : 'abnormally_low_price', score: Math.round(score * 100) / 100 }); } return out; }