Skip to content

feat: durable market-data time series via TimescaleDB#72

Merged
emrebulutlar merged 2 commits into
mainfrom
feat/timescale-marketdata
Jul 5, 2026
Merged

feat: durable market-data time series via TimescaleDB#72
emrebulutlar merged 2 commits into
mainfrom
feat/timescale-marketdata

Conversation

@emrebulutlar

Copy link
Copy Markdown
Member

Problem

Everything on the market-data plane truncated on a market gateway 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.

  • Raw aggregated trades -> trades hypertable (TimescaleDB, new marketdata DB on the system PG16)
  • Candles derived in-database: candles_1m continuous 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
  • REST /api/candles, WS on-connect CANDLE_HISTORY, trade tape: DB-first with the in-memory rings as fallback; live per-trade CANDLE_UPDATE stays in-memory
  • True rolling-24h ticker recomputed from the 1m cagg every 5s and merged with live trades
  • Boot hydration seeds the rings (incl. the in-flight bucket) before egress polling starts

Uptime 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/candles response, time still epoch seconds, doubles throughout (display-grade; money authority is the OMS plane). trading-ui needs no changes. One improvement: candle tradeCount now sums the aggregated trade_count instead of counting batch entries, so in-memory and DB-derived candles are identical.

Testing

  • 257 unit tests green; new: bucket-alignment proof (time_bucket origin ≡ epoch flooring for all six widths), TradeWriter overflow/retention/poison-batch, read fallback + cache, ticker baseline merge, ring seeding
  • TimescaleIntegrationTest (gated on MARKET_PG_TEST_URL, CI runs a timescale/timescaledb:latest-pg16 service): schema idempotency, identical trade stream -> identical candles from DB caggs vs in-memory rings across all six intervals, 24h baseline, policy registration
  • New /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_total

Rollout

Deploys dark: without MARKET_PG_PASSWORD in the environment the persistence layer is off. Provisioning runbook: deploy/timescale/provision-timescaledb.sh (installs the extension into the system PG16, creates marketdata + 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

… 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
emrebulutlar merged commit b751670 into main Jul 5, 2026
2 checks passed
@emrebulutlar
emrebulutlar deleted the feat/timescale-marketdata branch July 5, 2026 23:11
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant