feat: durable market-data time series via TimescaleDB#72
Merged
Conversation
… rolling 24h ticker) Chart candles, the recent-trades tape, and ticker stats previously lived only in gateway memory and truncated on every market-gateway restart; the "24h" ticker was actually since-boot accumulation. The raw aggregated-trade stream is now persisted to a TimescaleDB `trades` hypertable (the source of truth), candles for all six intervals are derived in-database by hierarchical real-time continuous aggregates, and REST/WS history reads are DB-first with the in-memory rings as live-push path and fallback. On boot the gateway seeds its rings from the DB, so history survives restarts; a rolling-24h ticker baseline is recomputed from the 1m aggregate every 5s. - egress thread never touches JDBC: lock-free queue -> single writer thread, batched inserts, backoff + retention during outages, drop-and-count when full - gateway keeps serving from memory when Postgres is down (uptime first); without MARKET_PG_* env the whole layer is off and behavior is unchanged - schema bootstrap is idempotent and applied by the gateway at startup; reference DDL in match-gateway/src/main/resources/sql/marketdata-schema.sql - candle tradeCount now carries the aggregated trade_count so in-memory and DB-derived candles are identical (proven by TimescaleIntegrationTest) - provisioning runbook: deploy/timescale/provision-timescaledb.sh - new /metrics series: market_pg_up, market_pg_trades_written_total, market_pg_trades_dropped_total, market_pg_write_errors_total, market_pg_batch_flushes_total, market_pg_queue_depth, market_pg_read_fallbacks_total Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… during connect) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
emrebulutlar
added a commit
to openexch/admin-gateway
that referenced
this pull request
Jul 5, 2026
The market gateway now persists chart/time-series data to TimescaleDB (openexch/match#72). URL and user are plumbed through the market ServiceDef; the password comes from the admin service environment via the systemd drop-in admin.service.d/marketdata-db.conf (created by match's deploy/timescale/provision-timescaledb.sh). Without the password the gateway runs pure in-memory as before. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Everything on the market-data plane truncated on a
marketgateway restart: chart candles (all 6 intervals, 500-candle in-memory rings), the recent-trades tape, and the 24h ticker. Worse, the ticker's "24h" figures were since-boot accumulations (open = first trade after boot, volume never rolls off).Design
The database is the source of truth; memory exists only for performance.
tradeshypertable (TimescaleDB, newmarketdataDB on the system PG16)candles_1mcontinuous aggregate from trades, then hierarchical caggs 5m/15m/1h/4h/1d, all with real-time aggregation (materialized_only = false) so the current bucket is included at query time/api/candles, WS on-connectCANDLE_HISTORY, trade tape: DB-first with the in-memory rings as fallback; live per-tradeCANDLE_UPDATEstays in-memoryUptime discipline: the egress thread never touches JDBC (lock-free queue -> dedicated writer thread, batched inserts, exponential backoff, batch retention during outages, drop-and-count on a full queue). Netty event loops only receive completed futures; reads run on a 2-thread pool with a 1.5s time-box and a 2s JSON cache (absorbs WS connect/resync storms). If Postgres is down or unconfigured the gateway behaves exactly as before this PR.
Consumer contract
Unchanged. Same WS message shapes, same
/api/candlesresponse,timestill epoch seconds, doubles throughout (display-grade; money authority is the OMS plane). trading-ui needs no changes. One improvement: candletradeCountnow sums the aggregatedtrade_countinstead of counting batch entries, so in-memory and DB-derived candles are identical.Testing
TimescaleIntegrationTest(gated onMARKET_PG_TEST_URL, CI runs atimescale/timescaledb:latest-pg16service): schema idempotency, identical trade stream -> identical candles from DB caggs vs in-memory rings across all six intervals, 24h baseline, policy registration/metrics:market_pg_up,market_pg_trades_written_total,market_pg_trades_dropped_total,market_pg_write_errors_total,market_pg_batch_flushes_total,market_pg_queue_depth,market_pg_read_fallbacks_totalRollout
Deploys dark: without
MARKET_PG_PASSWORDin the environment the persistence layer is off. Provisioning runbook:deploy/timescale/provision-timescaledb.sh(installs the extension into the system PG16, createsmarketdata+marketdata_test, writes the admin-service drop-in). Rollback = remove the drop-in and restart.Known accepted limitations: trades during gateway downtime are absent (no cluster replay backfill this phase); leader-seam redelivery can double-insert (no trade IDs in the aggregated stream — same as today's in-memory double-count).
🤖 Generated with Claude Code