import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_THRESHOLD, PREMIUM_THRESHOLD } from '@rareindex/valuation'; import type { AssetDetail, VariantRow } from '@/lib/queries/assets'; import { confidenceLabel, fmtMoney, fmtNum, fmtPct, fmtRelative } from '@/lib/format'; import { ScoreExplainer } from '@/components/ui/score-explainer'; /* * Asset-specific explainers (§204). Every component mirrors packages/valuation/src/{scores,valuation}.ts; * weights and scales are the ones in code. Inputs the platform does not persist per asset (dispersion of * log prices, median days between sales, source trust) are shown as "not available" rather than guessed. */ const DAY = 86_400_000; const pct = (v: number) => `${Math.round(v * 100)} %`; export function LiquidityExplainer({ asset, variant, className }: { asset: AssetDetail; variant: VariantRow | null; className?: string }) { const score = variant ? variant.liquidityScore : asset.liquidityScore; const salesPerMonth = variant ? null : asset.sales1y / 12; const activeListings = variant ? variant.activeListings : asset.activeListings; const minAsk = variant ? variant.minAskUsd : asset.minAskUsd; const riv = variant ? variant.rivUsd : asset.rivUsd; const spread = minAsk !== null && riv !== null && riv > 0 ? (minAsk - riv) / riv : null; return ( ); } export function RarityExplainer({ asset, population, className }: { asset: AssetDetail; population?: number | null; className?: string }) { return ( 0 || asset.sales1y > 0 ? fmtNum(asset.activeListings * 4) : null, weight: '10 %', note: '1 − log10(1 + listings) / 3' }, ]} footnote="Rarity is null when no supply signal exists at all — it is never guessed. Population figures come only from published grading-company reports." /> ); } export function ConfidenceExplainer({ asset, variant, windowDays }: { asset: AssetDetail; variant: VariantRow | null; windowDays?: number | null }) { const conf = variant ? variant.rivConfidence : asset.rivConfidence; const n = variant ? variant.rivSampleSize : asset.rivSampleSize; const latestAt = variant ? variant.latestSaleAt : asset.latestSaleAt; // recency is measured at the valuation's own computation time (asset_stats.updated_at), keeping the render pure const asOf = asset.updatedAt ? new Date(asset.updatedAt).getTime() : null; const ageDays = latestAt && asOf ? Math.max(0, (asOf - new Date(latestAt).getTime()) / DAY) : null; const sizeScore = n > 0 ? Math.min(1, Math.log2(n + 1) / Math.log2(41)) : 0; return ( 365 ? `Fewer than five sales in the last year: the window was extended to ${windowDays} days and confidence is capped at 70 %.` : n > 0 && n < 3 ? 'Fewer than three transactions: RIV rests on 1–2 sales (confidence ≤ 30 %) or on grade-adjusted comparables / guide prices (≤ 50 %).' : 'Comps-only and guide-only valuations are capped at 50 % and 45 % respectively.' } /> ); } export function AskVsRivExplainer({ asset, variant }: { asset: AssetDetail; variant: VariantRow | null }) { const riv = variant ? variant.rivUsd : asset.rivUsd; const conf = variant ? variant.rivConfidence : asset.rivConfidence; const n = variant ? variant.rivSampleSize : asset.rivSampleSize; const minAsk = variant ? variant.minAskUsd : asset.minAskUsd; const best = variant ? null : asset.valueOpportunity; const gated = riv !== null && riv > 0 && (conf ?? 0) >= ASK_MIN_CONFIDENCE && n >= ASK_MIN_SAMPLE; const verdict = best === null ? (gated ? (minAsk === null ? 'no active ask' : 'no ask passed the gate') : 'valuation does not qualify') : best <= DEAL_THRESHOLD ? 'below RIV' : best >= PREMIUM_THRESHOLD ? 'above RIV' : 'near RIV'; return ( = ASK_MIN_CONFIDENCE ? '✓' : '✗'}`, note: 'required' }, { label: `Transactions used ≥ ${ASK_MIN_SAMPLE}`, value: `${fmtNum(n)} ${n >= ASK_MIN_SAMPLE ? '✓' : '✗'}`, note: 'required' }, { label: `Listing identification confidence ≥ ${pct(ASK_MIN_MATCH_CONFIDENCE)}`, value: 'per listing', note: 'required; slabs with an unreadable grade never compare against raw' }, { label: `Plausibility band ${ASK_ANOMALY_LOW_RATIO}× – ${ASK_ANOMALY_HIGH_RATIO}× RIV`, value: minAsk !== null && riv ? `lowest ask ${fmtMoney(minAsk)} = ${fmtNum(minAsk / riv, { digits: 2 })}× RIV` : null, note: 'outside → anomaly, not a deal' }, { label: 'Best gated ask vs RIV', value: best === null ? null : fmtPct(best, 1), note: `deal ≤ ${fmtPct(DEAL_THRESHOLD, 0)} · premium ≥ ${fmtPct(PREMIUM_THRESHOLD, 0, false)}` }, ]} footnote={`Analytical data, not advice. ${asset.latestSaleAt ? `Latest sale ${fmtRelative(asset.latestSaleAt)}.` : ''}`} /> ); }