tickstreamdocs

DOCS

Historical data archive

Full tick, Level 2 and option-chain history since 2019 over REST — for backtesting and research.

requiresRealtime or above — the window depends on the package

The historical archive serves the complete record — every trade tick, every Level 2 book update, and daily option-chain snapshots (full greeks & open interest) — back to 2019, straight from our Parquet stores as JSON. It's the same data our own research and live-tested algos run on. Base URL: https://api.tick-stream.xyz/v1.

plans

History over the API comes with the futures packages. The window is your package’s (7 days, 1, 5 or 7 years), and a request further back is clamped to it rather than refused. What the store holds differs by symbol: NQ ticks and Level 2 from March 2019; ES ticks from January 2014, ES Level 2 from June 2026; GC, SI and CL ticks from 2017 (with a gap in mid-August 2026), their Level 2 from late August 2026; YM and RTY Level 2 from about July 2026; every other symbol ticks and Level 2 from our own recording, most from late August 2026. Deeper history for the other symbols is sold as files, the historical archive packs below. Buying a pack does not add API access, and the API does not serve the packs’ contents. /history/options is a separate legacy archive; options history for the options packages lives under /v1/options. Earlier one-time NQ archive purchases keep working: ranges past the purchase date are clamped and the response carries snapshot_until. Want to evaluate first? A free 2-month NQ tick sample (Feb – Mar 2025, 22.9M trades, plus two full days of Level-2 depth, in the API’s schemas, not the archive files’) is downloadable without signup. The live snapshot endpoints (/quote, /ticks) are separate and come with any package that streams live.

Common parameters

All three endpoints share the same range and paging contract.

ParamRequiredDescription
start / endnoISO-8601 date-time in UTC (2022-06-13T13:30:00Z) or Unix seconds. Defaults to a short window ending now — pass an explicit range for the archive.
limitnoMax rows per call (default 50,000, max 500,000).

Every response carries count and truncated. When truncated is true, fetch the next page from the last returned ts (rows are time-ordered, oldest first). Range parameters are Unix seconds, but tick and book rows stamp ts in Unix microseconds (options rows in seconds) — so page ticks and book with start = last_ts // 1_000_000 and drop the rows you already have.

A range reaching past your package’s window is clamped, not refused: the response then carries history_clamped: true and the headers X-History-Clamped: true and X-History-Years (the window applied). snapshot_until is set when an earlier one-time archive purchase ends the range at its purchase date, and null otherwise.

GET /history/ticks

Full trade-tick archive for a futures symbol.

ParamRequiredDescription
symbolyesFutures root, e.g. NQ, ES.
curl "https://api.tick-stream.xyz/v1/history/ticks?symbol=NQ&start=2022-06-13T00:00:00Z&end=2022-06-14T00:00:00Z" \
  -H "Authorization: Bearer sk_live_…"
from tickstream import Tickstream
ts = Tickstream("sk_live_…")
r = ts.history.ticks("NQ", start="2022-06-13T00:00:00Z", end="2022-06-14T00:00:00Z")
print(len(r["ticks"]), "ticks")
{
  "symbol": "NQ",
  "start": 1655078400, "end": 1655164800,
  "count": 48213, "truncated": false,
  "snapshot_until": null, "history_clamped": false,
  "ticks": [
    { "ts": 1655128800021114, "price": 12043.25, "size": 2, "side": "buy" }
  ]
}

Each tick: ts, price, size, and side (the aggressor — "buy" or "sell").

Aggressor provenance. From March 2026 onward, ticks are our own live capture and side is the exchange-reported aggressor. For the archived years (ES from Jan 2014, NQ from Mar 2019, GC, SI and CL from 2017, up to Feb 2026) the archive is built from a historical BBO tape (every trade print plus the prevailing best bid/ask); side is inferred with the standard quote rule — trade at the ask = "buy", at the bid = "sell", tick rule inside the spread (<1% of prints remain "unknown"). Re-encoded 2026-07-03; daily volume now matches real contract volume across the whole archive. If you downloaded tick history before that date, re-pull it — the earlier encoding exposed raw quote updates alongside trades.

