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/<day>/<connector>/<sha>.json.gz(redacted, content-addressed). Polled/bulk payloads always; streaming frames sampled (MA_RAW_SAMPLE_RATE).raw_refon observations points back.ma replay <day>re-normalizes offline. - L1/L2 — connector
normalize()(pure, deterministic, no network) →NormalizedObservationwith source symbol + instrument hint, rights & real-time status, timestamp trust. - L3 —
observations(partitioned byreceived_atday; fingerprint dedupes reconnect replays;normalizer_version; REALTIME streaming ticks are persisted at most once per source/instrument/field perMA_TICK_PERSIST_INTERVAL_MS= 2 s while consensus/events see every tick). RetentionMA_OBSERVATION_RETENTION_DAYSthen gzip NDJSON archive inMA_DATA_DIR/archive(never silently deleted;observation_archivesindex). - L4 —
canonical_quotes,bars,market_events,filings,connector_health,fact_changes,document_snapshots.
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). Weight = source reliability × timestamp trust × real-time class × freshness decay × official bonus. One vote per
source family (sources believed to share an upstream count once). When live classes exist, END_OF_DAY/INDICATIVE values are
superseded. 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 (> 50 bps between included sources). 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 (240/min); 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.