spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1// Labels and helpers for Source Mesh v2: observation types, comparability classes, roles, coverage tiers.2import type { Comparability, CoverageStatus, CoverageTier, ObservationType } from "./types";34export const OBSERVATION_TYPE_LABEL: Record<ObservationType, string> = {5 TRADE: "Trade",6 MID: "Mid",7 QUOTE: "Quote",8 INDEX_VALUE: "Index value",9 INDICATIVE: "Indicative",10 STABLECOIN_PROXY: "Stablecoin proxy",11 DERIVED: "Derived",12 OFFICIAL_FIX: "Official fix",13 REFERENCE_RATE: "Reference rate",14 SETTLEMENT: "Settlement",15 EOD_CLOSE: "EOD close",16 NAV: "NAV",17};1819export const COMPARABILITY_LABEL: Record<Comparability, string> = {20 LIVE: "live market consensus",21 FIX: "official fixing",22 EOD: "end-of-day close",23};2425export const COMPARABILITY_OF: Record<ObservationType, Comparability> = {26 TRADE: "LIVE",27 MID: "LIVE",28 QUOTE: "LIVE",29 INDEX_VALUE: "LIVE",30 INDICATIVE: "LIVE",31 STABLECOIN_PROXY: "LIVE",32 DERIVED: "LIVE",33 OFFICIAL_FIX: "FIX",34 REFERENCE_RATE: "FIX",35 SETTLEMENT: "EOD",36 EOD_CLOSE: "EOD",37 NAV: "EOD",38};3940export const PROXY_TYPES = new Set<ObservationType>(["STABLECOIN_PROXY", "DERIVED", "INDICATIVE"]);4142/** Readable copy for exclusion reasons. `canonical` is the comparability class of the canonical value. */43export function reasonLabel(reason: string | null | undefined, type?: ObservationType, canonical?: Comparability | null): string {44 switch (reason) {45 case "stale":46 return "stale — outside its freshness window";47 case "not_comparable": {48 const own = type ? COMPARABILITY_LABEL[COMPARABILITY_OF[type]] : null;49 const can = canonical ? COMPARABILITY_LABEL[canonical] : null;50 return own && can ? `not comparable: ${own} vs ${can}` : "not comparable with the canonical class";51 }52 case "temporal_mismatch":53 return "temporal mismatch — too far from the freshest observation";54 case "validation_only":55 return "validation only — restricted rights, never votes";56 case "validator_disagrees":57 return "validator disagrees with the consensus";58 case "outlier":59 return "outlier — more than 2 % from the median";60 case "superseded_by_live":61 return "superseded by a live source";62 default:63 return reason ?? "excluded";64 }65}6667export type Role = "votes" | "confirms" | "validates" | "excluded";6869export function roleOf(c: { included: boolean; reason: string | null; observation_type?: ObservationType; rights_status: string }): Role {70 if (c.included) return c.observation_type && PROXY_TYPES.has(c.observation_type) ? "confirms" : "votes";71 if (c.reason === "validation_only" || c.reason === "validator_disagrees") return "validates";72 return "excluded";73}7475export const ROLE_LABEL: Record<Role, string> = { votes: "votes", confirms: "confirms (proxy)", validates: "validates", excluded: "excluded" };7677export const TIER_LABEL: Record<CoverageTier, string> = { A: "Tier A · major", B: "Tier B · liquid", C: "Tier C · long tail", D: "Tier D · official reference" };78export const TIER_TARGET: Record<CoverageTier, string> = { A: "≥ 3 independent families, 5 observations", B: "≥ 2 families, 3 observations", C: "1 family, 2 observations", D: "one official source is the truth" };7980export const COVERAGE_STATUS_LABEL: Record<CoverageStatus, string> = { COVERED: "covered", UNDERCOVERED: "under-covered", SINGLE_SOURCE: "single source", NO_DATA: "no data" };81export const COVERAGE_STATUS_TONE: Record<CoverageStatus, string> = {82 COVERED: "bg-positive-soft text-positive",83 UNDERCOVERED: "bg-warning-soft text-warning",84 SINGLE_SOURCE: "bg-warning-soft text-warning",85 NO_DATA: "bg-stale-soft text-stale",86};8788export function formatDeltaBps(v: number | null | undefined): string {89 if (v == null || !Number.isFinite(v)) return "—";90 const a = Math.abs(v);91 const s = a < 10 ? a.toFixed(2) : a < 100 ? a.toFixed(1) : Math.round(a).toString();92 return `${v > 0 ? "+" : v < 0 ? "−" : ""}${s} bp`;93}9495export function deltaTone(v: number | null | undefined): string {96 if (v == null) return "text-ink-3";97 const a = Math.abs(v);98 return a <= 5 ? "text-positive" : a <= 25 ? "text-ink-2" : a <= 100 ? "text-warning" : "text-negative";99}100