// Formatting helpers shared by server and client components. Deterministic (en-US) to avoid hydration drift. const nf = (opts: Intl.NumberFormatOptions) => new Intl.NumberFormat("en-US", opts); const cache = new Map(); function fmt(min: number, max: number) { const k = `${min}:${max}`; let f = cache.get(k); if (!f) { f = nf({ minimumFractionDigits: min, maximumFractionDigits: max }); cache.set(k, f); } return f; } /** Price precision by magnitude and asset class: crypto sub-cent, FX 4–5 decimals, yields 2–3. */ export function priceDecimals(v: number, assetClass?: string | null): number { const a = Math.abs(v); if (assetClass === "FOREX") return a >= 20 ? 3 : a >= 1 ? 4 : 5; if (assetClass === "TREASURY" || assetClass === "INTEREST_RATE" || assetClass === "BOND") return 2; if (a === 0) return 2; if (a >= 10000) return 0; if (a >= 1000) return 1; if (a >= 1) return 2; if (a >= 0.1) return 4; if (a >= 0.01) return 5; return 6; } export function formatPrice(v: number | null | undefined, assetClass?: string | null): string { if (v == null || !Number.isFinite(v)) return "—"; const d = priceDecimals(v, assetClass); return fmt(d, d).format(v); } export function isRateClass(assetClass?: string | null): boolean { return assetClass === "TREASURY" || assetClass === "INTEREST_RATE" || assetClass === "BOND"; } /** Price with unit: yields get a % suffix, everything else the currency code when provided. */ export function formatQuoteValue(v: number | null | undefined, assetClass?: string | null, currency?: string | null): string { if (v == null) return "—"; if (isRateClass(assetClass)) return `${formatPrice(v, assetClass)}%`; const p = formatPrice(v, assetClass); return currency && currency !== "USD" && currency !== "USDT" ? `${p} ${currency}` : p; } export function formatPercent(v: number | null | undefined, digits = 2, sign = true): string { if (v == null || !Number.isFinite(v)) return "—"; const s = fmt(digits, digits).format(Math.abs(v)); return `${sign ? (v > 0 ? "+" : v < 0 ? "−" : "") : v < 0 ? "−" : ""}${s}%`; } export function formatChange(v: number | null | undefined, assetClass?: string | null): string { if (v == null || !Number.isFinite(v)) return "—"; const d = priceDecimals(v, assetClass); const s = fmt(Math.min(d, 4), Math.min(d, 4)).format(Math.abs(v)); return `${v > 0 ? "+" : v < 0 ? "−" : ""}${s}`; } export function formatCompact(v: number | null | undefined, digits = 1): string { if (v == null || !Number.isFinite(v)) return "—"; const a = Math.abs(v); if (a >= 1e12) return `${fmt(0, digits).format(v / 1e12)}T`; if (a >= 1e9) return `${fmt(0, digits).format(v / 1e9)}B`; if (a >= 1e6) return `${fmt(0, digits).format(v / 1e6)}M`; if (a >= 1e4) return `${fmt(0, digits).format(v / 1e3)}K`; return fmt(0, a < 10 ? 2 : 0).format(v); } export function formatInt(v: number | null | undefined): string { if (v == null || !Number.isFinite(v)) return "—"; return fmt(0, 0).format(v); } export function formatBps(v: number | null | undefined): string { if (v == null) return "—"; return `${fmt(0, v < 10 ? 2 : 0).format(v)} bps`; } /** Parse API timestamps: ISO, epoch ms, or Postgres text "2026-09-11 19:50:00-04". */ export function toMs(v: string | number | null | undefined): number | null { if (v == null) return null; if (typeof v === "number") return v; let s = v.trim(); if (/^\d+$/.test(s)) return Number(s); s = s.replace(" ", "T"); if (/[+-]\d{2}$/.test(s)) s += ":00"; const ms = Date.parse(s); return Number.isFinite(ms) ? ms : null; } export function formatDuration(ms: number | null | undefined): string { if (ms == null || !Number.isFinite(ms)) return "—"; if (ms < 1000) return `${Math.round(ms)} ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`; if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`; if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`; return `${Math.floor(ms / 86_400_000)}d ${Math.floor((ms % 86_400_000) / 3_600_000)}h`; } export function relativeTime(ms: number | null | undefined, now = Date.now()): string { if (ms == null) return "—"; const d = now - ms; if (d < 0) return "just now"; if (d < 1000) return `${d} ms ago`; if (d < 60_000) return `${Math.floor(d / 1000)}s ago`; if (d < 3_600_000) return `${Math.floor(d / 60_000)}m ago`; if (d < 86_400_000) return `${Math.floor(d / 3_600_000)}h ago`; return `${Math.floor(d / 86_400_000)}d ago`; } const dtfCache = new Map(); export function formatDateTime(ms: number | string | null | undefined, opts: { tz?: string; seconds?: boolean; dateOnly?: boolean } = {}): string { const t = typeof ms === "string" ? toMs(ms) : ms; if (t == null) return "—"; const key = `${opts.tz ?? "UTC"}:${opts.seconds ? 1 : 0}:${opts.dateOnly ? 1 : 0}`; let f = dtfCache.get(key); if (!f) { f = new Intl.DateTimeFormat("en-US", { timeZone: opts.tz ?? "UTC", year: "numeric", month: "short", day: "2-digit", ...(opts.dateOnly ? {} : { hour: "2-digit", minute: "2-digit", ...(opts.seconds ? { second: "2-digit" } : {}), hourCycle: "h23", timeZoneName: "short" }), }); dtfCache.set(key, f); } return f.format(new Date(t)); } const timeFmt = new Intl.DateTimeFormat("en-GB", { timeZone: "UTC", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }); /** "09:11:41" (UTC) for tape rows. */ export function formatClock(ms: number | null | undefined): string { return ms == null ? "—" : timeFmt.format(new Date(ms)); } export const ASSET_CLASS_LABEL: Record = { EQUITY: "Stock", ETF: "ETF", ETN: "ETN", INDEX: "Index", CRYPTO: "Crypto", FOREX: "FX", COMMODITY: "Commodity", FUTURE: "Future", OPTION: "Option", BOND: "Bond yield", TREASURY: "Treasury", INTEREST_RATE: "Rate", MUTUAL_FUND: "Fund", ADR: "ADR", REIT: "REIT", WARRANT: "Warrant", OTHER: "Other", }; export const EVENT_TYPE_LABEL: Record = { PRICE_CHANGE: "Price move", SESSION_HIGH: "Session high", SESSION_LOW: "Session low", VOLATILITY_SPIKE: "Volatility spike", VOLUME_SPIKE: "Volume spike", SOURCE_DIVERGENCE: "Source divergence", SOURCE_FAILURE: "Source failure", SOURCE_RECOVERY: "Source recovery", SCHEMA_DRIFT: "Schema drift", MARKET_OPEN: "Market open", MARKET_CLOSE: "Market close", TRADING_HALT: "Trading halt", TRADING_RESUME: "Trading resumed", FILING_PUBLISHED: "Filing", DOCUMENT_CHANGED: "Document changed", INSTRUMENT_LISTED: "Listed", INSTRUMENT_DELISTED: "Delisted", REFERENCE_RATE_PUBLISHED: "Reference rate", RATE_DECISION: "Rate decision", }; export const STATUS_LABEL: Record = { REALTIME: "Live", DELAYED: "Delayed", END_OF_DAY: "End of day", AT_CLOSE: "At close", STALE: "Stale", WITHHELD: "Withheld", INDICATIVE: "Indicative", UNKNOWN: "Unknown", }; export function instrumentHref(idOrSymbol: string): string { return `/instruments/${encodeURIComponent(idOrSymbol)}`; } export function cx(...xs: Array): string { return xs.filter(Boolean).join(" "); }