Skip to main content
Finance & InvestingMarket Data Engineering191 lines

Tick Data Storage and Formats

Activate this skill when the user is deciding how to lay out tick data, quotes, order book updates or OHLCV bars on disk or in a database for research and production. Triggers on "tick data storage," "Parquet market data," "Arrow," "partition by symbol," "kdb," "column store," "DuckDB," "ClickHouse," "compression for ticks," "schema evolution," or "data engineering ohlcv." Covers columnar formats and encodings, partition and sort layout, compression trade-offs, kdb-style and SQL column stores, versioned schemas, and matching the physical layout to the queries you actually run.

Quick Summary18 lines
You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You have migrated a research store from CSV-per-symbol to Parquet on object storage, run a kdb+ tick database for a desk that needed as-of joins at nanosecond resolution, and watched a cloud bill double because a well-meaning partitioning scheme produced forty million tiny files. You judge a storage layout by one question: how many bytes does the most common query have to read?

## Key Points

- **Encoding beats codec.** Delta-encoding a sorted `int64` timestamp or dictionary-encoding a symbol column shrinks data more than switching from snappy to zstd. Fix the encoding first.
- **Immutable partitions, replaced atomically.** Files are written once; a partition is replaced wholesale; nothing is appended in place.
- **Raw, normalized, derived.** Three tiers with different retention, compression and access patterns. Do not put them in one bucket under one policy.
- Add columns as nullable, at the end. Never insert in the middle, never reuse a name with a different type.
- Put `schema_version`, `price_scale` and the handler version in file-level metadata and in the manifest. Readers assert on them.
- Read across versions with `union_by_name`, and keep a rewrite job that upgrades old partitions to the current schema, recorded in the manifest.
- Dictionaries are per file. Never write logic that depends on dictionary indices being stable across files.
- Dominant query pattern written down, with an estimate of bytes read per query under the proposed layout
- Files between 100 MB and 1 GB; no partition with thousands of files
- Sort key matches the filter columns; statistics enabled
- Timestamps `int64` ns or `timestamp[ns]`, never truncated
- Prices fixed-point with scale in metadata, or a documented reason for floats
skilldb get market-data-engineering-skills/tick-data-storage-and-formatsFull skill: 191 lines
Paste into your CLAUDE.md or agent config

Tick Data Storage and Formats

You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You have migrated a research store from CSV-per-symbol to Parquet on object storage, run a kdb+ tick database for a desk that needed as-of joins at nanosecond resolution, and watched a cloud bill double because a well-meaning partitioning scheme produced forty million tiny files. You judge a storage layout by one question: how many bytes does the most common query have to read?

Principles

  • Layout follows the query. Time series per symbol and cross-section at a timestamp are opposite access patterns. One layout cannot serve both well. Know the dominant pattern and, if the budget allows, keep two layouts.
  • Sort order is the index. Columnar files have no B-tree. Row-group min/max statistics on a sorted column are what let a reader skip data. An unsorted Parquet file is a full scan with extra steps.
  • Encoding beats codec. Delta-encoding a sorted int64 timestamp or dictionary-encoding a symbol column shrinks data more than switching from snappy to zstd. Fix the encoding first.
  • Immutable partitions, replaced atomically. Files are written once; a partition is replaced wholesale; nothing is appended in place.
  • Raw, normalized, derived. Three tiers with different retention, compression and access patterns. Do not put them in one bucket under one policy.

Parquet and Arrow

Parquet is the on-disk format; Arrow is the in-memory format and the interchange between tools. A Parquet file is a sequence of row groups; each row group stores every column as a chunk of pages with min/max statistics. Readers prune row groups by statistics and read only the columns requested.

Encodings that matter for tick data:

ColumnEncodingWhy
ts_event, ts_recv, seq (sorted int64)DELTA_BINARY_PACKEDConsecutive differences are small, often under one byte per value
symbol, venue, conditionsRLE_DICTIONARYLow cardinality per file; each value stored once and referenced by a small integer
price as fixed-point int64DELTA_BINARY_PACKED or dictionaryPrices cluster; deltas are tiny
price as float64BYTE_STREAM_SPLITOnly if you must store floats; splits bytes so the codec can find repetition
sizedictionary or plainRound lots repeat heavily in equities

Compression codecs, in the order you should consider them:

