Post

go-fdp & go-trade — Finance Data Proxy and Market Model

Binance historical klines from public S3, DuckDB + Parquet cache, unified cross-exchange market data model.

go-fdp & go-trade — Finance Data Proxy and Market Model

Two complementary Go libraries for market data: a finance data proxy and a unified trading data model.

Repositories:

go-fdp architecture

graph TB
    subgraph "Data sources"
        S3["Binance Vision S3<br/>data.binance.vision"]
        API["Binance REST<br/>api.binance.com"]
    end

    subgraph "go-fdp"
        ETL["pkg/binance<br/>S3 bulk + live hours"]
        APIpkg["pkg/binance<br/>REST klines · aggTrades"]
        MKT["internal/market.API<br/>lazy gap repair"]
        STORE["internal/store<br/>DuckDB over Parquet"]
        HTTP["internal/handler<br/>REST /v1/* gzip"]
    end

    subgraph "Storage"
        PQ["Parquet + Hive partitions<br/>data/mtype=spot/..."]
    end

    S3 -->|"anonymous S3"| ETL
    APIpkg -->|"today's candles"| PQ
    ETL --> PQ
    HTTP --> MKT
    MKT --> ETL
    MKT --> APIpkg
    MKT --> STORE
    STORE --> PQ

Raw files vs queryable history

graph LR
    subgraph "Without FDP"
        DL["Download ZIPs<br/>from Binance Vision"]
        MAN["Manual unzip<br/>+ CSV parsing"]
        ADH["Ad-hoc scripts<br/>per symbol / day"]
    end

    subgraph "With go-fdp"
        REQ["GET /v1/data<br/>from · to · market"]
        AUTO["Auto ETL + cache"]
        OUT["JSON candles<br/>or aggTrades"]
    end

    DL --> MAN --> ADH
    REQ --> AUTO --> OUT

Without a proxy: manage S3 paths, daily ZIP layouts, decompression, schema mapping, and missing “today” data yourself.

With FDP: request a time range; the service fetches missing Parquet from S3 (or live API for the current day), runs DuckDB, and returns JSON.

go-trade — unified data model

Exchange-agnostic types so Binance, CME, NASDAQ, and forex connectors normalize into the same structs.

graph TB
    subgraph "Exchange Connectors"
        BIN["Binance"]
        CME["CME Group"]
        NAS["NASDAQ"]
        FX["Forex Brokers"]
        CEX["Other CEX/DEX"]
    end

    subgraph "go-trade — Unified Data Model"
        TAS["TimeAndSale<br/>Atomic trade events"]
        CND["Candle<br/>OHLC + microstructure"]
        OB["OrderBook<br/>Bid/Ask snapshots"]
        ORD["Order<br/>Trade orders"]
        INS["Instrument<br/>Tradeable assets"]
        MKT["Market<br/>Trading pairs"]
        SYM["Symbol<br/>Hierarchical asset tree"]
        CUR["currency.Provider<br/>170+ fiat, 60+ crypto"]
    end

    subgraph "Storage & Analysis"
        DB["TimescaleDB<br/>PostgreSQL"]
        PQ["Parquet Files"]
        DASH["Analytics<br/>Dashboard"]
    end

    BIN -->|"normalize"| TAS
    CME -->|"normalize"| TAS
    NAS -->|"normalize"| TAS
    FX -->|"normalize"| TAS
    CEX -->|"normalize"| TAS

    TAS --> CND
    TAS --> OB

    TAS --> DB
    CND --> DB
    OB --> PQ
    ORD --> DB

    DB --> DASH
    PQ --> DASH

    INS -.->|"classifies"| TAS
    MKT -.->|"identifies"| INS
    SYM -.->|"defines"| MKT
    CUR -.->|"resolves"| SYM

Normalization flow

sequenceDiagram
    participant Exchange
    participant Connector
    participant Model as go-trade Model
    participant Store

    Exchange->>Connector: Raw trade event (JSON/WebSocket)
    Connector->>Connector: Parse exchange-specific format
    Connector->>Model: trade.TimeAndSale{Ticker, Price, Volume, Side}
    Model->>Store: Normalized row

    Note over Model: Same struct regardless<br/>of source exchange

How they compose

Tech stack

Go · DuckDB · Parquet · Binance API · TimescaleDB · REST

This post is licensed under CC BY 4.0 by the author.