SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.0 KB · 84 lines typescript
Raw Blame History
1/**2 * Auction intelligence (§33–§35): a lot's current bid / estimate is compared with the RareIndex3 * Valuation only on a buyer-pays (all-in) basis — hammer + the house's buyer premium — and only4 * through the same gates as a marketplace ask (same variant, transaction-based RIV, ≥ 5 sales,5 * confidence ≥ 0.5, plausibility band, review threshold). Taxes, duties and shipping are out of6 * scope and are stated as such wherever an all-in figure is shown.7 */8import { assessAsk, type AskVerdict } from './scores.js';9import { allInPrice, type FeeBasis } from './fees.js';10import { round } from '@rareindex/shared';1112export interface LotAssessmentInput {13  /** auction house name / connector id for the fee schedule */14  house: string | null | undefined;15  /** lot amounts already converted to USD (null when absent) */16  currentBidUsd: number | null;17  bidCount: number | null;18  estimateLowUsd: number | null;19  estimateHighUsd: number | null;20  hammerPriceUsd?: number | null;21  /** connector flag for the lot's quoted amounts; lots are hammer-basis unless the connector says otherwise */22  buyerPremiumIncluded?: boolean | null;23  /** rate converting USD into the schedule currency (for tier boundaries); 1 when unknown */24  fxUsdToScheduleCurrency?: number;25  /** valuation of the lot's own variant (or the asset when raw and representative) */26  riv: { rivUsd: number | null; confidence: number | null; sampleSize: number | null; basis?: 'transactions' | 'comps' | 'guide' | 'none' | null } | null;27  /** false when the lot's grade does not match the valuation's variant */28  sameVariant?: boolean;29}3031export interface LotAssessment {32  buyerPremiumRate: number | null;33  feeBasis: FeeBasis;34  allInBidUsd: number | null;35  allInEstimateLowUsd: number | null;36  allInEstimateHighUsd: number | null;37  allInHammerUsd: number | null;38  rivUsd: number | null;39  /** (all-in bid − RIV) / RIV — null without a bid or when ungated/anomalous/review */40  bidVsRiv: number | null;41  /** (all-in low estimate − RIV) / RIV — null when ungated/anomalous/review */42  estimateVsRiv: number | null;43  /** verdict of the most informative comparison: the bid when there is one, else the low estimate */44  verdict: AskVerdict;45  reasons: string[];46  /** what the verdict is based on */47  basedOn: 'bid' | 'estimate' | 'none';48}4950const allIn = (usd: number | null, i: LotAssessmentInput) => {51  if (usd === null || !(usd > 0)) return null;52  return allInPrice({ price: usd, buyerPremiumIncluded: i.buyerPremiumIncluded ?? false, house: i.house, saleType: 'auction', sourceType: 'auction_house', fxToScheduleCurrency: i.fxUsdToScheduleCurrency });53};5455export function assessLot(i: LotAssessmentInput): LotAssessment {56  const hasBid = (i.bidCount ?? 0) > 0 && i.currentBidUsd !== null && i.currentBidUsd > 0;57  const bid = hasBid ? allIn(i.currentBidUsd, i) : null;58  const lo = allIn(i.estimateLowUsd, i);59  const hi = allIn(i.estimateHighUsd, i);60  const hammer = allIn(i.hammerPriceUsd ?? null, i);61  const ref = bid ?? lo ?? hi ?? hammer;62  const riv = i.riv?.rivUsd ?? null;63  const gate = (askUsd: number | null) =>64    assessAsk({ askUsd, rivUsd: riv, confidence: i.riv?.confidence ?? null, sampleSize: i.riv?.sampleSize ?? null, basis: i.riv?.basis ?? undefined, sameVariant: i.sameVariant });65  const bidA = bid ? gate(bid.allIn) : null;66  const loA = lo ? gate(lo.allIn) : null;67  const primary = bidA ?? loA;68  const usable = (a: ReturnType<typeof assessAsk> | null) => (a && (a.verdict === 'deal' || a.verdict === 'fair' || a.verdict === 'premium') ? a.discount : null);69  return {70    buyerPremiumRate: ref ? round(ref.rate, 4) : null,71    feeBasis: ref?.basis ?? 'unknown',72    allInBidUsd: bid?.allIn ?? null,73    allInEstimateLowUsd: lo?.allIn ?? null,74    allInEstimateHighUsd: hi?.allIn ?? null,75    allInHammerUsd: hammer?.allIn ?? null,76    rivUsd: riv,77    bidVsRiv: usable(bidA),78    estimateVsRiv: usable(loA),79    verdict: primary?.verdict ?? 'ungated',80    reasons: primary?.reasons ?? (riv === null ? ['no_riv'] : ['no_bid_or_estimate']),81    basedOn: bidA ? 'bid' : loA ? 'estimate' : 'none',82  };83}84