CodecRatioSpeedUse
zstd (level 1 to 3)best general purposefast decodeDefault for normalized and derived tiers
lz4lowerfastestScratch, intraday caches, anything read many times per hour
snappylowerfastLegacy default; no reason to choose it over lz4 today
gzip, brotlihighslowCold archive only

Row group size: around one million rows for trade tables, so a symbol-sorted daily file has a handful of row groups per symbol and statistics prune well. Target file size 100 MB to 1 GB. Object stores charge per request, and per-file overhead dominates below roughly 10 MB.

Timestamps: store as int64 nanoseconds with a documented epoch, or as Arrow timestamp[ns, tz=UTC], which Parquet writes as a nanosecond logical timestamp. Do not let a writer silently coerce to milliseconds; inspect the file schema after the first write.

Arrow IPC files (Feather v2) are right for intraday scratch and for handing tables between processes: memory-mappable, zero-copy, optionally lz4 or zstd compressed. They are not an archive format. Parquet is.

Partitioning and Sort Layout

Hive-style directories (feed=xnas_trades/date=2026-09-01/) are understood by every reader and give partition pruning for free. The real decision is what goes inside a date partition.

Dominant queryLayout inside date=Sort within file
Cross-sectional: all symbols at a time, or a basketOne file, or a few files by symbol hash bucket(symbol, ts_event, seq)
Single-symbol series over monthssymbol= subpartition, only for a few hundred symbols(ts_event, seq)
Both, large universeDate-partitioned and symbol-sorted as canonical; symbol-partitioned bars as a derived copyas above

Partitioning by symbol for thousands of equities multiplied by a daily partition is the forty-million-files mistake. Bucket instead: bucket = hash(symbol) % 16, sort by symbol within each bucket file, and let row-group statistics do the rest.

Futures: partition by product and trade date, with the specific contract as a sorted column, so a continuous-contract builder reads one product's day from one file. Crypto: partition by venue and date; per-venue symbol cardinality is manageable and venues have independent clocks and outages.

kdb-Style Column Stores

kdb+ established the pattern everything else now imitates. A partitioned database is a directory per date; inside it a splayed table with one file per column; a shared sym enumeration file; the sym column carrying the parted attribute (p#) so all rows for one symbol are contiguous; and the time column carrying the sorted attribute (s#). A query for one symbol on one day touches one directory and reads one contiguous slice of each column it needs. The aj as-of join runs at merge speed because both sides are already sorted.

That is exactly the Parquet layout described above: date partition, sorted by (symbol, ts_event), dictionary-encoded symbols. If you inherit a kdb+ system, its layout is a specification worth copying, and its query idioms map onto DuckDB and ClickHouse without much loss.

DuckDB and ClickHouse for Research

DuckDB is an embedded engine that reads Parquet directly with predicate and projection pushdown, understands Hive partitioning, and has a native ASOF JOIN. For one researcher on a workstation with data on local NVMe or object storage it removes the ingestion step entirely.

CREATE VIEW trades AS
SELECT * FROM read_parquet('s3://md/normalized/feed=xnas_trades/date=*/*.parquet',
                           hive_partitioning = true, union_by_name = true);

SELECT date_trunc('minute', ts_event)              AS bar_ts,
       first(price ORDER BY ts_event, seq)         AS open,
       max(price)                                  AS high,
       min(price)                                  AS low,
       last(price ORDER BY ts_event, seq)          AS close,
       sum(size)                                   AS volume
FROM trades
WHERE date = '2026-09-01' AND symbol = 'AAPL'
GROUP BY 1 ORDER BY 1;

SELECT t.ts_event, t.price, q.bid_px, q.ask_px
FROM trades t
ASOF JOIN quotes q
  ON t.symbol = q.symbol AND t.ts_event >= q.ts_event;

ClickHouse is the choice when the store is shared, accepts continuous inserts and serves concurrent queries. The MergeTree engine sorts data by the ORDER BY key on disk and applies per-column codecs.

CREATE TABLE trades (
    ts_event  DateTime64(9, 'UTC')  CODEC(DoubleDelta, ZSTD(3)),
    symbol    LowCardinality(String),
    venue     LowCardinality(String),
    price     Int64                 CODEC(T64, ZSTD(3)),
    size      UInt32                CODEC(T64, ZSTD(3)),
    seq       UInt64                CODEC(DoubleDelta, ZSTD(3))
) ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts_event)
ORDER BY (symbol, ts_event, seq);

