spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { describe, expect, it } from "vitest";2import type { Observation } from "@market-atlas/market-model";3import { ConsensusEngine, weightedMedian } from "./consensus.js";45const NOW = 1_789_200_000_000;6const profiles: Record<string, { family: string | null; reliability: number; isOfficial: boolean }> = {7 a: { family: "fa", reliability: 0.9, isOfficial: false },8 b: { family: "fb", reliability: 0.9, isOfficial: false },9 c: { family: "fc", reliability: 0.9, isOfficial: false },10 d: { family: "fd", reliability: 0.9, isOfficial: false },11 e: { family: "fa", reliability: 0.9, isOfficial: false }, // same upstream as a12 eod: { family: "official", reliability: 0.95, isOfficial: true },13};14const engine = () => new ConsensusEngine((id) => ({ ...(profiles[id] ?? { family: null, reliability: 0.6, isOfficial: false }), realtimeStatus: "REALTIME" }));1516function obs(source: string, value: number, ageMs = 500, field: Observation["field"] = "LAST_PRICE", rt: Observation["realtimeStatus"] = "REALTIME", trust: Observation["timestampTrust"] = "EXCHANGE"): Observation {17 return {18 observationId: `${source}-${field}-${value}-${ageMs}`,19 instrumentId: "crypto_btc_usd",20 symbol: "BTC-USD",21 field,22 value,23 currency: "USD",24 sourceTimestamp: NOW - ageMs,25 timestampTrust: trust,26 rightsStatus: "PUBLIC_ATTRIBUTED",27 realtimeStatus: rt,28 sourceId: source,29 connectorId: `${source}-ws`,30 receivedAt: NOW - ageMs + 50,31 latencyMs: 50,32 rawRef: null,33 normalizerVersion: "1.0",34 };35}3637describe("consensus engine", () => {38 it("all sources agree → canonical price with high confidence", () => {39 const e = engine();40 for (const [s, v] of [["a", 100], ["b", 100], ["c", 100]] as const) e.ingest(obs(s, v));41 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;42 expect(q.price).toBe(100);43 expect(q.sourceCount).toBe(3);44 expect(q.dispersionBps).toBe(0);45 expect(q.confidence).toBeGreaterThan(0.85);46 expect(q.confidence).toBeLessThanOrEqual(0.995);47 });48 it("one stale source is excluded and labelled", () => {49 const e = engine();50 e.ingest(obs("a", 100));51 e.ingest(obs("b", 100.1));52 e.ingest(obs("c", 90, 60_000)); // 60 s old realtime → stale53 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;54 expect(q.price).toBeGreaterThanOrEqual(100);55 expect(q.contributions.find((c) => c.sourceId === "c")).toMatchObject({ included: false, reason: "stale" });56 expect(q.sourceCount).toBe(2);57 });58 it("one extreme outlier among ≥3 is rejected", () => {59 const e = engine();60 e.ingest(obs("a", 100));61 e.ingest(obs("b", 100.2));62 e.ingest(obs("c", 99.9));63 e.ingest(obs("d", 130));64 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;65 expect(q.price).toBeLessThan(101);66 expect(q.contributions.find((c) => c.sourceId === "d")).toMatchObject({ included: false, reason: "outlier" });67 });68 it("two source clusters disagree → low confidence, large dispersion", () => {69 const e = engine();70 e.ingest(obs("a", 100));71 e.ingest(obs("b", 101.5));72 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;73 expect(q.dispersionBps).toBeGreaterThan(100);74 expect(q.confidence).toBeLessThan(0.7);75 });76 it("only one source available → modest confidence, count 1", () => {77 const e = engine();78 e.ingest(obs("a", 100));79 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;80 expect(q.price).toBe(100);81 expect(q.sourceCount).toBe(1);82 expect(q.confidence).toBeLessThan(0.85);83 });84 it("sources sharing an upstream family count once", () => {85 const e = engine();86 e.ingest(obs("a", 100));87 e.ingest(obs("e", 100));88 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;89 expect(q.sourceCount).toBe(1);90 });91 it("out-of-order older observation does not overwrite the newer one", () => {92 const e = engine();93 e.ingest(obs("a", 100, 100));94 e.ingest(obs("a", 90, 5000));95 expect(e.compute("crypto_btc_usd", "BTC-USD", NOW)!.price).toBe(100);96 });97 it("end-of-day value is superseded by a live source, but used when alone", () => {98 const e = engine();99 e.ingest(obs("eod", 95, 3_600_000, "LAST_PRICE", "END_OF_DAY", "SOURCE"));100 expect(e.compute("crypto_btc_usd", "BTC-USD", NOW)!.price).toBe(95);101 expect(e.compute("crypto_btc_usd", "BTC-USD", NOW)!.realtimeStatus).toBe("END_OF_DAY");102 e.ingest(obs("a", 100));103 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;104 expect(q.price).toBe(100);105 expect(q.realtimeStatus).toBe("REALTIME");106 expect(q.contributions.find((c) => c.sourceId === "eod")).toMatchObject({ included: false, reason: "not_comparable" });107 });108 it("market closed / everything stale → STALE status with last known value and zero confidence", () => {109 const e = engine();110 e.ingest(obs("a", 100, 10 * 60_000));111 const q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;112 expect(q.realtimeStatus).toBe("STALE");113 expect(q.price).toBe(100);114 expect(q.confidence).toBe(0);115 });116 it("derives change from previous close and tracks session high/low", () => {117 const e = engine();118 e.ingest(obs("a", 100));119 e.ingest(obs("a", 95, 500, "PREVIOUS_CLOSE"));120 let q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;121 expect(q.change).toBeCloseTo(5);122 expect(q.changePercent).toBeCloseTo(5.263, 2);123 e.ingest(obs("a", 102, 100));124 q = e.compute("crypto_btc_usd", "BTC-USD", NOW)!;125 expect(q.sessionHigh).toBe(102);126 expect(q.sessionLow).toBe(100);127 });128 it("weighted median", () => {129 expect(weightedMedian([{ v: 1, w: 1 }, { v: 2, w: 1 }, { v: 100, w: 1 }])).toBe(2);130 expect(weightedMedian([{ v: 1, w: 10 }, { v: 2, w: 1 }, { v: 100, w: 1 }])).toBe(1);131 });132});133134describe("comparability, roles and coverage inputs", () => {135 const obsT = (source: string, value: number, ageMs: number, type: Observation["observationType"], rt: Observation["realtimeStatus"], rights: Observation["rightsStatus"] = "PUBLIC_ATTRIBUTED"): Observation => ({136 ...obs(source, value, ageMs, "LAST_PRICE", rt, "SOURCE"),137 observationType: type,138 rightsStatus: rights,139 observationId: `${source}-${type}-${value}`,140 });141 it("an official fixing is never compared to a live market value (no false divergence)", () => {142 const e = engine();143 e.ingest(obsT("a", 1.1725, 500, "TRADE", "REALTIME")); // Kraken EUR/USD live144 e.ingest(obsT("eod", 1.1592, 20 * 3_600_000, "OFFICIAL_FIX", "END_OF_DAY", "OFFICIAL_OPEN_DATA")); // ECB fixing yesterday145 const q = e.compute("crypto_btc_usd", "EURUSD", NOW)!;146 expect(q.price).toBe(1.1725);147 expect(q.comparability).toBe("LIVE");148 expect(q.dispersionBps).toBe(0);149 expect(q.contributions.find((c) => c.sourceId === "eod")).toMatchObject({ included: false, reason: "not_comparable" });150 });151 it("official fixing is canonical when no real live market exists; stablecoin proxies alone are INDICATIVE", () => {152 const e = engine();153 e.ingest(obsT("eod", 1.1592, 20 * 3_600_000, "OFFICIAL_FIX", "END_OF_DAY", "OFFICIAL_OPEN_DATA"));154 e.ingest(obsT("b", 1.1601, 500, "STABLECOIN_PROXY", "REALTIME"));155 let q = e.compute("crypto_btc_usd", "EURUSD", NOW)!;156 expect(q.comparability).toBe("FIX");157 expect(q.price).toBe(1.1592);158 const e2 = engine();159 e2.ingest(obsT("b", 1.1601, 500, "STABLECOIN_PROXY", "REALTIME"));160 q = e2.compute("crypto_btc_usd", "EURUSD", NOW)!;161 expect(q.realtimeStatus).toBe("INDICATIVE");162 expect(q.sourceCount).toBe(0); // proxies are not independent families163 expect(q.price).toBe(1.1601);164 });165 it("proxies confirm a real market without setting the price; validators never vote", () => {166 const e = engine();167 e.ingest(obsT("a", 100, 300, "TRADE", "REALTIME"));168 e.ingest(obsT("b", 100.02, 300, "STABLECOIN_PROXY", "REALTIME"));169 e.ingest(obsT("c", 100.01, 300, "TRADE", "DELAYED", "PUBLIC_RESTRICTED_REDISTRIBUTION"));170 const q = e.compute("crypto_btc_usd", "EURUSD", NOW)!;171 expect(q.price).toBe(100);172 expect(q.sourceCount).toBe(1);173 expect(q.proxyCount).toBe(1);174 expect(q.validatorCount).toBe(1);175 expect(q.contributions.find((c) => c.sourceId === "c")).toMatchObject({ included: false, reason: "validation_only" });176 expect(q.contributions.find((c) => c.sourceId === "c")!.deltaBps).toBeCloseTo(1, 0);177 expect(q.rightsStatus).toBe("PUBLIC_ATTRIBUTED"); // restricted validator does not taint the canonical value178 });179 it("same-class values far apart in time are temporal mismatches, not divergence", () => {180 const e = engine();181 e.ingest(obsT("h", 91.01, 8 * 86_400_000, "EOD_CLOSE", "END_OF_DAY", "LICENSED")); // last week's close182 e.ingest(obsT("g", 95.5, 20 * 3_600_000, "EOD_CLOSE", "END_OF_DAY", "LICENSED")); // yesterday's close183 const q = e.compute("crypto_btc_usd", "CL=F", NOW)!;184 expect(q.price).toBe(95.5);185 expect(q.contributions.find((c) => c.sourceId === "h")).toMatchObject({ included: false, reason: "temporal_mismatch" });186 });187});188