# Market Atlas — architecture (v0.1, 2026-09-12) Market Atlas is a market **observation infrastructure**: a connector network observes public/official sources, normalizes their outputs into one observation model, resolves instruments, builds a multi-source consensus, derives events, keeps the history, and redistributes the result through one web app, a REST API and a stream. ``` sources (WS / XHR / official APIs / RSS / XML / HTML / bulk files) └─ connectors/ ConnectorDefinition (metadata + rights + normalize()) [L0 raw archived, sampled] └─ ConnectorManager lifecycle · adaptive polling · rate limits · circuit breaker · schema drift └─ Pipeline validate → resolve instrument → Observation (L3) → ObservationWriter (Postgres, daily partitions) ├─ ConsensusEngine weighted median · source families · outliers · freshness → CanonicalQuote │ └─ QuoteStore hot cache + debounced upsert · bus "canonical.quote" ├─ EventEngine price change · session high/low · volatility · divergence · halts · filings · market open/close ├─ BarAggregator live 1m bars → SQL rollups 5m/15m/1h/1d (+ backfill bars from historical connectors) └─ HealthEngine state · latency · parse rate · reliability score (0–100) · minute snapshots MarketBus (in-process, topic names = spec) → API (Fastify): REST /v1 · WebSocket /v1/stream · SSE /v1/sse · Prometheus /v1/metrics apps/web (Next.js) consumes the API and the stream; in production the API process is the public edge and proxies to Next. ``` ## Packages | Path | Role | |---|---| | `packages/market-model` | Vocabularies (asset classes, fields, rights/real-time statuses, event types), types (Instrument, Observation, CanonicalQuote, MarketEvent…), stable id builders, zod schemas, Intl-only time-zone utilities. | | `packages/connector-sdk` | `defineConnector`, `ConnectorContext`, polite `HttpClient` (per-host token buckets, ETag/If-Modified-Since, jittered retries, redacted URLs), `ManagedWebSocket` (reconnect, heartbeat, stale detection), parsers (RSS/Atom, XML, delimited, numbers, HTML tables, change-detection fingerprints), schema fingerprints, SSRF guard, offline test helpers. | | `connectors/` | One folder per connector: `index.ts`, `fixtures/`, `README.md`; `sources.ts` = source registry with families; `connectors.test.ts`. | | `apps/api` | Runtime: config, Postgres access + forward-only SQL migrations (`infra/migrations`), core engines, connector manager, scheduler, REST/WS/SSE, admin API, discovery prototype, CLI (`ma`). Bundled with esbuild to `dist/`. | | `apps/web` | Next.js 16 app (see `docs/FRONTEND.md`). | ## Data layers - **L0 raw** — `MA_DATA_DIR/raw///.json.gz` (redacted, content-addressed). Polled/bulk payloads always; streaming frames sampled (`MA_RAW_SAMPLE_RATE`). `raw_ref` on observations points back. `ma replay ` re-normalizes offline. - **L1/L2** — connector `normalize()` (pure, deterministic, no network) → `NormalizedObservation` with source symbol + instrument hint, rights & real-time status, timestamp trust. - **L3** — `observations` (partitioned by `received_at` day; fingerprint dedupes reconnect replays; `normalizer_version`; REALTIME streaming ticks are persisted at most once per source/instrument/field per `MA_TICK_PERSIST_INTERVAL_MS` = 2 s while consensus/events see every tick). Retention `MA_OBSERVATION_RETENTION_DAYS` then gzip NDJSON archive in `MA_DATA_DIR/archive` (never silently deleted; `observation_archives` index). - **L4** — `canonical_quotes`, `bars`, `market_events`, `filings`, `connector_health`, `fact_changes`, `document_snapshots`. ## Source Mesh v2 (2026-09-13) **One instrument → many observers → one canonical market state.** Every observation carries an `observation_type` (TRADE, MID, QUOTE, INDEX_VALUE, INDICATIVE, STABLECOIN_PROXY, DERIVED, OFFICIAL_FIX, REFERENCE_RATE, SETTLEMENT, EOD_CLOSE, NAV) that maps to a comparability class (LIVE / FIX / EOD). Only comparable observations are compared: an ECB fixing is never measured against a live market, last week's close is never "divergent" from yesterday's. Roles: real markets **vote**; stablecoin/derived proxies **confirm** (reduced weight, never counted as families); restricted-rights sources (nasdaq.com) **validate** only (`validation_only`, never voting, never redistributed). `core/coverage.ts` scores every quoted instrument against a tier target (A ≥ 3 families / 5 observations; B ≥ 2 / 3; C 1 / 2; D official single source) and produces the source-expansion queue (`GET /v1/coverage`). `core/lineage.ts` infers shared upstreams statistically (identical values within 2 s over ≥ 120 aligned samples → similarity ≥ 0.92 → one family, `source_lineage` table). Live FX without keys: Kraken fiat markets (12 pairs, TRADE), Bitstamp EUR/USD & GBP/USD, Bitfinex EUR/GBP/JPY vs USD, plus stablecoin proxies (EUR/USDT, USDC/EUR, USDT/BRL…) on Binance, OKX, Coinbase, Bybit, Gate, KuCoin. ## Consensus (apps/api/src/core/consensus.ts) Per instrument and field, the newest observation of each source is kept (out-of-order values ignored). A value is *fresh* within a window depending on its real-time class (REALTIME 15 s, DELAYED 30 min, INDICATIVE 1 h, END_OF_DAY 10 d; 4 d for live classes while the venue is closed — the last session value is reported as AT_CLOSE, never as live). Comparability class first (LIVE from real markets > FIX > EOD > live proxies alone), then temporal tolerance inside the class (live: 2 min; fixings/closes: same UTC day → `temporal_mismatch`). Weight = source reliability × timestamp trust × real-time class × freshness decay × official bonus (×1.6 for rates). One vote per **source family** (declared or statistically inferred). Crypto weights scale with the venue's share of 24 h notional volume. Outliers (> 2 % from the weighted median with ≥ 3 candidates) are excluded. Output: weighted median, dispersion (bps), independent-family count, freshness, confidence (agreement + redundancy + freshness + reliability, capped at 0.995), and the full contribution list with inclusion reasons — exposed as **"Why this price?"** (`GET /v1/quotes/:id/provenance`). Rights guard: a canonical value built from any non-redistributable source is `INTERNAL_ONLY` and withheld from public responses (metadata still visible). `PUBLICLY_REDISTRIBUTABLE` in `market-model` is the single source of truth. ## Market hours (core/calendar.ts) Exchanges carry IANA time zone, sessions (regular/pre/post, multi-session for Asia, weekday sets for Gulf markets, `continuous` for crypto venues) and holidays (seed + HTML connector). States PRE/OPEN/POST/CLOSED with DST handled by Intl. Used by adaptive polling, consensus freshness, MARKET_OPEN/CLOSE events, session resets and the world map. ## Events (core/events.ts) Derived from canonical quotes with per-instrument adaptive baselines (EWMA of tick moves): PRICE_CHANGE (≥ max(0.5 %, 8× typical tick)), SESSION_HIGH/LOW (after history, ≥ 0.1 % improvement, cooldown), VOLATILITY_SPIKE (30-tick realized vol ≥ 4× baseline), SOURCE_DIVERGENCE (> 100 bps between included sources, hourly per instrument). Connector-proposed events (halts, filings, document changes) and system events (SOURCE_FAILURE, SCHEMA_DRIFT, MARKET_OPEN/CLOSE) go through the same dedupe (fingerprint, 24 h) and confirmation counting. ## Resilience - Connector failures: jittered exponential backoff, FAILED after 3 consecutive poll failures (SOURCE_FAILURE event), auto-restart. - Schema drift: structural fingerprint per payload kind; new shapes are recorded (`connector_schema_changes`); repeated shapes that normalize to nothing pause the connector (SCHEMA_DRIFT event) rather than emitting wrong values. - Backpressure: bounded observation queue (200 k) — only REALTIME ticks are dropped under pressure, never events; batched inserts. - Graceful shutdown flushes writers, quotes, bars, events, raw archive. - Split roles are possible (`MA_ROLE=api|worker`) — the bus is in-process today; topic names match the spec for a future NATS/Redpanda. ## Security Admin API behind `x-ma-admin-token` (timing-safe compare); public REST rate-limited per IP (20 req/s, burst 300; web pages and streams exempt); security headers; strict parameter validation (zod); SSRF guard on discovery URLs (private ranges, localhost, cluster hostnames, DNS resolution check); secrets redacted in logs and raw archives; no stack traces in responses.