TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { 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';2import type { AssetDetail, VariantRow } from '@/lib/queries/assets';3import { confidenceLabel, fmtMoney, fmtNum, fmtPct, fmtRelative } from '@/lib/format';4import { ScoreExplainer } from '@/components/ui/score-explainer';56/*7 * Asset-specific explainers (§204). Every component mirrors packages/valuation/src/{scores,valuation}.ts;8 * weights and scales are the ones in code. Inputs the platform does not persist per asset (dispersion of9 * log prices, median days between sales, source trust) are shown as "not available" rather than guessed.10 */1112const DAY = 86_400_000;13const pct = (v: number) => `${Math.round(v * 100)} %`;1415export function LiquidityExplainer({ asset, variant, className }: { asset: AssetDetail; variant: VariantRow | null; className?: string }) {16 const score = variant ? variant.liquidityScore : asset.liquidityScore;17 const salesPerMonth = variant ? null : asset.sales1y / 12;18 const activeListings = variant ? variant.activeListings : asset.activeListings;19 const minAsk = variant ? variant.minAskUsd : asset.minAskUsd;20 const riv = variant ? variant.rivUsd : asset.rivUsd;21 const spread = minAsk !== null && riv !== null && riv > 0 ? (minAsk - riv) / riv : null;22 return (23 <ScoreExplainer24 className={className}25 title={`Liquidity Score ${score === null ? '—' : `${Math.round(score)}/100`}`}26 anchor="liquidity"27 formula="100 × (0.40·sales pace + 0.15·listing depth + 0.10·source breadth + 0.20·sale spacing + 0.15·ask–RIV spread); each component clamped to 0–1"28 rows={[29 { label: 'Sales per month (12-month average)', value: salesPerMonth === null ? null : fmtNum(salesPerMonth, { digits: 1 }), weight: '40 %', note: 'log-scaled, 30 / month → 1' },30 { label: 'Active listings', value: fmtNum(activeListings), weight: '15 %', note: 'log-scaled, 50 listings → 1' },31 { label: 'Distinct sources with evidence', value: variant ? null : fmtNum(asset.sourcesCount), weight: '10 %', note: '(sources − 1) / 4' },32 { label: 'Median days between sales', value: null, weight: '20 %', note: '1 − days / 90 (0 when unknown)' },33 { label: 'Lowest ask vs RIV', value: spread === null ? null : fmtPct(spread, 1), weight: '15 %', note: '1 − |spread| / 50 % (0.5 when no ask)' },34 ]}35 footnote={variant ? 'Variant-level liquidity uses the sales of this variant over the last three years; days between sales and sources are computed but not stored per variant.' : score === null ? 'No sales and no listings observed: the score is not computed rather than set to 0.' : undefined}36 />37 );38}3940export function RarityExplainer({ asset, population, className }: { asset: AssetDetail; population?: number | null; className?: string }) {41 return (42 <ScoreExplainer43 className={className}44 title={`Rarity Score ${asset.rarityScore === null ? '—' : `${Math.round(asset.rarityScore)}/100`}`}45 anchor="rarity"46 formula="weighted mean of the available supply signals (weights renormalised over the signals present); ×0.9 when the graded population grew > 20 % since the previous report"47 rows={[48 { label: 'Graded population (latest report)', value: population === null || population === undefined ? null : fmtNum(population), weight: '40 %', note: '1 − log10(1 + pop) / 5 · 100 k → 0' },49 { label: 'Documented production quantity', value: asset.productionQuantity === null ? null : fmtNum(asset.productionQuantity), weight: '30 %', note: '1 − log10(qty) / 7' },50 { label: 'Sales in the last 12 months', value: asset.sales1y || asset.salesCount ? fmtNum(asset.sales1y) : null, weight: '20 %', note: '1 − log10(1 + sales) / 3 · 1 000 / yr → 0' },51 { label: 'Listings per year (active × 4)', value: asset.activeListings > 0 || asset.sales1y > 0 ? fmtNum(asset.activeListings * 4) : null, weight: '10 %', note: '1 − log10(1 + listings) / 3' },52 ]}53 footnote="Rarity is null when no supply signal exists at all — it is never guessed. Population figures come only from published grading-company reports."54 />55 );56}5758export function ConfidenceExplainer({ asset, variant, windowDays }: { asset: AssetDetail; variant: VariantRow | null; windowDays?: number | null }) {59 const conf = variant ? variant.rivConfidence : asset.rivConfidence;60 const n = variant ? variant.rivSampleSize : asset.rivSampleSize;61 const latestAt = variant ? variant.latestSaleAt : asset.latestSaleAt;62 // recency is measured at the valuation's own computation time (asset_stats.updated_at), keeping the render pure63 const asOf = asset.updatedAt ? new Date(asset.updatedAt).getTime() : null;64 const ageDays = latestAt && asOf ? Math.max(0, (asOf - new Date(latestAt).getTime()) / DAY) : null;65 const sizeScore = n > 0 ? Math.min(1, Math.log2(n + 1) / Math.log2(41)) : 0;66 return (67 <ScoreExplainer68 title={`RIV confidence ${conf === null ? '—' : `${(conf * 100).toFixed(0)} % · ${confidenceLabel(conf)}`}`}69 anchor="confidence"70 formula="0.35·sample size + 0.30·(1 − dispersion) + 0.20·recency + 0.15·source trust; labels High ≥ 75 %, Medium ≥ 50 %, Low > 20 %"71 rows={[72 { label: 'Transactions used', value: `${fmtNum(n)} → ${pct(sizeScore)}`, weight: '35 %', note: 'log2(n + 1) / log2(41) · 40 sales → 1' },73 { label: 'Dispersion of log prices (robust MAD)', value: null, weight: '30 %', note: '1 − σ / 0.6 · 60 % dispersion → 0' },74 { label: 'Age of the latest sale', value: ageDays === null ? null : `${Math.round(ageDays)} d → ${pct(Math.max(0, 1 - ageDays / 365))}`, weight: '20 %', note: '1 − days / 365' },75 { label: 'Source trust × identification confidence', value: null, weight: '15 %', note: 'mean over the sales used' },76 ]}77 footnote={78 windowDays && windowDays > 36579 ? `Fewer than five sales in the last year: the window was extended to ${windowDays} days and confidence is capped at 70 %.`80 : n > 0 && n < 381 ? 'Fewer than three transactions: RIV rests on 1–2 sales (confidence ≤ 30 %) or on grade-adjusted comparables / guide prices (≤ 50 %).'82 : 'Comps-only and guide-only valuations are capped at 50 % and 45 % respectively.'83 }84 />85 );86}8788export function AskVsRivExplainer({ asset, variant }: { asset: AssetDetail; variant: VariantRow | null }) {89 const riv = variant ? variant.rivUsd : asset.rivUsd;90 const conf = variant ? variant.rivConfidence : asset.rivConfidence;91 const n = variant ? variant.rivSampleSize : asset.rivSampleSize;92 const minAsk = variant ? variant.minAskUsd : asset.minAskUsd;93 const best = variant ? null : asset.valueOpportunity;94 const gated = riv !== null && riv > 0 && (conf ?? 0) >= ASK_MIN_CONFIDENCE && n >= ASK_MIN_SAMPLE;95 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';96 return (97 <ScoreExplainer98 summary="Why is (or isn't) an ask compared with RIV?"99 title={`Ask vs RIV · ${verdict}`}100 anchor="ask-vs-riv"101 formula="(ask − RIV) / RIV, per variant, only when every gate below passes; asks outside the plausibility band are labelled Data/identity anomaly"102 rows={[103 { label: 'RIV rests on transactions (not comps / guide)', value: riv === null ? null : 'see valuation breakdown', note: 'required' },104 { label: `RIV confidence ≥ ${pct(ASK_MIN_CONFIDENCE)}`, value: conf === null ? null : `${(conf * 100).toFixed(0)} % ${conf >= ASK_MIN_CONFIDENCE ? '✓' : '✗'}`, note: 'required' },105 { label: `Transactions used ≥ ${ASK_MIN_SAMPLE}`, value: `${fmtNum(n)} ${n >= ASK_MIN_SAMPLE ? '✓' : '✗'}`, note: 'required' },106 { label: `Listing identification confidence ≥ ${pct(ASK_MIN_MATCH_CONFIDENCE)}`, value: 'per listing', note: 'required; slabs with an unreadable grade never compare against raw' },107 { 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' },108 { 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)}` },109 ]}110 footnote={`Analytical data, not advice. ${asset.latestSaleAt ? `Latest sale ${fmtRelative(asset.latestSaleAt)}.` : ''}`}111 />112 );113}114