/** * Auction intelligence (§33–§35): a lot's current bid / estimate is compared with the RareIndex * Valuation only on a buyer-pays (all-in) basis — hammer + the house's buyer premium — and only * through the same gates as a marketplace ask (same variant, transaction-based RIV, ≥ 5 sales, * confidence ≥ 0.5, plausibility band, review threshold). Taxes, duties and shipping are out of * scope and are stated as such wherever an all-in figure is shown. */ import { assessAsk, type AskVerdict } from './scores.js'; import { allInPrice, type FeeBasis } from './fees.js'; import { round } from '@rareindex/shared'; export interface LotAssessmentInput { /** auction house name / connector id for the fee schedule */ house: string | null | undefined; /** lot amounts already converted to USD (null when absent) */ currentBidUsd: number | null; bidCount: number | null; estimateLowUsd: number | null; estimateHighUsd: number | null; hammerPriceUsd?: number | null; /** connector flag for the lot's quoted amounts; lots are hammer-basis unless the connector says otherwise */ buyerPremiumIncluded?: boolean | null; /** rate converting USD into the schedule currency (for tier boundaries); 1 when unknown */ fxUsdToScheduleCurrency?: number; /** valuation of the lot's own variant (or the asset when raw and representative) */ riv: { rivUsd: number | null; confidence: number | null; sampleSize: number | null; basis?: 'transactions' | 'comps' | 'guide' | 'none' | null } | null; /** false when the lot's grade does not match the valuation's variant */ sameVariant?: boolean; } export interface LotAssessment { buyerPremiumRate: number | null; feeBasis: FeeBasis; allInBidUsd: number | null; allInEstimateLowUsd: number | null; allInEstimateHighUsd: number | null; allInHammerUsd: number | null; rivUsd: number | null; /** (all-in bid − RIV) / RIV — null without a bid or when ungated/anomalous/review */ bidVsRiv: number | null; /** (all-in low estimate − RIV) / RIV — null when ungated/anomalous/review */ estimateVsRiv: number | null; /** verdict of the most informative comparison: the bid when there is one, else the low estimate */ verdict: AskVerdict; reasons: string[]; /** what the verdict is based on */ basedOn: 'bid' | 'estimate' | 'none'; } const allIn = (usd: number | null, i: LotAssessmentInput) => { if (usd === null || !(usd > 0)) return null; return allInPrice({ price: usd, buyerPremiumIncluded: i.buyerPremiumIncluded ?? false, house: i.house, saleType: 'auction', sourceType: 'auction_house', fxToScheduleCurrency: i.fxUsdToScheduleCurrency }); }; export function assessLot(i: LotAssessmentInput): LotAssessment { const hasBid = (i.bidCount ?? 0) > 0 && i.currentBidUsd !== null && i.currentBidUsd > 0; const bid = hasBid ? allIn(i.currentBidUsd, i) : null; const lo = allIn(i.estimateLowUsd, i); const hi = allIn(i.estimateHighUsd, i); const hammer = allIn(i.hammerPriceUsd ?? null, i); const ref = bid ?? lo ?? hi ?? hammer; const riv = i.riv?.rivUsd ?? null; const gate = (askUsd: number | null) => assessAsk({ askUsd, rivUsd: riv, confidence: i.riv?.confidence ?? null, sampleSize: i.riv?.sampleSize ?? null, basis: i.riv?.basis ?? undefined, sameVariant: i.sameVariant }); const bidA = bid ? gate(bid.allIn) : null; const loA = lo ? gate(lo.allIn) : null; const primary = bidA ?? loA; const usable = (a: ReturnType | null) => (a && (a.verdict === 'deal' || a.verdict === 'fair' || a.verdict === 'premium') ? a.discount : null); return { buyerPremiumRate: ref ? round(ref.rate, 4) : null, feeBasis: ref?.basis ?? 'unknown', allInBidUsd: bid?.allIn ?? null, allInEstimateLowUsd: lo?.allIn ?? null, allInEstimateHighUsd: hi?.allIn ?? null, allInHammerUsd: hammer?.allIn ?? null, rivUsd: riv, bidVsRiv: usable(bidA), estimateVsRiv: usable(loA), verdict: primary?.verdict ?? 'ungated', reasons: primary?.reasons ?? (riv === null ? ['no_riv'] : ['no_bid_or_estimate']), basedOn: bidA ? 'bid' : loA ? 'estimate' : 'none', }; }