Known gaps

The tick archive is not gap-free. Since 2017 there are 21 ES sessions and 9 NQ sessions (all between 2017 and early 2020, about 0.6 % of regular sessions) with no prints for part of the regular session — recording holes in the vendor archive. Every affected session is listed with the missing UTC hours in tick-history-gaps.csv; historical /v1/gex?at= answers no_futures_price inside these windows rather than guessing a price. 46 further sessions (2020 – 2026) and the June 2026 roll days were refilled in September 2026 from a second exchange feed, so those days carry prints from two sources with identical fields; our own recording holes of 2 – 4 September 2026 were refilled the same way, ticks and book alike. US market holidays and exchange closures are not in the list.

GET /history/book

Level 2 order-book depth archive — every bid/ask update. Requires Realtime + L2 or above (or an earlier NQ archive purchase).

ParamRequiredDescription
symbolyesFutures root, e.g. NQ.
curl "https://api.tick-stream.xyz/v1/history/book?symbol=NQ&start=2024-01-10T14:30:00Z&end=2024-01-10T15:00:00Z" \
  -H "Authorization: Bearer sk_live_…"
{
  "symbol": "NQ", "count": 12044, "truncated": false,
  "book": [
    { "ts": 1704897000012503, "side": "bid", "level": 1, "price": 16942.50, "size": 14, "flag": 0 }
  ]
}

