spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1"use client";23import { formatBps, formatCompact, formatDuration, formatQuoteValue } from "@/lib/format";4import { useLiveQuote } from "@/lib/stream";5import type { Instrument, Quote } from "@/lib/types";67/** Key stats grid; bid/ask/volume follow the live stream, session values from the canonical quote. */8export function InstrumentStats({ instrument: i, quote: q }: { instrument: Instrument; quote: Quote | null }) {9 const live = useLiveQuote(i.id);10 const fresh = live && q && live.received >= Date.parse(q.updated_at);11 const f = (v: number | null | undefined) => formatQuoteValue(v, i.asset_class, q?.currency);12 const bid = fresh ? live!.bid : q?.bid;13 const ask = fresh ? live!.ask : q?.ask;14 const spreadBps = bid != null && ask != null && ask > 0 ? ((ask - bid) / ((ask + bid) / 2)) * 10_000 : null;15 const items: Array<[string, string]> = [16 ["Open", f(q?.open)],17 ["High", f(q?.high)],18 ["Low", f(q?.low)],19 ["Previous close", f(q?.previous_close)],20 ["Bid", f(bid)],21 ["Ask", f(ask)],22 ["Spread", formatBps(spreadBps)],23 ["Volume", formatCompact(fresh ? live!.volume : q?.volume)],24 ["Session high", f(q?.session_high)],25 ["Session low", f(q?.session_low)],26 ["Source dispersion", formatBps(q?.dispersion_bps)],27 ["Newest observation", q?.freshness_ms == null ? "—" : `${formatDuration(q.freshness_ms)} old at compute`],28 ];29 return (30 <dl className="grid grid-cols-2 gap-x-6 rounded-md border border-rule bg-surface px-4 py-2 sm:grid-cols-3 lg:grid-cols-4">31 {items.map(([k, v]) => (32 <div key={k} className="flex items-baseline justify-between gap-3 border-b border-rule py-1.5 text-sm last:border-0 sm:[&:nth-last-child(-n+3)]:border-0 lg:[&:nth-last-child(-n+4)]:border-0">33 <dt className="text-ink-3">{k}</dt>34 <dd className="mono truncate tnum">{v}</dd>35 </div>36 ))}37 </dl>38 );39}40