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)}.` : ''}`}
/>
);
}