Each row: ts, side ("bid"/"ask"), level (1-based depth rank, 1 = best level in the stream's window), price, size, and flag.

Two eras, two models. From mid-June 2026 rows are our own live capture: full-book snapshots at a ~250 ms cadence (≈4/s) — all levels of both sides re-stated in each snapshot, flag always 0. Group rows by ts and replace the whole book; no event-sourcing needed. A few refilled windows in September 2026 carry one snapshot per book update instead of the 250 ms cadence — same shape, denser.

Before that — the archive up to mid-June 2026 — the source is an incremental depth tape and flag is the book action: 0 = add, 1 = update, 2 = remove. The replay rules below were derived empirically against two full days (24M events) and are the best-performing of ~10 candidate rule sets we measured:

  • remove → delete by price; the level field on removes is window bookkeeping (reads 10) — ignore it. A miss is a no-op (measured miss rate ~2–4%).
  • add → dedupe the price, then insert sorted (bids descending, asks ascending). An add is "a level entered the visible window" — do not insert at the level index (bottom entries report as level 9).
  • update at level L with price P — three cases vs. the current occupant of slot L: same price → size update; P better (closer to the touch) → insert at L (upward ladder shift); P worse → overwrite slot L (downward shift — the displaced level dies implicitly, no remove is sent for it). Always drop any other slot holding P.
  • Apply no cap and no re-sort of your own — the window maintains itself through the feed's add/remove flow (depth settles at ~9 per side).

Measured on full-day replays (2025-03-03 / 2025-06-13): whole-book consistency 72% / 61% of observations, top-3 levels clean 89% / 82%. The residual inconsistency lives in the lower ranks and is inherent to the vendor tape; for a guaranteed-consistent view, optionally drop any level that breaks strict monotonicity (top-down walk). Rows are stored and served in exact event order; replay each day from its first row (each day opens with an add-sequence snapshot, L1…L9 per side).

Archive window caveat. In the archive era the depth window sits behind the touch: the vendor streams best bid/ask separately, and the book stream's level 1 is typically one tick behind the true best (mode +1 tick, ~75% of the time). Treat the archive book as depth/liquidity structure, not as an exact top-of-book reference — for touch-sensitive work use the trade prints from the tick dataset alongside it, or the snapshot era from mid-June 2026, which is exact.

no L3 in the archive

This archive is aggregated depth. It contains no order-level (market-by-order) data, and neither does any vendor archive we know of — CME MBO history is simply not sold back in time. Order-level data exists only going forward, from the day we started recording it: see L3 / market-by-order.

GET /history/options

Our own recorded option-chain snapshots for QQQ, SPY, DIA and IWM, from about mid-June to early August 2026. A separate, legacy archive purchase — the options packages do not include it and do not need it: their twelve years of OPRA history are the dated request types under /v1/options, e.g. oi and eod with expiration=* for whole chains, and greeks_history per expiration with strike=* for intraday greeks.

ParamRequiredDescription
underlyingyesOption underlying — historical archive covers QQQ, SPY, DIA, IWM.
sourceyesarchive. Without it these roots answer 301 superseded_by_options_package, pointing at /v1/options.
curl "https://api.tick-stream.xyz/v1/history/options?underlying=QQQ&source=archive&start=2026-07-10T00:00:00Z&end=2026-07-11T00:00:00Z" \
  -H "Authorization: Bearer sk_live_…"
{
  "underlying": "QQQ", "count": 8800, "truncated": false,
  "options": [
    { "ts": 1783692000, "expiry": "2026-07-17", "strike": 560, "right": "call",
      "bid": 2.41, "ask": 2.44, "last": 2.42, "oi": 15234, "volume": 812,
      "iv": 0.142, "delta": 0.55, "gamma": 0.04, "theta": -0.08, "vega": 0.21 }
  ]
}

Each row: ts, expiry, strike, right ("call"/"put"), bid, ask, last, oi, volume, iv, and Black-76 greeks delta, gamma, theta, vega.

Archive files — the historical archive packs

The packs bought under Historical archive are monthly files, one per symbol and kind, zstd-compressed CSV (.csv.zst), downloaded from your dashboard. They are a different format from the JSON API above. No header row; every timestamp is UTC; ts is YYYYMMDDHHMMSSffffff (20 digits, microseconds). Monthly files up to August 2026 run from 00:00 New York time, so a summer month’s first row is at 04:00 UTC; from September 2026 each day is appended as a UTC day.

FileColumnsDelimiter · line end
SYM_L1_YYYYMMts;type;side;price;size[;aggressor]; · CRLF
SYM_L2_YYYYMMts;type;side;price;size;level;flag; · CRLF
SYM_1min_YYYYMMYYYYMMDDHHMM,open,high,low,close,volume, · LF
  • L1. type is always 1. side: 0 = best bid update, 1 = best ask update, 2 = trade; rare values 3–9 are session records and can be skipped. size is contracts (for 0/1 the new size at the top of book). aggressor is present on rows appended from our own feed from 21 September 2026: B or S on trades, empty on quotes. Earlier rows have five fields.
  • L2. type is always 2. side: 0 = bid, 1 = ask. level 1–10. flag: 0 = add, 1 = update, 2 = remove. Keep the book keyed by (side, price): 0/1 set the size at that price, 2 removes it. Treat level as informational. From September 2026 the L2 rows are a top-10 positional diff: a level pushed beyond the 10th gets no remove row, so after each update drop any price outside the best 10 of its side.
  • 1-minute bars. The timestamp is the minute’s start; volume is contracts, built from the trades.
  • Contracts. One continuous front-month series per symbol, prices exactly as traded (not back-adjusted); rows carry no contract field.
import pandas as pd
df = pd.read_csv("MNQ_L1_202510.csv.zst", sep=";", header=None,
                 names=["ts", "type", "side", "price", "size", "aggressor"], dtype={"ts": str})
df["ts"] = pd.to_datetime(df["ts"], format="%Y%m%d%H%M%S%f", utc=True)
trades = df[df.side == 2]
tip

Pulling a large range? Page in chunks (e.g. one trading day at a time) and watch truncated. For continuous live data use the WebSocket stream; for recent backfill without the archive plan use /v1/ticks.