Live

A self-hosted warehouse for market data.

FX, metals, indices, commodities, crypto, OHLC bars and tick data, stored in DuckDB and served over a FastAPI layer with cursor-paginated queries, Parquet backups, and a WebSocket endpoint for historical replay.

Current coverage

updated

On disk
~4 GB
DuckDB, compressed
Instruments
73
across asset classes
Timeframes
8
M1 → D1, plus ticks
History depth
25 yrs
2000-07-16 → 2026-04-20

Architecture

Receive. Store. Serve.

Three components, clean boundaries, one VPS.

  1. 01

    FastAPI

    ingest · query · WebSocket

    Handles ingestion, auto-derivation from M1 to D1, cursor-paginated queries, and historical replay.

  2. 02

    DuckDB + Postgres

    market data · auth

    DuckDB holds OHLC bars and ticks in an embedded columnar file. Postgres holds users and API keys.

  3. 03

    Caddy

    TLS · reverse proxy

    Terminates TLS, proxies /api/* to FastAPI and / to this static site.

Stack

What it's built with.

  • FastAPI
    REST + WebSocket API
  • DuckDB
    OHLC + tick storage
  • PostgreSQL
    users + API keys
  • Caddy
    TLS reverse proxy
  • Docker Compose
    container orchestration
  • Hetzner
    VPS

API

Sample query.

Queries are cursor-paginated; timestamps are UTC; each row is tagged raw or derived.

GET /api/query?instrument=XAUUSD&timeframe=D1&limit=3
{
  "instrument": "XAUUSD",
  "timeframe": "D1",
  "count": 3,
  "next_cursor": "eyJ0cyI6IjIwMjUtMTEtMDMifQ==",
  "rows": [
    {
      "timestamp": "2025-11-03T00:00:00Z",
      "open":  3987.12,
      "high":  4021.88,
      "low":   3971.05,
      "close": 4014.60,
      "volume": 128334,
      "source": "raw"
    },
    {
      "timestamp": "2025-11-04T00:00:00Z",
      "open":  4014.60,
      "high":  4055.20,
      "low":   4002.15,
      "close": 4048.73,
      "volume": 142019,
      "source": "raw"
    },
    {
      "timestamp": "2025-11-05T00:00:00Z",
      "open":  4048.73,
      "high":  4071.40,
      "low":   4031.90,
      "close": 4060.25,
      "volume": 117445,
      "source": "raw"
    }
  ]
}

Row-level queries require an API key. The stats above come from an aggregate-only public endpoint — no row content is exposed without auth.