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.
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.
| Param | Required | Description |
|---|---|---|
start / end | no | ISO-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. |
limit | no | Max 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.
| Param | Required | Description |
|---|---|---|
symbol | yes | Futures 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).
| Param | Required | Description |
|---|---|---|
symbol | yes | Futures 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
levelfield 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
levelindex (bottom entries report as level 9). - update at level
Lwith priceP— three cases vs. the current occupant of slotL: same price → size update;Pbetter (closer to the touch) → insert atL(upward ladder shift);Pworse → overwrite slotL(downward shift — the displaced level dies implicitly, no remove is sent for it). Always drop any other slot holdingP. - 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.
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.
| Param | Required | Description |
|---|---|---|
underlying | yes | Option underlying — historical archive covers QQQ, SPY, DIA, IWM. |
source | yes | archive. 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.
| File | Columns | Delimiter · line end |
|---|---|---|
SYM_L1_YYYYMM | ts;type;side;price;size[;aggressor] | ; · CRLF |
SYM_L2_YYYYMM | ts;type;side;price;size;level;flag | ; · CRLF |
SYM_1min_YYYYMM | YYYYMMDDHHMM,open,high,low,close,volume | , · LF |
- L1.
typeis always 1.side: 0 = best bid update, 1 = best ask update, 2 = trade; rare values 3–9 are session records and can be skipped.sizeis contracts (for 0/1 the new size at the top of book).aggressoris present on rows appended from our own feed from 21 September 2026:BorSon trades, empty on quotes. Earlier rows have five fields. - L2.
typeis always 2.side: 0 = bid, 1 = ask.level1–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. Treatlevelas 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] 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.