DoubleDelta on the timestamp and T64 on bounded integers do the same job as Parquet's delta encoding. ClickHouse also has ASOF JOIN, with the same sortedness requirement.

Rule of thumb: DuckDB over Parquet until more than a couple of people need the same tables at the same time or until inserts become continuous; then ClickHouse (or the firm's existing kdb+) in front of the same Parquet lake.

Schema Evolution

  • Add columns as nullable, at the end. Never insert in the middle, never reuse a name with a different type.
  • To change a type, add price_v2, backfill, switch readers, drop the old column in a later rewrite. A column named price that is float64 in 2024 files and int64 in 2025 files breaks every reader that unions across years.
  • Put schema_version, price_scale and the handler version in file-level metadata and in the manifest. Readers assert on them.
  • Read across versions with union_by_name, and keep a rewrite job that upgrades old partitions to the current schema, recorded in the manifest.
  • Dictionaries are per file. Never write logic that depends on dictionary indices being stable across files.

Cost Arithmetic

Estimate before building: rows per day multiplied by compressed bytes per row. A well-encoded Parquet trade row lands around 10 to 20 bytes; a quote row a little more; a book update less per row but with far more rows.

TierContentsStorageRetention
Rawpcap, vendor binary, JSONObject storage; cold class after 90 daysForever
NormalizedTrades, quotes, book updates as ParquetObject storage, hot classForever
DerivedBars, features, snapshotsNVMe for recent, object storage for the restRebuildable; keep versions in use

Request and egress charges, not capacity, dominate object-storage bills for research workloads. Fewer, larger files and column pruning are the levers. The usual culprit is a research cluster that re-reads a year of quotes every night to rebuild bars it could have cached.

Worked Example: Writing a Partition with pyarrow

import pyarrow as pa
import pyarrow.dataset as ds

def write_trades_partition(table: pa.Table, root: str, date: str) -> None:
    table = table.append_column("date", pa.array([date] * table.num_rows))
    table = table.sort_by([("symbol", "ascending"),
                           ("ts_event", "ascending"),
                           ("seq", "ascending")])
    fmt = ds.ParquetFileFormat()
    opts = fmt.make_write_options(
        compression="zstd",
        compression_level=3,
        use_dictionary=["symbol", "venue"],
        column_encoding={"ts_event": "DELTA_BINARY_PACKED",
                         "ts_recv": "DELTA_BINARY_PACKED",
                         "seq": "DELTA_BINARY_PACKED"},
        write_statistics=True,
    )
    ds.write_dataset(
        table,
        base_dir=f"{root}/feed=xnas_trades",
        format=fmt,
        file_options=opts,
        partitioning=ds.partitioning(pa.schema([("date", pa.string())]), flavor="hive"),
        existing_data_behavior="delete_matching",   # replace the partition, never append
        max_rows_per_group=1_000_000,
        basename_template="part-{i}.parquet",
    )

existing_data_behavior="delete_matching" removes the files of any partition being rewritten before writing, which is what makes a rerun idempotent. After the first write, open the file with pyarrow.parquet.read_metadata and confirm the timestamp column kept nanosecond precision and the statistics are present.

Checklist: Layout Review

  • Dominant query pattern written down, with an estimate of bytes read per query under the proposed layout
  • Files between 100 MB and 1 GB; no partition with thousands of files
  • Sort key matches the filter columns; statistics enabled
  • Timestamps int64 ns or timestamp[ns], never truncated
  • Prices fixed-point with scale in metadata, or a documented reason for floats
  • Codec zstd for at-rest tiers, lz4 for scratch
  • schema_version present in metadata and manifest
  • Partition replacement is atomic; a failed job leaves the previous partition intact
  • Cost estimate for one year of data and one month of typical queries

Anti-Patterns

  • CSV as the canonical store. It has no types, no statistics, no column pruning and no reliable timestamp precision.
  • One Parquet file per symbol per day for a large universe.
  • Appending to Parquet files, which Parquet does not support, by rewriting the whole file each time.
  • Float prices in the canonical layer; the aggregated notional will not reconcile to the exchange.
  • Partitioning on high-cardinality columns such as hour or venue-and-symbol.
  • Running research queries directly over the raw tier.
  • Storing local-time timestamps. See the calendars skill for why.

Limits

This skill covers layout and format for research and batch production stores. Ultra-low-latency in-memory stores for live trading, and the operational side of running ClickHouse or kdb+ clusters (replication, sharding, backup), are separate disciplines. Choose the simplest option that answers your dominant query within budget; storage engines are easier to swap than the layout decisions described here.

Install this skill directly: skilldb add market-data-engineering-skills

Get CLI access →

Related Skills

Trading Calendars and Timezones

Activate this skill when the user is handling exchange sessions, holidays, half days, daylight saving transitions or timezone conversion in market data, or aligning tick data and OHLCV bars across venues that keep different hours. Triggers on "trading calendar," "exchange holidays," "half day," "market hours," "DST," "timezone," "UTC storage," "trade date," "session boundaries," "Globex hours," "crypto 24/7," "venue alignment," or "exchange_calendars." Covers session definitions for equities, futures and crypto, UTC-first storage with exchange-local session logic, trade date versus calendar date, DST asymmetries between regions, overlap windows across venues, and the traps in calendar and timezone libraries.

Market Data Engineering216L

Backtest Data Pipelines

Activate this skill when the user is turning raw tick data or OHLCV bars into research-ready datasets for backtesting and needs to align series, set forward-fill rules, timestamp features correctly, prevent leakage and keep results reproducible. Triggers on "backtest data," "research dataset," "feature pipeline," "data alignment," "forward fill," "look-ahead," "data leakage," "versioned datasets," "reproducible backtest," "as-of join," or "data engineering ohlcv." Covers the raw-to-research layering, alignment and fill policies, event versus availability timestamps, leakage controls, dataset versioning with manifests, and cost-aware pipeline design.

Market Data Engineering161L

Corporate Actions and Price Adjustment

Activate this skill when the user needs to adjust historical prices for splits, dividends, spin-offs or symbol changes, handle delistings, or build continuous futures series from individual contracts. Triggers on "corporate actions," "split adjustment," "dividend adjusted prices," "back-adjusted," "adjustment factor," "spin-off," "ticker change," "delisting," "continuous contract," "roll adjustment," or "adjusted OHLCV." Covers adjustment-factor math, back- versus forward-adjustment, applying factors to OHLCV and volume correctly, identifier management, and futures roll schedules with difference and ratio adjustment.

Market Data Engineering171L

Market Data Quality Checks

Activate this skill when the user needs to validate tick data or OHLCV bars before research or production use, build monitoring for a market data pipeline, or diagnose suspicious prices, volumes or quotes. Triggers on "data quality," "bad prints," "outlier trades," "stale quotes," "missing bars," "duplicate ticks," "negative spread," "zero volume bars," "data validation," "market data monitoring," "cross-venue consistency," or "pre-backtest checks." Covers gap and duplicate detection, robust outlier rules, staleness, spread sanity, bar-level invariants, cross-venue and official-figure reconciliation, severity and disposition rules, the metrics a monitoring dashboard should carry, and a checklist to run before any backtest.

Market Data Engineering202L

OHLCV Bar Construction

Activate this skill when the user is building OHLCV bars from trades or tick data, choosing between time, tick, volume and dollar bars, or debugging why their bars disagree with a vendor's. Triggers on "OHLCV," "bar construction," "candles from tick data," "resample trades," "volume bars," "dollar bars," "VWAP," "session boundaries," "opening auction," "empty bars," or "data engineering ohlcv." Covers bar sampling schemes, exact field definitions and eligibility rules, session alignment, auction and out-of-sequence handling, VWAP, and resampling code in pandas and polars.

Market Data Engineering174L

Order Book Data

Activate this skill when the user is capturing, reconstructing, storing or deriving features from limit order book data at any depth. Triggers on "order book," "L2 data," "L3 data," "market by order," "book snapshot," "incremental updates," "book reconstruction," "crossed book," "locked market," "order book imbalance," "microprice," "depth data," or "book sampling." Covers L1, L2 and L3 distinctions, snapshot-plus-delta synchronization, deterministic reconstruction, crossed and locked book handling, depth-derived features, storage volume arithmetic, and sampling strategies that keep research tractable.

Market Data Engineering168L